diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97a659744..510946aa1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -900,6 +900,7 @@ jobs: uses: ./.github/workflows/scan.yml with: artifact: droiddeck-signed + variant: standard secrets: inherit packs: diff --git a/.github/workflows/publish-packs.yml b/.github/workflows/publish-packs.yml index ca61616d1..53e26f4ed 100644 --- a/.github/workflows/publish-packs.yml +++ b/.github/workflows/publish-packs.yml @@ -55,7 +55,7 @@ jobs: jq -r --arg n "$2" '.assets // [] | .[] | select(.name == $n) | .digest // empty' } commit=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN" --jq .head_sha 2>/dev/null || echo "$GITHUB_SHA") - for name in proot directaudio tracking xbox comet butler powervr; do + for name in proot directaudio tracking xbox comet butler powervr ubisoft; do file="packs/$name.tar.zst" [ -f "$file" ] || { echo "::error::$name.tar.zst is missing from Build APK run $BUILD_RUN"; exit 1; } sha=$(sha256sum "$file" | cut -d' ' -f1) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 408983955..9790f40c7 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -7,6 +7,10 @@ on: description: Prefix of the signed artifacts to scan, as passed to sign.yml type: string required: true + variant: + description: Variant to scan, as listed in tools/release/variants.txt; every variant when omitted + type: string + default: '*' fail_on_detection: type: boolean default: false @@ -27,7 +31,7 @@ jobs: - uses: actions/download-artifact@v4 with: - pattern: ${{ inputs.artifact }}-* + pattern: ${{ inputs.artifact }}-${{ inputs.variant }} merge-multiple: true path: scan diff --git a/app/build.gradle b/app/build.gradle index c87e70938..28bf184e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,7 +93,7 @@ def trackingPack = rootProject.file('tools/packs/tracking/release.env').readLine 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'].collectEntries { name -> +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") @@ -205,7 +205,7 @@ android { } androidResources { - ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:_*:!CVS:!thumbs.db:!picasa.ini:!*~:droiddeck-xbox:droiddeck-comet:directaudio:hybris' + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:_*:!CVS:!thumbs.db:!picasa.ini:!*~:droiddeck-xbox:droiddeck-comet:droiddeck-ubisoft:directaudio:hybris' } packagingOptions { diff --git a/app/src/main/java/com/droiddeck/launcher/App.kt b/app/src/main/java/com/droiddeck/launcher/App.kt index 2137828dd..be81fc696 100644 --- a/app/src/main/java/com/droiddeck/launcher/App.kt +++ b/app/src/main/java/com/droiddeck/launcher/App.kt @@ -77,6 +77,8 @@ class App : Application() { if (com.droiddeck.launcher.stores.gog.GogComet.enabled(app)) com.droiddeck.launcher.core.AddonPack.COMET.ensure(app) if (com.droiddeck.launcher.stores.StoreAccounts.signedInAs(app, com.droiddeck.launcher.stores.Store.XBOX) != null) com.droiddeck.launcher.core.AddonPack.XBOX.ensure(app) + if (com.droiddeck.launcher.stores.StoreAccounts.signedInAs(app, com.droiddeck.launcher.stores.Store.UBISOFT) != null) + com.droiddeck.launcher.session.SessionFiles.ensureUbisoftService(app) com.droiddeck.launcher.handtracking.HandTracking.load(app) }.onFailure { android.util.Log.w("App", "add-on pack prefetch failed", it) } }, "addon-packs").start() diff --git a/app/src/main/java/com/droiddeck/launcher/core/AddonPack.kt b/app/src/main/java/com/droiddeck/launcher/core/AddonPack.kt index 4f88d90e3..d3d5cf2c7 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/AddonPack.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/AddonPack.kt @@ -152,5 +152,7 @@ internal class AddonPack( @JvmField val POWERVR = AddonPack("powervr", BuildConfig.PACK_POWERVR_SHA256, BuildConfig.PACK_POWERVR_SIZE, listOf("runtime.tzst", "version.txt")) + val UBISOFT = AddonPack("ubisoft", BuildConfig.PACK_UBISOFT_SHA256, BuildConfig.PACK_UBISOFT_SIZE, + listOf("droiddeck-upc.exe")) } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt index 1984f8c93..3ade0b497 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt @@ -33,6 +33,37 @@ object SessionFiles { "etc/xdg/autostart/droiddeck-clipboard.desktop" to "etc/xdg/autostart/droiddeck-clipboard.desktop", ) + private const val UBISOFT_SERVICE = "usr/local/lib/droiddeck-ubisoft/droiddeck-upc.exe" + + /** Fetches the Ubisoft pack and, once it lands, stages the service so a running session gets it without restarting. */ + fun ensureUbisoftService(context: Context) { + AddonPack.UBISOFT.ensure(context) { app -> + com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(app).takeIf { it.isDirectory } + ?.let { stageUbisoftService(app, it, null) } + } + } + + @Synchronized + private fun stageUbisoftService(context: Context, root: File, ledger: StageLedger?) { + val pack = AddonPack.UBISOFT.takeIf { it.installed(context) }?.dir(context) ?: return + val target = File(root, UBISOFT_SERVICE) + if (ledger?.current(UBISOFT_SERVICE, target) == true) return + val staged = File(target.parentFile, target.name + ".staged") + var installed = false + try { + target.parentFile?.mkdirs() + File(pack, "droiddeck-upc.exe").inputStream().use { input -> + staged.outputStream().use { output -> FileUtils.copy(input, output) } + } + installed = staged.setExecutable(true, false) && staged.renameTo(target) + } catch (e: Exception) { + Log.w(TAG, "could not stage the Ubisoft launcher service", e) + } finally { + if (!installed) staged.delete() + } + if (installed) ledger?.staged(UBISOFT_SERVICE, target) else Log.e(TAG, "$UBISOFT_SERVICE NOT staged") + } + fun stage(context: Context, root: File, startsSession: Boolean = false) { if (DesktopCatalog.desktopInstalled(root) && !DesktopCatalog.desktopFromPack(root)) try { DesktopCatalog.cleanLegacy(context) } catch (e: Exception) { Log.w(TAG, "LXQt cleanup will retry next session", e) } @@ -196,7 +227,6 @@ object SessionFiles { "usr/local/lib/droiddeck-pipewire.tzst" to "usr/local/lib/droiddeck-pipewire.tzst", "usr/local/share/licenses/uruntime/LICENSE" to "usr/local/share/licenses/uruntime/LICENSE", "usr/local/lib/droiddeck-dirt/ws2_32.dll" to "usr/local/lib/droiddeck-dirt/ws2_32.dll", - "usr/local/lib/droiddeck-ubisoft/droiddeck-upc.exe" to "usr/local/lib/droiddeck-ubisoft/droiddeck-upc.exe", ) + mangoapp + xclipse.map { it to it } + maliCompat + msitools.map { it to it } + vr + gpuLoad + fexPreloads).filter { (asset, _) -> val dir = asset.substringBeforeLast('/') runCatching { context.assets.list("linuxfs/$dir")?.contains(asset.substringAfterLast('/')) == true }.getOrDefault(false) @@ -252,6 +282,7 @@ object SessionFiles { } if (installed) ledger.staged(key, target) else Log.e(TAG, "DirectAudio $set/$relative NOT staged") } + stageUbisoftService(context, root, ledger) ledger.save() for (old in arrayOf("aarch64-unix", "aarch64-windows", "i386-windows")) File(root, "$DIRECTAUDIO_DIR/lib/wine/$old").deleteRecursively() val preload = StringBuilder("/usr/local/lib/libblsession.so\n") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/ubisoft/UbisoftAuth.kt b/app/src/main/java/com/droiddeck/launcher/stores/ubisoft/UbisoftAuth.kt index 5de002dad..f1cf2ea84 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/ubisoft/UbisoftAuth.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/ubisoft/UbisoftAuth.kt @@ -132,6 +132,7 @@ object UbisoftAuth { } ?: remember.takeIf { it.isNotEmpty() }?.let { rm -> parseSession(session("rm_v1 t=$rm").body, rm) } ?: return "no ticket" save(context, fresh) + com.droiddeck.launcher.session.SessionFiles.ensureUbisoftService(context) return null } diff --git a/tools/packs/build.sh b/tools/packs/build.sh index 7b7e7e76f..4ea6a4193 100755 --- a/tools/packs/build.sh +++ b/tools/packs/build.sh @@ -77,6 +77,7 @@ pack tracking "$work/tracking" hand_landmarker.task hand_landmarker_lite.task po pack xbox "$linuxfs/droiddeck-xbox" version xodus-cli xodus-service pack comet "$linuxfs/droiddeck-comet" version comet pack powervr "$repo/app/src/main/assets/hybris" runtime.tzst version.txt +[ ! -f "$linuxfs/droiddeck-ubisoft/droiddeck-upc.exe" ] || pack ubisoft "$linuxfs/droiddeck-ubisoft" droiddeck-upc.exe mkdir "$work/butler" fetch "https://broth.itch.zone/butler/linux-arm64/${BUTLER_VERSION}/archive/default" \ diff --git a/tools/release/runtime_inputs.py b/tools/release/runtime_inputs.py index 806c49f79..5afaae5f6 100644 --- a/tools/release/runtime_inputs.py +++ b/tools/release/runtime_inputs.py @@ -14,15 +14,16 @@ ROOT = Path(__file__).resolve().parents[2] MANIFEST = Path('app/build/runtime-inputs.json') PACKS = Path('app/build/packs') PACKED = {'assets/linuxfs/usr/local/lib/droiddeck-xbox/': 'xbox', 'assets/linuxfs/usr/local/lib/droiddeck-comet/': 'comet', + 'assets/linuxfs/usr/local/lib/droiddeck-ubisoft/': 'ubisoft', 'assets/directaudio/': 'directaudio', 'assets/hybris/': 'powervr'} PACKED_FILES = {'lib/arm64-v8a/libproot.so': ('proot', 'libproot.so'), 'lib/arm64-v8a/libproot-loader.so': ('proot', 'libproot-loader.so')} PACKED_LIBRARIES = ['lib/arm64-v8a/libopencv_java4.so', 'lib/arm64-v8a/libmediapipe_tasks_jni.so'] -PACK_NAMES = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler', 'powervr'] +PACK_NAMES = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler', 'powervr', 'ubisoft'] SOURCE_PATHS = ['app/build.gradle', 'app/src/main/cpp/fakeinput_steam.cpp', 'app/src/main/assets/pulseaudio.tzst', 'tools/linuxfs', 'tools/proot', 'tools/aaudio-sink', 'tools/directaudio', 'tools/packs', 'tools/gamescope/release.env', 'tools/droiddeck-esync/release.env', - 'tools/mangoapp', 'tools/msitools', 'tools/pipewire', 'tools/comet', 'tools/xbox', 'tools/dirt-ws2', 'tools/components', 'tools/ci', 'tools/build_local.sh', + 'tools/mangoapp', 'tools/msitools', 'tools/pipewire', 'tools/comet', 'tools/xbox', 'tools/dirt-ws2', 'tools/ubisoft-launcher', 'tools/components', 'tools/ci', 'tools/build_local.sh', 'tools/powervr', 'tools/hybris', 'tools/dxvk', 'tools/vulkan-wsi', 'tools/release/runtime_inputs.py', 'app/src/main/assets/desktop-removal', 'tools/quest', 'tools/vr/src', 'tools/vr/driver_droiddeck', 'tools/vr/layer', 'tools/vr/build.sh', @@ -211,7 +212,8 @@ def pack_digests(root, name): def check_apk(apk, root=ROOT): data = verify(root) - packs = {name: pack_digests(root, name) for name in PACK_NAMES} + packs = {name: pack_digests(root, name) for name in PACK_NAMES + if name != 'ubisoft' or (root / PACKS / 'ubisoft.tar.zst').is_file()} with ZipFile(apk) as package: names = set(package.namelist()) for library in PACKED_LIBRARIES: @@ -231,7 +233,7 @@ def check_apk(apk, root=ROOT): if prefix: if name in names: raise ValueError('APK still carries ' + name + ', which the ' + PACKED[prefix] + ' pack provides') - if packs[PACKED[prefix]].get(name[len(prefix):]) != output['sha256']: + if packs.get(PACKED[prefix], {}).get(name[len(prefix):]) != output['sha256']: raise ValueError('Add-on pack ' + PACKED[prefix] + ' is missing or has a stale ' + name) continue try: diff --git a/tools/tests/test_runtime_inputs.py b/tools/tests/test_runtime_inputs.py index 60c0fc992..906bef4c2 100644 --- a/tools/tests/test_runtime_inputs.py +++ b/tools/tests/test_runtime_inputs.py @@ -33,6 +33,9 @@ class RuntimeInputsTest(unittest.TestCase): self.source = self.root / 'tools/linuxfs/preload/test.c' self.source.parent.mkdir(parents=True) self.source.write_text('current source') + service = self.root / 'app/src/main/assets/linuxfs/usr/local/lib/droiddeck-ubisoft/droiddeck-upc.exe' + service.parent.mkdir(parents=True) + service.write_bytes(b'MZ service') self.audio(['pactl', 'modules/arm64/module-aaudio-sink.so', 'modules/arm64/module-directaudio-native-sink.so']) self.packs() @@ -179,6 +182,13 @@ class RuntimeInputsTest(unittest.TestCase): with self.assertRaisesRegex(ValueError, 'xbox pack provides'): runtime.check_apk(apk, self.root) + def test_an_apk_still_carrying_the_ubisoft_service_is_rejected(self): + name = 'assets/linuxfs/usr/local/lib/droiddeck-ubisoft/droiddeck-upc.exe' + self.assertIn(name, runtime.outputs(self.root)) + apk = self.current_apk(self.root / 'bundled.apk', {name: b'MZ service'}) + with self.assertRaisesRegex(ValueError, 'ubisoft pack provides'): + runtime.check_apk(apk, self.root) + def test_an_apk_still_carrying_the_powervr_runtime_is_rejected(self): apk = self.current_apk(self.root / 'bundled.apk', {'assets/hybris/runtime.tzst': b'runtime'}) with self.assertRaisesRegex(ValueError, 'powervr pack provides'):