Files
Kurt Himebauch dd9363e5f2 fix: ship the Ubisoft launcher service as an add-on pack, scan only the standard APK on main (#2436)
* fix: ship the Ubisoft launcher service as an add-on pack, not inside the APK

* fix: treat the Ubisoft service as a packed tree, stage it when its pack lands

* fix: serialize staging of the Ubisoft service
2026-11-10 16:33:41 -05:00

401 lines
19 KiB
Groovy

plugins {
id 'com.android.application'
}
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
def localBuildOptions = new Properties()
def localBuildOptionsFile = rootProject.layout.projectDirectory.file('local.properties')
if (localBuildOptionsFile.asFile.exists()) {
localBuildOptions.load(new StringReader(providers.fileContents(localBuildOptionsFile).asText.get()))
}
def pythonExecutable = project.findProperty('pythonExecutable')
?: localBuildOptions.getProperty('droiddeck.python', 'python3')
def vitureSdkDir = project.hasProperty('vitureSdkDir') ? project.property('vitureSdkDir') : localBuildOptions.getProperty('droiddeck.vitureSdk', '')
def uvcDir = project.hasProperty('uvcDir') ? project.property('uvcDir') : localBuildOptions.getProperty('droiddeck.uvcDir', '')
if (uvcDir) {
['arm64-v8a/libusb.so', 'arm64-v8a/libuvc.a', 'include/libusb.h', 'include/libuvc/libuvc.h',
'licenses/libusb-COPYING', 'licenses/libuvc-LICENSE.txt'].each { required ->
if (!new File(uvcDir, required).isFile()) throw new GradleException("Incomplete UVC build: missing ${required}")
}
}
if (vitureSdkDir) {
['android/arm64-v8a/libglasses.so', 'android/arm64-v8a/libcarina_vio.so',
'android/arm64-v8a/libcloud_protocol.so', 'include/viture_glasses_provider.h',
'android/LICENSE', 'android/NOTICE'].each { required ->
if (!new File(vitureSdkDir, required).isFile()) {
throw new GradleException("Incomplete VITURE Android SDK: missing ${required}")
}
}
}
def gitValue = { List<String> arguments ->
try {
def output = providers.exec {
commandLine(['git'] + arguments)
workingDir rootProject.projectDir
ignoreExitValue = true
}
output.result.get().exitValue == 0 ? output.standardOutput.asText.get().trim() : null
} catch (Exception ignored) {
null
}
}
def buildLabel = providers.environmentVariable('DROIDDECK_BUILD_LABEL').getOrNull()
if (buildLabel == null || buildLabel.trim().isEmpty()) {
def eventName = providers.environmentVariable('GITHUB_EVENT_NAME').getOrNull()
def eventPath = providers.environmentVariable('GITHUB_EVENT_PATH').getOrNull()
if (eventName == 'pull_request_target' && eventPath) {
try {
def eventFile = layout.file(providers.provider { new File(eventPath) })
def eventText = providers.fileContents(eventFile).asText.get()
def pullRequest = new groovy.json.JsonSlurper().parseText(eventText).pull_request
if (pullRequest?.number && pullRequest?.head?.sha) {
buildLabel = "pr${pullRequest.number} at ${pullRequest.head.sha.take(7)}"
}
} catch (Exception ignored) {
}
}
if (buildLabel == null || buildLabel.trim().isEmpty()) {
def branch = providers.environmentVariable('GITHUB_REF_NAME').getOrNull()
?: gitValue(['branch', '--show-current']) ?: 'local'
def commit = providers.environmentVariable('GITHUB_SHA').getOrNull()
?.take(7) ?: gitValue(['rev-parse', '--short=7', 'HEAD']) ?: 'unknown'
buildLabel = "${branch} at ${commit}"
}
}
def buildOrigin = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true' ? 'remote' : 'local'
def treeState = providers.environmentVariable('DROIDDECK_BUILD_TREE_STATE').getOrNull()
if (!(treeState in ['clean', 'dirty'])) {
def status = gitValue(['status', '--porcelain'])
treeState = status == null ? 'unknown' : (status.isEmpty() ? 'clean' : 'dirty')
}
buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}"
def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull()
?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: ''
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
def prTitle = providers.environmentVariable('DROIDDECK_PR_TITLE').getOrNull() ?: ''
def buildBranch = providers.environmentVariable('GITHUB_HEAD_REF').getOrNull()?.trim()
?: gitValue(['branch', '--show-current']) ?: ''
def escapeJava = { String value -> value.replace('\\', '\\\\').replace('"', '\\"').replace('\n', ' ').replace('\r', ' ') }
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
def commitTime = (gitValue(['show', '-s', '--format=%ct', '--end-of-options', sourceCommit]) ?: '0').with { it.isLong() ? it : '0' }
def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
.findAll { it.contains('=') && !it.startsWith('#') }
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
def trackingPack = rootProject.file('tools/packs/tracking/release.env').readLines()
.findAll { it.contains('=') && !it.startsWith('#') }
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
def prootPackPin = rootProject.file('tools/packs/proot/release.env').readLines()
.findAll { it.contains('=') && !it.startsWith('#') }
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler', 'powervr', 'ubisoft'].collectEntries { name ->
def pack = file("build/packs/${name}.tar.zst")
if (!pack.isFile()) {
if (ciBuild) throw new GradleException("Add-on pack ${pack} is missing: run tools/packs/build.sh app/build/packs")
if (name == 'proot' && prootPackPin.PROOT_PACK_SHA256) {
return [(name): [sha256: prootPackPin.PROOT_PACK_SHA256, size: (prootPackPin.PROOT_PACK_SIZE ?: '0') as long]]
}
return [(name): [sha256: '', size: 0L]]
}
def digest = java.security.MessageDigest.getInstance('SHA-256')
pack.withInputStream { input ->
byte[] buffer = new byte[1 << 20]
int n
while ((n = input.read(buffer)) > 0) digest.update(buffer, 0, n)
}
[(name): [sha256: digest.digest().encodeHex().toString(), size: pack.length()]]
}
def desktopRemovalSources = new groovy.json.JsonSlurper().parse(file('src/main/assets/desktop-removal/sources.json'))
if (desktopRemovalSources.archives[desktopKde.DESKTOP_KDE_TAG] != desktopKde.DESKTOP_KDE_SHA256) {
throw new GradleException('Desktop removal inventories do not match the pinned archive')
}
if (!file('src/main/assets/desktop-removal/versions.txt').readLines().contains(desktopKde.DESKTOP_KDE_TAG)) {
throw new GradleException('Regenerate desktop removal inventories for ' + desktopKde.DESKTOP_KDE_TAG)
}
def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
.findAll { it.trim() && !it.startsWith('#') }
.collect { def f = it.trim().split(/\s+/); "${f[1]}=${f[2] == '-' ? '' : f[2]}" }
.join(';')
android {
namespace 'com.droiddeck.launcher'
compileSdk 34
ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724'
defaultConfig {
applicationId "com.droiddeck.launcher"
minSdkVersion 26
targetSdkVersion 28
versionCode 19
versionName "0.7.1"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
buildConfigField "String", "PR_TITLE", "\"${escapeJava(prTitle)}\""
buildConfigField "String", "BUILD_BRANCH", "\"${escapeJava(buildBranch)}\""
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
buildConfigField "String", "VARIANT_SUFFIXES", "\"${variantSuffixes}\""
buildConfigField "String", "SGDB_API_KEY", "\"${System.getenv("SGDB_API_KEY") ?: project.findProperty("sgdbApiKey") ?: ""}\""
buildConfigField "String", "DESKTOP_KDE_TAG", "\"${desktopKde.DESKTOP_KDE_TAG}\""
buildConfigField "String", "DESKTOP_KDE_URL", "\"https://github.com/${desktopKde.DESKTOP_KDE_REPO}/releases/download/${desktopKde.DESKTOP_KDE_TAG}/desktop-kde.tar.zst\""
buildConfigField "String", "DESKTOP_KDE_SHA256", "\"${desktopKde.DESKTOP_KDE_SHA256}\""
buildConfigField "long", "DESKTOP_KDE_SIZE", "${desktopKde.DESKTOP_KDE_SIZE}L"
addonPacks.each { name, pack ->
buildConfigField "String", "PACK_${name.toUpperCase()}_SHA256", "\"${pack.sha256}\""
buildConfigField "long", "PACK_${name.toUpperCase()}_SIZE", "${pack.size}L"
}
ndk {
abiFilters 'arm64-v8a'
}
externalNativeBuild {
cmake {
arguments '-DANDROID_STL=c++_shared'
arguments "-DDROIDDECK_VITURE_SDK_DIR=${vitureSdkDir.toString().replace('\\', '/')}"
arguments "-DDROIDDECK_UVC_DIR=${uvcDir.toString().replace('\\', '/')}"
}
}
}
externalNativeBuild {
cmake {
path 'src/main/cpp/CMakeLists.txt'
version '3.22.1'
}
}
signingConfigs {
testkey {
storeFile file("${rootDir}/keystore/testkey.p12")
storeType "PKCS12"
storePassword "android"
keyAlias "testkey"
keyPassword "android"
enableV1Signing = true
enableV2Signing = true
enableV3Signing = true
}
}
buildTypes {
debug {
minifyEnabled false
signingConfig signingConfigs.testkey
def localSuffix = localBuildOptions.getProperty('droiddeck.debugApplicationIdSuffix')
if (localSuffix) {
applicationIdSuffix localSuffix
resValue 'string', 'app_name', 'DroidDeck XR Dev'
}
}
release {
minifyEnabled false
signingConfig signingConfigs.testkey
}
}
lint {
disable 'ExpiredTargetSdkVersion'
}
androidResources {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~:<dir>droiddeck-xbox:<dir>droiddeck-comet:<dir>droiddeck-ubisoft:<dir>directaudio:<dir>hybris'
}
packagingOptions {
resources {
excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']
}
jniLibs {
useLegacyPackaging = true
pickFirsts += ['**/libtermux.so']
excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so']
}
}
buildFeatures {
prefab true
compose true
buildConfig true
}
testOptions {
unitTests.returnDefaultValues = true
unitTests.includeAndroidResources = true
unitTests.all {
if (ciBuild) {
outputs.upToDateWhen { false }
outputs.doNotCacheIf('CI executes the test suite on every run') { true }
}
maxHeapSize = '2g'
maxParallelForks = 2
def logTarget = providers.systemProperty('robolectric.logging').orNull
if (logTarget != null) systemProperty 'robolectric.logging', logTarget
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
// No instrumented tests; the unused androidTest variant breaks the VS Code Kotlin import.
androidComponents {
beforeVariants(selector().all()) { it.androidTest.enable = false }
}
// Vendor binaries remain in the user's SDK directory; never vendor them into Git.
if (vitureSdkDir) {
android.sourceSets.main.jniLibs.srcDir(new File(vitureSdkDir, 'android'))
def stageVitureNotices = tasks.register('stageVitureNotices', Copy) {
from(new File(vitureSdkDir, 'android')) { include 'LICENSE', 'NOTICE' }
into(layout.buildDirectory.dir('vitureAssets/licenses/viture'))
}
tasks.named('preBuild').configure { dependsOn(stageVitureNotices) }
}
def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
from(rootProject.file('tools/linuxfs/overlay')) {
include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**'
}
into(layout.projectDirectory.dir('src/main/assets/linuxfs'))
}
tasks.named('preBuild').configure { dependsOn(stageSessionScripts) }
def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets')
def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
dependsOn(stageSessionScripts)
from(layout.projectDirectory.dir('src/main/assets')) { exclude 'pulseaudio.tzst' }
from(rootProject.file('tools/linuxfs/innoextract'))
from(layout.buildDirectory.dir('prepared-assets'))
into(runtimeAssetsDir)
}
android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir])
if (vitureSdkDir) android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('vitureAssets'))
if (uvcDir) {
def stageUvcNotices = tasks.register('stageUvcNotices', Copy) {
from(new File(uvcDir, 'licenses'))
into(layout.buildDirectory.dir('uvcAssets/licenses/uvc'))
}
tasks.named('preBuild').configure { dependsOn(stageUvcNotices) }
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('uvcAssets'))
}
tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) }
def stageGogNotices = tasks.register('stageGogNotices', Copy) {
from(layout.projectDirectory.dir('src/main/rust/stores/vendor/xdelta3')) { include 'LICENSE', 'README.droiddeck.md' }
into(layout.buildDirectory.dir('gogNotices/licenses/xdelta3'))
}
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('gogNotices'))
tasks.named('preBuild').configure { dependsOn(stageGogNotices) }
if (!ciBuild) {
def stageLocalPacks = tasks.register('stageLocalPacks', Sync) {
from(layout.buildDirectory.dir('packs')) { include '*.tar.zst' }
into(layout.buildDirectory.dir('localPackAssets/packs'))
}
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('localPackAssets'))
tasks.named('preBuild').configure { dependsOn(stageLocalPacks) }
}
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
mustRunAfter(stageSessionScripts, stageRuntimeAssets)
workingDir rootProject.projectDir
def allowStale = (project.findProperty('allowStaleRuntime')
?: localBuildOptions.getProperty('droiddeck.allowStaleRuntime', 'false')).toString().toBoolean()
commandLine([pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'] + (allowStale ? ['--allow-stale-sources'] : []))
}
tasks.configureEach { task ->
if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs)
}
def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores')
def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs')
def skipRust = (project.findProperty('skipRust')
?: localBuildOptions.getProperty('droiddeck.skipRust', 'false')).toString().toBoolean()
def buildRustStores = tasks.register('buildRustStores', Exec) {
description = 'Cross-compiles the store download engines (libdroiddeckstores.so) with cargo-ndk.'
onlyIf { !skipRust }
inputs.dir(rustCrateDir.dir('src'))
inputs.dir(rustCrateDir.dir('vendor'))
inputs.file(rustCrateDir.file('build.rs'))
inputs.files(rustCrateDir.file('Cargo.toml'), rustCrateDir.file('Cargo.lock'))
inputs.property('ndkVersion', android.ndkVersion)
if (!skipRust) inputs.property('rustToolchain', providers.exec {
environment 'PATH', "${System.getProperty('user.home')}/.cargo/bin${File.pathSeparator}${System.getenv('PATH')}"
commandLine 'rustup', 'run', 'stable', 'rustc', '-Vv'
}.standardOutput.asText)
outputs.dir(rustJniLibsDir)
workingDir rustCrateDir
def cargoBin = "${System.getProperty('user.home')}/.cargo/bin"
environment 'PATH', "${cargoBin}${File.pathSeparator}${System.getenv('PATH')}"
environment 'ANDROID_NDK_HOME', android.ndkDirectory.absolutePath
environment 'CARGO_TERM_COLOR', 'never'
if (!skipRust) environment 'RUSTC', providers.exec {
commandLine 'rustup', 'which', '--toolchain', 'stable', 'rustc'
}.standardOutput.asText.get().trim()
commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26',
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
'build', '--release', '--locked'
}
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
def maliBridgeDir = rootProject.file('external/MaliBridge')
def vkbridgeJniLibsDir = layout.buildDirectory.dir('vkbridgeJniLibs')
def buildVkBridgeServer = tasks.register('buildVkBridgeServer', Exec) {
description = 'Builds the Vulkan bridge server (libvkbridge_server.so) with the NDK.'
inputs.dir(new File(maliBridgeDir, 'common'))
inputs.dir(new File(maliBridgeDir, 'server'))
inputs.dir(new File(maliBridgeDir, 'third_party'))
inputs.file(new File(maliBridgeDir, 'scripts/build-server-android.sh'))
outputs.dir(vkbridgeJniLibsDir)
commandLine 'bash', new File(maliBridgeDir, 'scripts/build-server-android.sh').absolutePath,
android.ndkDirectory.absolutePath, vkbridgeJniLibsDir.get().asFile.absolutePath
}
android.sourceSets.main.jniLibs.srcDir(vkbridgeJniLibsDir)
tasks.named('preBuild').configure { dependsOn(buildVkBridgeServer) }
dependencies {
implementation "org.opencv:opencv:${trackingPack.OPENCV_VERSION}"
implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'
implementation 'com.github.luben:zstd-jni:1.5.7-4@aar'
implementation 'org.apache.commons:commons-compress:1.27.1'
implementation 'org.tukaani:xz:1.10'
implementation platform('androidx.compose:compose-bom:2024.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'io.coil-kt:coil-compose:2.6.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'com.flyfishxu:kadb-android:1.2.1'
implementation 'com.termux.termux-app:terminal-view:0.118.0'
implementation 'androidx.camera:camera-camera2:1.4.2'
implementation 'androidx.camera:camera-lifecycle:1.4.2'
implementation 'androidx.webkit:webkit:1.12.1'
implementation 'io.github.webrtc-sdk:android:125.6422.07'
implementation("com.google.mediapipe:tasks-vision:${trackingPack.MEDIAPIPE_VERSION}") {
exclude group: 'com.google.auto.value', module: 'auto-value'
}
implementation 'com.google.zxing:core:3.5.3'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.16.1'
testImplementation 'androidx.compose.ui:ui-test-junit4'
testRuntimeOnly 'com.github.luben:zstd-jni:1.5.7-4@jar'
}