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'):