mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-09 01:59:35 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48fa1df3ba | ||
|
|
2770a481c2 | ||
|
|
d80e4485ac | ||
|
|
1e23bb6d8d | ||
|
|
9222a1fe4d |
No files matched your search
@@ -450,7 +450,7 @@ jobs:
|
||||
~/.cargo/git/db
|
||||
~/.cargo/bin/cargo-ndk
|
||||
app/src/main/rust/stores/target
|
||||
key: stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-${{ hashFiles('app/src/main/rust/stores/src/**', 'app/src/main/rust/stores/Cargo.toml', 'app/src/main/rust/stores/build.rs', 'app/src/main/rust/stores/.cargo/**', 'app/build.gradle') }}
|
||||
key: stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-${{ hashFiles('app/src/main/rust/stores/amazon/**', 'tools/stores/**', 'app/src/main/rust/stores/src/**', 'app/src/main/rust/stores/Cargo.toml', 'app/src/main/rust/stores/build.rs', 'app/src/main/rust/stores/.cargo/**', 'app/build.gradle') }}
|
||||
restore-keys: |
|
||||
stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-
|
||||
stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-
|
||||
@@ -523,6 +523,13 @@ jobs:
|
||||
cd app/src/main/rust/stores
|
||||
cargo test --locked 2>&1 | tail -60
|
||||
|
||||
- name: Amazon engine host tests
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export CARGO_TARGET_DIR="$PWD/app/src/main/rust/stores/target"
|
||||
cargo test --manifest-path app/src/main/rust/stores/amazon/Cargo.toml --locked
|
||||
|
||||
- name: Upload the regenerated Cargo.lock
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -61,7 +61,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; do
|
||||
for name in proot directaudio tracking xbox comet amazon; 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)
|
||||
|
||||
@@ -15,6 +15,7 @@ app/src/main/jniLibs/arm64-v8a/.proot-outputs
|
||||
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
|
||||
app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so
|
||||
app/src/main/rust/stores/target/
|
||||
app/src/main/rust/amazon/target/
|
||||
app/src/main/assets/droiddeck-esync/
|
||||
|
||||
.kotlin/
|
||||
|
||||
+3
-2
@@ -87,7 +87,7 @@ def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
|
||||
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 addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet'].collectEntries { name ->
|
||||
def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'amazon'].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")
|
||||
@@ -296,6 +296,7 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
|
||||
inputs.dir(rustCrateDir.dir('src'))
|
||||
inputs.files(rustCrateDir.file('Cargo.toml'), rustCrateDir.file('Cargo.lock'))
|
||||
inputs.property('ndkVersion', android.ndkVersion)
|
||||
inputs.property('storeFeatures', 'gog,epic')
|
||||
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'
|
||||
@@ -311,7 +312,7 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
|
||||
}.standardOutput.asText.get().trim()
|
||||
commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26',
|
||||
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
|
||||
'build', '--release', '--locked'
|
||||
'build', '--release', '--locked', '--no-default-features', '--features', 'gog,epic'
|
||||
}
|
||||
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
|
||||
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
|
||||
|
||||
@@ -146,5 +146,7 @@ internal class AddonPack(
|
||||
listOf("version", "xodus-cli", "xodus-service"))
|
||||
val COMET = AddonPack("comet", BuildConfig.PACK_COMET_SHA256, BuildConfig.PACK_COMET_SIZE,
|
||||
listOf("version", "comet"))
|
||||
val AMAZON = AddonPack("amazon", BuildConfig.PACK_AMAZON_SHA256, BuildConfig.PACK_AMAZON_SIZE,
|
||||
listOf("engine.json", "libdroiddeckamazon.so"), reuseDeviceCopy = false)
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,7 @@ class StoreLoginActivity : ComponentActivity() {
|
||||
private fun newFlow(state: String): Flow? {
|
||||
val url = clientUrl
|
||||
if (url != null) return com.droiddeck.launcher.stores.epic.EpicClient.loginFlow(url, chromeUa)
|
||||
if (store == Store.AMAZON && !com.droiddeck.launcher.stores.amazon.AmazonNative.loadInstalled(applicationContext)) return null
|
||||
return store?.let { LoginFlows.forStore(it, state) }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.droiddeck.launcher.stores.amazon
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.stores.CatalogItem
|
||||
import com.droiddeck.launcher.stores.InstalledStoreGame
|
||||
import com.droiddeck.launcher.stores.LoginFlows
|
||||
@@ -16,25 +18,53 @@ import org.json.JSONObject
|
||||
|
||||
object AmazonBackend : StoreBackend {
|
||||
override val store: Store = Store.AMAZON
|
||||
private val signingIn = java.util.concurrent.atomic.AtomicBoolean(false)
|
||||
|
||||
init {
|
||||
LoginFlows.amazonFlow = { AmazonLogin.create() }
|
||||
}
|
||||
|
||||
private fun engineMissing(): Boolean {
|
||||
if (AmazonNative.ready()) return false
|
||||
StoresState.logLine(AmazonNative.ENGINE_MISSING)
|
||||
StoresState.post { StoresState.problems[store] = AmazonNative.ENGINE_MISSING }
|
||||
return true
|
||||
}
|
||||
|
||||
override fun signIn(context: Context) {
|
||||
if (engineMissing()) return
|
||||
context.startActivity(
|
||||
Intent(context, StoreLoginActivity::class.java)
|
||||
.putExtra(StoreLoginActivity.EXTRA_STORE, store.id)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
if (!signingIn.compareAndSet(false, true)) return
|
||||
val app = context.applicationContext
|
||||
try {
|
||||
Thread({
|
||||
try {
|
||||
val available = runCatching {
|
||||
AmazonNative.prepare(app) { state ->
|
||||
if (state is com.droiddeck.launcher.core.AddonPack.State.Downloading) {
|
||||
StoresState.post { StoresState.status[store] = app.getString(R.string.stores_engine_downloading, store.label, (state.progress * 100).toInt()) }
|
||||
}
|
||||
}
|
||||
}.onFailure { Log.w("AmazonBackend", "Engine preparation failed", it) }.getOrDefault(false)
|
||||
StoresState.post {
|
||||
try {
|
||||
StoresState.status.remove(store)
|
||||
if (available) {
|
||||
StoresState.problems.remove(store)
|
||||
app.startActivity(Intent(app, StoreLoginActivity::class.java)
|
||||
.putExtra(StoreLoginActivity.EXTRA_STORE, store.id)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
} else {
|
||||
StoresState.problems[store] = app.getString(R.string.stores_engine_failed, store.label)
|
||||
}
|
||||
} finally {
|
||||
signingIn.set(false)
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
signingIn.set(false)
|
||||
Log.w("AmazonBackend", "Could not finish engine preparation", error)
|
||||
}
|
||||
}, "amazon-engine").start()
|
||||
} catch (error: Exception) {
|
||||
signingIn.set(false)
|
||||
StoresState.post {
|
||||
StoresState.status.remove(store)
|
||||
StoresState.problems[store] = app.getString(R.string.stores_engine_failed, store.label)
|
||||
}
|
||||
Log.w("AmazonBackend", "Could not start engine preparation", error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun signOut(context: Context) {
|
||||
|
||||
@@ -75,6 +75,7 @@ object AmazonCredentialStore {
|
||||
|
||||
private fun refreshLocked(context: Context, a: Account): String? {
|
||||
if (a.refreshToken.isEmpty()) return null
|
||||
if (!AmazonNative.prepare(context)) return null
|
||||
val answer = AmazonNative.call("auth.refresh", JSONObject().put("refreshToken", a.refreshToken))
|
||||
val access = answer?.takeIf { !it.has("error") }?.optString(ACCESS_TOKEN, "").orEmpty()
|
||||
if (access.isEmpty()) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.droiddeck.launcher.stores.amazon
|
||||
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import org.json.JSONObject
|
||||
|
||||
internal object AmazonEngine {
|
||||
fun verify(directory: File): Boolean = runCatching {
|
||||
val manifest = File(directory, "engine.json")
|
||||
val library = File(directory, "libdroiddeckamazon.so")
|
||||
val root = directory.canonicalFile
|
||||
check(manifest.canonicalFile == File(root, manifest.name) && library.canonicalFile == File(root, library.name))
|
||||
check(manifest.length() in 1..131_072 && library.isFile)
|
||||
val data = JSONObject(manifest.readText())
|
||||
check(data.getInt("schema") == 1 && data.getInt("abi") == 1)
|
||||
check(data.getString("engine") == "amazon" && data.getString("target") == "aarch64-linux-android")
|
||||
val hash = MessageDigest.getInstance("SHA-256")
|
||||
library.inputStream().use { input ->
|
||||
val block = ByteArray(64 * 1024)
|
||||
while (true) {
|
||||
val count = input.read(block)
|
||||
if (count < 0) break
|
||||
hash.update(block, 0, count)
|
||||
}
|
||||
}
|
||||
val actual = hash.digest().joinToString("") { "%02x".format(it.toInt() and 0xff) }
|
||||
check(actual == data.getString("library_sha256"))
|
||||
true
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
@@ -24,6 +24,11 @@ internal class AmazonInstaller(
|
||||
private val cancelled = AtomicBoolean(false)
|
||||
|
||||
override fun run(handle: DownloadQueue.JobHandle): String? {
|
||||
handle.stage(DownloadStage.MANIFEST, app.getString(R.string.stores_engine_preparing, Store.AMAZON.label))
|
||||
if (!AmazonNative.prepare(app, { !cancelled.get() })) {
|
||||
if (cancelled.get()) return null
|
||||
throw IllegalStateException(AmazonNative.ENGINE_MISSING)
|
||||
}
|
||||
handle.stage(DownloadStage.MANIFEST, app.getString(R.string.stores_sync_sign_in))
|
||||
val token = AmazonCredentialStore.getValidAccessToken(app) ?: throw IllegalStateException("Not signed in to Amazon Games")
|
||||
val entitlementId = item.extra["entitlementId"].orEmpty()
|
||||
|
||||
@@ -163,7 +163,6 @@ object AmazonLibrary {
|
||||
}
|
||||
try {
|
||||
if (cache.isNotEmpty()) publish(app, cache)
|
||||
if (!AmazonNative.ready()) return problem(AmazonNative.ENGINE_MISSING)
|
||||
val syncedAt = prefs.getLong(KEY_SYNCED_AT, 0L)
|
||||
if (!force && cache.isNotEmpty() && System.currentTimeMillis() - syncedAt in 0 until THROTTLE_MS) {
|
||||
backfillPosters(app, cache)
|
||||
@@ -173,6 +172,7 @@ object AmazonLibrary {
|
||||
|
||||
status(R.string.stores_sync_sign_in)
|
||||
val account = AmazonCredentialStore.load(app) ?: return problem(StoresState.notSignedInLine(store))
|
||||
if (!AmazonNative.prepare(app)) return problem(AmazonNative.ENGINE_MISSING)
|
||||
val token = AmazonCredentialStore.getValidAccessToken(app) ?: return problem(StoresState.notSignedInLine(store))
|
||||
status(R.string.stores_sync_game_list)
|
||||
var answer = entitlements(token, account.deviceSerial)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.droiddeck.launcher.stores.amazon
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.BuildConfig
|
||||
import com.droiddeck.launcher.stores.StoresNative
|
||||
import com.droiddeck.launcher.core.AddonPack
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -16,16 +17,32 @@ object AmazonNative {
|
||||
|
||||
@Volatile private var configured = false
|
||||
|
||||
fun ready(): Boolean {
|
||||
if (!StoresNative.ensureLoaded()) return false
|
||||
if (!configured) synchronized(this) {
|
||||
if (!configured) {
|
||||
runCatching { nativeConfigure(BuildConfig.VERSION_NAME) }
|
||||
.onFailure { Log.w(TAG, "configure: ${it.javaClass.simpleName}") }
|
||||
configured = true
|
||||
@Volatile private var loaded = false
|
||||
|
||||
fun ready(): Boolean = loaded && configured
|
||||
|
||||
internal fun prepare(context: Context, running: () -> Boolean = { true },
|
||||
onState: (AddonPack.State) -> Unit = {}): Boolean {
|
||||
if (ready()) return true
|
||||
if (!AddonPack.AMAZON.await(context, running, onState)) return false
|
||||
return loadInstalled(context)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
internal fun loadInstalled(context: Context): Boolean {
|
||||
if (ready()) return true
|
||||
if (!AddonPack.AMAZON.installed(context)) return false
|
||||
return runCatching {
|
||||
check(AmazonEngine.verify(AddonPack.AMAZON.dir(context))) { "Amazon engine verification failed" }
|
||||
if (!loaded) {
|
||||
System.load(AddonPack.AMAZON.file(context, "libdroiddeckamazon.so").absolutePath)
|
||||
check(nativeEngineAbi() == 1) { "Incompatible Amazon engine" }
|
||||
loaded = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
nativeConfigure(BuildConfig.VERSION_NAME)
|
||||
configured = true
|
||||
true
|
||||
}.onFailure { Log.w(TAG, "Amazon engine could not load", it) }.getOrDefault(false)
|
||||
}
|
||||
|
||||
fun call(op: String, request: JSONObject = JSONObject()): JSONObject? {
|
||||
@@ -109,6 +126,7 @@ object AmazonNative {
|
||||
return holder[0] ?: InstallOutcome(false, false, "The Amazon install ended without a result", null)
|
||||
}
|
||||
|
||||
@JvmStatic private external fun nativeEngineAbi(): Int
|
||||
@JvmStatic private external fun nativeConfigure(appVersion: String)
|
||||
@JvmStatic private external fun nativeCall(op: String, request: String): String?
|
||||
@JvmStatic private external fun nativeFetch(op: String, request: String): ByteArray?
|
||||
|
||||
@@ -3064,4 +3064,7 @@
|
||||
<string name="spatial_saved_views_hint">Saved views can be recalculated. Start a new calibration to capture in the current tracking frame.</string>
|
||||
<string name="spatial_model_checksum_error">Hand model checksum mismatch</string>
|
||||
<string name="spatial_opencv_load_error">OpenCV could not load</string>
|
||||
<string name="stores_engine_downloading">Downloading %1$s support… %2$d%%</string>
|
||||
<string name="stores_engine_preparing">Preparing %1$s support…</string>
|
||||
<string name="stores_engine_failed">%1$s support could not be downloaded or loaded. Try again.</string>
|
||||
</resources>
|
||||
Generated
+16
@@ -182,6 +182,22 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "droiddeckamazon"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"jni",
|
||||
"libc",
|
||||
"lzma-rs",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "droiddeckstores"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -9,6 +9,13 @@ publish = false
|
||||
name = "droiddeckstores"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["gog", "epic", "amazon"]
|
||||
gog = []
|
||||
epic = []
|
||||
amazon = []
|
||||
amazon-pack = ["amazon"]
|
||||
|
||||
[dependencies]
|
||||
jni = "0.21"
|
||||
libc = "0.2"
|
||||
@@ -25,3 +32,8 @@ sha2 = "0.10"
|
||||
codegen-units = 1
|
||||
lto = "thin"
|
||||
strip = "symbols"
|
||||
|
||||
[workspace]
|
||||
members = ["amazon"]
|
||||
default-members = ["."]
|
||||
resolver = "2"
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "droiddeckamazon"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "droiddeckamazon"
|
||||
crate-type = ["cdylib"]
|
||||
path = "../src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["amazon-pack"]
|
||||
gog = []
|
||||
epic = []
|
||||
amazon = []
|
||||
amazon-pack = ["amazon"]
|
||||
|
||||
[dependencies]
|
||||
jni = "0.21"
|
||||
libc = "0.2"
|
||||
lzma-rs = { version = "0.3", default-features = false }
|
||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip"] }
|
||||
tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "sync"] }
|
||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
@@ -24,6 +24,14 @@ fn guarded<T>(fallback: T, f: impl FnOnce() -> T) -> T {
|
||||
catch_unwind(AssertUnwindSafe(f)).unwrap_or(fallback)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeEngineAbi(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jni::sys::jint {
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeConfigure(
|
||||
mut env: JNIEnv,
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
#![allow(clippy::missing_safety_doc, clippy::result_large_err)]
|
||||
|
||||
#[cfg(feature = "amazon")]
|
||||
pub mod amazon;
|
||||
#[cfg(feature = "epic")]
|
||||
pub mod epic;
|
||||
pub mod fetch_core;
|
||||
pub mod fetch_tuning;
|
||||
#[cfg(feature = "gog")]
|
||||
pub mod gog;
|
||||
pub mod md5_small;
|
||||
pub mod priority;
|
||||
|
||||
@@ -6,6 +6,7 @@ use jni::JNIEnv;
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(not(feature = "amazon-pack"))]
|
||||
pub extern "system" fn Java_com_droiddeck_launcher_stores_StoresNative_nativeVersion(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.droiddeck.launcher.stores.amazon
|
||||
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import java.nio.file.Files
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Assume.assumeFalse
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AmazonEngineTest {
|
||||
@get:Rule val temp = TemporaryFolder()
|
||||
|
||||
private fun prepared(): File = temp.newFolder().apply {
|
||||
val library = File(this, "libdroiddeckamazon.so").apply { writeBytes(byteArrayOf(1, 2, 3)) }
|
||||
val hash = MessageDigest.getInstance("SHA-256").digest(library.readBytes())
|
||||
.joinToString("") { "%02x".format(it.toInt() and 0xff) }
|
||||
File(this, "engine.json").writeText(JSONObject().put("schema", 1).put("abi", 1)
|
||||
.put("engine", "amazon").put("target", "aarch64-linux-android")
|
||||
.put("library_sha256", hash).toString())
|
||||
}
|
||||
|
||||
@Test fun installedEngineWorksRepeatedlyWithoutNetwork() {
|
||||
val directory = prepared()
|
||||
assertTrue(AmazonEngine.verify(directory))
|
||||
assertTrue(AmazonEngine.verify(directory))
|
||||
}
|
||||
|
||||
@Test fun changedLibraryIsRejected() {
|
||||
val directory = prepared()
|
||||
File(directory, "libdroiddeckamazon.so").appendText("tampered")
|
||||
assertFalse(AmazonEngine.verify(directory))
|
||||
}
|
||||
|
||||
@Test fun incompatibleInterfaceIsRejected() {
|
||||
val directory = prepared()
|
||||
val manifest = File(directory, "engine.json")
|
||||
manifest.writeText(JSONObject(manifest.readText()).put("abi", 2).toString())
|
||||
assertFalse(AmazonEngine.verify(directory))
|
||||
}
|
||||
|
||||
@Test fun incompleteAndMalformedPacksAreRejected() {
|
||||
val directory = prepared()
|
||||
File(directory, "libdroiddeckamazon.so").delete()
|
||||
assertFalse(AmazonEngine.verify(directory))
|
||||
val malformed = prepared()
|
||||
File(malformed, "engine.json").writeText("not json")
|
||||
assertFalse(AmazonEngine.verify(malformed))
|
||||
}
|
||||
|
||||
@Test fun parentStorageAliasIsAllowedButLibrarySymlinkIsRejected() {
|
||||
assumeFalse(System.getProperty("os.name").startsWith("Windows"))
|
||||
val directory = prepared()
|
||||
val alias = File(temp.root, "alias")
|
||||
Files.createSymbolicLink(alias.toPath(), directory.toPath())
|
||||
assertTrue(AmazonEngine.verify(alias))
|
||||
val library = File(directory, "libdroiddeckamazon.so")
|
||||
val outside = temp.newFile("outside.so").apply { writeBytes(library.readBytes()) }
|
||||
library.delete()
|
||||
Files.createSymbolicLink(library.toPath(), outside.toPath())
|
||||
assertFalse(AmazonEngine.verify(directory))
|
||||
}
|
||||
}
|
||||
@@ -83,3 +83,11 @@ pack tracking "$work/tracking" hand_landmarker.task hand_landmarker_lite.task po
|
||||
libopencv_java4.so libmediapipe_tasks_jni.so
|
||||
pack xbox "$linuxfs/droiddeck-xbox" version xodus-cli xodus-service
|
||||
pack comet "$linuxfs/droiddeck-comet" version comet
|
||||
if [[ "${DROIDDECK_SKIP_RUST:-0}" == 1 && "${CI:-false}" != true ]]; then
|
||||
rm -f "$out/amazon.tar.zst"
|
||||
else
|
||||
bash "$repo/tools/stores/build-amazon.sh"
|
||||
mkdir "$work/amazon"
|
||||
cp "$repo/app/build/amazon-engine/engine.json" "$repo/app/build/amazon-engine/libdroiddeckamazon.so" "$work/amazon/"
|
||||
pack amazon "$work/amazon" engine.json libdroiddeckamazon.so
|
||||
fi
|
||||
@@ -1,9 +1,13 @@
|
||||
"""Validate the JNI entry points and Android ELF dependency closure in the actual APK."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import tarfile
|
||||
from zipfile import ZipFile
|
||||
|
||||
|
||||
@@ -13,9 +17,43 @@ SYSTEM = set('libc.so libm.so libdl.so liblog.so libandroid.so libz.so libvulkan
|
||||
EXPORTS = ['Java_com_droiddeck_launcher_stores_StoresNative_nativeVersion',
|
||||
'Java_com_droiddeck_launcher_stores_gog_GogNative_nativeStart',
|
||||
'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeCall',
|
||||
'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeInstall',
|
||||
'Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeCall',
|
||||
'Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeInstall']
|
||||
'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeInstall']
|
||||
AMAZON_EXPORTS = ['Java_com_droiddeck_launcher_stores_amazon_AmazonNative_native' + name
|
||||
for name in ['EngineAbi', 'Configure', 'Call', 'Fetch', 'Inspect', 'Install', 'Cancel', 'Release']]
|
||||
|
||||
|
||||
def check_amazon_pack(path, readelf):
|
||||
errors = []
|
||||
raw = subprocess.check_output(['zstd', '-dc', str(path)])
|
||||
with tarfile.open(fileobj=io.BytesIO(raw)) as archive:
|
||||
if sorted(archive.getnames()) != ['engine.json', 'libdroiddeckamazon.so']:
|
||||
return ['Amazon pack must contain exactly its manifest and native engine']
|
||||
for member in archive:
|
||||
if not member.isfile():
|
||||
return ['Amazon pack contains a non-regular file']
|
||||
library = archive.extractfile('libdroiddeckamazon.so').read()
|
||||
metadata = json.load(archive.extractfile('engine.json'))
|
||||
if (metadata.get('schema'), metadata.get('engine'), metadata.get('abi'), metadata.get('target')) != (
|
||||
1, 'amazon', 1, 'aarch64-linux-android'):
|
||||
errors.append('Amazon engine metadata is incompatible')
|
||||
if metadata.get('library_sha256') != hashlib.sha256(library).hexdigest():
|
||||
errors.append('Amazon engine digest differs from its manifest')
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
binary = Path(temp) / 'libdroiddeckamazon.so'
|
||||
binary.write_bytes(library)
|
||||
header = subprocess.check_output([readelf, '-h', str(binary)], text=True)
|
||||
if 'AArch64' not in header or 'ELF64' not in header:
|
||||
errors.append('Amazon engine is not Android ARM64')
|
||||
symbols = subprocess.check_output([readelf, '--dyn-syms', '--wide', str(binary)], text=True)
|
||||
exports = {line.split()[-1] for line in symbols.splitlines()
|
||||
if ' GLOBAL ' in line and ' UND ' not in line and line.split()}
|
||||
errors += ['Amazon library missing ' + symbol for symbol in AMAZON_EXPORTS if symbol not in exports]
|
||||
if any(symbol in exports for symbol in EXPORTS):
|
||||
errors.append('Amazon engine still exports other store entry points')
|
||||
dynamic = subprocess.check_output([readelf, '-d', str(binary)], text=True)
|
||||
errors += ['Amazon engine needs missing ' + name for name in re.findall(r'NEEDED.*\[(.*?)\]', dynamic)
|
||||
if name not in SYSTEM]
|
||||
return errors
|
||||
|
||||
|
||||
def check(apk, readelf, stores=True):
|
||||
@@ -41,8 +79,12 @@ def check(apk, readelf, stores=True):
|
||||
exports = {line.split()[-1] for line in symbols.splitlines()
|
||||
if ' GLOBAL ' in line and ' UND ' not in line and line.split()}
|
||||
errors += ['Store library missing ' + symbol for symbol in EXPORTS if symbol not in exports]
|
||||
if any(symbol in exports for symbol in AMAZON_EXPORTS):
|
||||
errors.append('Bundled store library still contains Amazon entry points')
|
||||
elif library is not None:
|
||||
errors.append('Rust was skipped but the APK contains a cached store library')
|
||||
if 'libdroiddeckamazon.so' in libraries:
|
||||
errors.append('APK still bundles the optional Amazon engine')
|
||||
return errors
|
||||
|
||||
|
||||
@@ -51,8 +93,11 @@ if __name__ == '__main__':
|
||||
parser.add_argument('apk')
|
||||
parser.add_argument('--readelf', default='readelf')
|
||||
parser.add_argument('--without-stores', action='store_true')
|
||||
parser.add_argument('--amazon-pack', default='app/build/packs/amazon.tar.zst')
|
||||
args = parser.parse_args()
|
||||
errors = check(args.apk, args.readelf, not args.without_stores)
|
||||
if not args.without_stores:
|
||||
errors += check_amazon_pack(args.amazon_pack, args.readelf)
|
||||
if errors:
|
||||
parser.exit(1, '\n'.join(errors) + '\n')
|
||||
print('APK native exports and dependencies verified')
|
||||
@@ -2,6 +2,7 @@
|
||||
import hashlib
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -17,10 +18,14 @@ PACKS = Path('app/build/packs')
|
||||
PACKED = {'assets/linuxfs/usr/local/lib/droiddeck-xbox/': 'xbox', 'assets/linuxfs/usr/local/lib/droiddeck-comet/': 'comet',
|
||||
'assets/directaudio/': 'directaudio'}
|
||||
PACKED_FILES = {'lib/arm64-v8a/libproot.so': ('proot', 'libproot.so'),
|
||||
'lib/arm64-v8a/libproot-loader.so': ('proot', 'libproot-loader.so')}
|
||||
'lib/arm64-v8a/libproot-loader.so': ('proot', 'libproot-loader.so'),
|
||||
'lib/arm64-v8a/libdroiddeckamazon.so': ('amazon', 'libdroiddeckamazon.so'),
|
||||
'assets/store-engines/amazon/engine.json': ('amazon', 'engine.json')}
|
||||
PACKED_LIBRARIES = ['lib/arm64-v8a/libopencv_java4.so', 'lib/arm64-v8a/libmediapipe_tasks_jni.so']
|
||||
PACK_NAMES = ['proot', 'directaudio', 'tracking', 'xbox', 'comet']
|
||||
PACK_NAMES = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'amazon']
|
||||
SOURCE_PATHS = ['app/build.gradle', 'app/src/main/cpp/fakeinput_steam.cpp', 'app/src/main/assets/pulseaudio.tzst',
|
||||
'app/src/main/rust/stores/src', 'app/src/main/rust/stores/Cargo.toml',
|
||||
'app/src/main/rust/stores/amazon/Cargo.toml', 'app/src/main/rust/stores/Cargo.lock', 'tools/stores',
|
||||
'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/components', 'tools/ci', 'tools/build_local.sh',
|
||||
@@ -61,6 +66,9 @@ def outputs(root):
|
||||
result['assets/' + tree + '/' + path.relative_to(base).as_posix()] = path.relative_to(root).as_posix()
|
||||
for library in ['libproot.so', 'libproot-loader.so', 'libdirectaudiorelay.so']:
|
||||
result['lib/arm64-v8a/' + library] = 'app/src/main/jniLibs/arm64-v8a/' + library
|
||||
if amazon_enabled():
|
||||
result['lib/arm64-v8a/libdroiddeckamazon.so'] = 'app/build/amazon-engine/libdroiddeckamazon.so'
|
||||
result['assets/store-engines/amazon/engine.json'] = 'app/build/amazon-engine/engine.json'
|
||||
result['assets/pulseaudio.tzst'] = 'app/build/prepared-assets/pulseaudio.tzst'
|
||||
result['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'] = \
|
||||
'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'
|
||||
@@ -70,8 +78,13 @@ def outputs(root):
|
||||
return result
|
||||
|
||||
|
||||
def amazon_enabled():
|
||||
return os.environ.get('CI') == 'true' or os.environ.get('DROIDDECK_SKIP_RUST') != '1'
|
||||
|
||||
|
||||
def required():
|
||||
paths = ['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst']
|
||||
paths = ['lib/arm64-v8a/libdroiddeckamazon.so', 'assets/store-engines/amazon/engine.json'] if amazon_enabled() else []
|
||||
paths += ['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst']
|
||||
paths += [f'assets/graphics_driver/adrenotools-{name}.tzst'
|
||||
for name in ['turnip26.3.0-a6xx', 'turnip26.3.0-quest2']]
|
||||
paths += ['assets/linuxfs/' + name for name in ['libfakeinput.so', 'libblsession.so',
|
||||
@@ -187,7 +200,7 @@ 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 != 'amazon' or amazon_enabled()}
|
||||
with ZipFile(apk) as package:
|
||||
names = set(package.namelist())
|
||||
for library in PACKED_LIBRARIES:
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Amazon engine pilot
|
||||
|
||||
The APK keeps GOG and Epic in `libdroiddeckstores.so`. Amazon is compiled separately
|
||||
as `libdroiddeckamazon.so`, using the same reviewed Rust sources and shared download
|
||||
implementation. Cargo features determine the exported stores; the source is not copied.
|
||||
Both crates share one Cargo workspace and the existing lockfile/target cache.
|
||||
The separate library duplicates some linked Rust dependencies. Measure combined
|
||||
download size before extending this model to other stores.
|
||||
|
||||
`tools/stores/build-amazon.sh` builds Android ARM64/API 26 with the selected NDK and
|
||||
locked Cargo dependencies. `engine.json` records interface version 1, library digest,
|
||||
source/recipe input digests, Rust compiler and NDK metadata. `tools/packs/build.sh`
|
||||
includes exactly that manifest and library in the Amazon pack. APK validation checks
|
||||
the engine's ARM64 ELF header, JNI exports, dependency closure and digest, and rejects
|
||||
Amazon exports left in the bundled store library. Runtime input checks reject stale
|
||||
pack bytes. CI runs the standalone engine's host tests as a required step.
|
||||
|
||||
The signed APK pins the complete pack SHA-256. Existing main-only publication produces
|
||||
an immutable content-addressed release; PR publication attaches the exact same-run pack
|
||||
to the PR release. This metadata documents the build; it is not an independent signed
|
||||
attestation or a reproducibility claim. Cargo's existing incremental cache is used;
|
||||
this pilot does not introduce independent prebuilt engine reuse or faster CI guarantees.
|
||||
|
||||
Amazon sign-in downloads support on a worker before opening the browser. Signed-in
|
||||
library sync and installation prepare it as needed. Other stores and ordinary startup
|
||||
do not request the pack. Existing library cache, credentials, game paths, sidecars and
|
||||
installed games are retained. Once installed, the pinned pack is usable offline.
|
||||
New APKs reuse an existing pack when its digest is unchanged; changed pins download
|
||||
their exact version rather than loading an arbitrary older engine. Loading verifies
|
||||
the manifest/library and JNI interface before configuring the engine. Unpinned local
|
||||
builds intentionally cannot borrow an arbitrary prior Amazon pack.
|
||||
The existing local `DROIDDECK_SKIP_RUST=1` mode omits Amazon too. CI always requires
|
||||
the engine and pack even when that local flag is present.
|
||||
|
||||
Device validation still needs a fresh Amazon sign-in, existing-account upgrade, failed
|
||||
download/retry, offline relaunch, and cancel/resume installation. Robolectric integrity
|
||||
tests and ELF checks do not substitute for Android native loading and live account tests.
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Record the Amazon engine's build inputs in its APK-pinned add-on pack."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def inputs(root=ROOT):
|
||||
paths = ['app/src/main/rust/stores/amazon/Cargo.toml', 'app/src/main/rust/stores/Cargo.lock', 'app/src/main/rust/stores/Cargo.toml',
|
||||
'app/src/main/rust/stores/src/lib.rs', 'app/src/main/rust/stores/src/stores_jni.rs',
|
||||
'app/src/main/rust/stores/src/amazon', 'app/src/main/rust/stores/src/fetch_core.rs',
|
||||
'app/src/main/rust/stores/src/fetch_tuning.rs', 'app/src/main/rust/stores/src/priority.rs',
|
||||
'app/src/main/rust/stores/src/md5_small.rs',
|
||||
'tools/stores/build-amazon.sh', 'tools/stores/amazon-manifest.py']
|
||||
files = []
|
||||
for name in paths:
|
||||
path = root / name
|
||||
files.extend(path.rglob('*') if path.is_dir() else [path])
|
||||
return {p.relative_to(root).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in sorted(set(files)) if p.is_file() and '__pycache__' not in p.parts}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
out, ndk = map(Path, sys.argv[1:])
|
||||
data = {'schema': 1, 'engine': 'amazon', 'abi': 1, 'target': 'aarch64-linux-android',
|
||||
'platform': 26, 'inputs': inputs(),
|
||||
'rustc': subprocess.check_output(['rustup', 'run', 'stable', 'rustc', '-Vv'], text=True),
|
||||
'ndk': (ndk / 'source.properties').read_text(),
|
||||
'library_sha256': hashlib.sha256((out / 'libdroiddeckamazon.so').read_bytes()).hexdigest()}
|
||||
(out / 'engine.json').write_text(json.dumps(data, sort_keys=True, indent=2) + '\n')
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
repo=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
out="$repo/app/build/amazon-engine"
|
||||
export ANDROID_NDK_HOME=${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-${NDK:-${ANDROID_NDK_LATEST_HOME:-}}}}
|
||||
[[ -d "$ANDROID_NDK_HOME" ]] || { echo 'Android NDK required for Amazon engine' >&2; exit 1; }
|
||||
export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"$repo/app/src/main/rust/stores/target"}
|
||||
export RUSTC=$(rustup which --toolchain stable rustc)
|
||||
mkdir -p "$out"
|
||||
cd "$repo/app/src/main/rust/stores/amazon"
|
||||
rustup run stable cargo ndk --target arm64-v8a --platform 26 --output-dir "$repo/app/build/amazon-native" build --release --locked
|
||||
cp "$repo/app/build/amazon-native/arm64-v8a/libdroiddeckamazon.so" "$out/libdroiddeckamazon.so"
|
||||
python3 "$repo/tools/stores/amazon-manifest.py" "$out" "$ANDROID_NDK_HOME"
|
||||
@@ -0,0 +1,77 @@
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / 'tools/release'))
|
||||
import check_android_libraries as native
|
||||
|
||||
|
||||
class AmazonEngineTest(unittest.TestCase):
|
||||
def pack(self, folder, abi=1, library=b'engine', checksum=None, extra=False):
|
||||
manifest = {'schema': 1, 'engine': 'amazon', 'abi': abi, 'target': 'aarch64-linux-android',
|
||||
'library_sha256': checksum or hashlib.sha256(library).hexdigest()}
|
||||
stream = io.BytesIO()
|
||||
files = {'engine.json': json.dumps(manifest).encode(), 'libdroiddeckamazon.so': library}
|
||||
if extra:
|
||||
files['unexpected'] = b'x'
|
||||
with tarfile.open(fileobj=stream, mode='w') as archive:
|
||||
for name, content in files.items():
|
||||
member = tarfile.TarInfo(name)
|
||||
member.size = len(content)
|
||||
archive.addfile(member, io.BytesIO(content))
|
||||
path = Path(folder) / 'amazon.tar.zst'
|
||||
path.write_bytes(stream.getvalue())
|
||||
return path
|
||||
|
||||
def validate(self, path, missing=False, bundled=False):
|
||||
def command(args, **kwargs):
|
||||
if args[0] == 'zstd':
|
||||
return path.read_bytes()
|
||||
if '-h' in args:
|
||||
return 'ELF64 AArch64'
|
||||
if '-d' in args:
|
||||
return 'NEEDED [libc.so]'
|
||||
symbols = native.AMAZON_EXPORTS[:-1] if missing else native.AMAZON_EXPORTS
|
||||
if bundled:
|
||||
symbols = symbols + native.EXPORTS
|
||||
return '\n'.join(' GLOBAL DEFAULT 1 ' + symbol for symbol in symbols)
|
||||
with patch.object(native.subprocess, 'check_output', side_effect=command):
|
||||
return native.check_amazon_pack(path, 'readelf')
|
||||
|
||||
def test_pack_checks_interface_digest_exports_and_isolation(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
self.assertEqual(self.validate(self.pack(directory)), [])
|
||||
self.assertTrue(self.validate(self.pack(directory, abi=2)))
|
||||
self.assertTrue(self.validate(self.pack(directory, checksum='0' * 64)))
|
||||
self.assertTrue(self.validate(self.pack(directory), missing=True))
|
||||
self.assertTrue(self.validate(self.pack(directory), bundled=True))
|
||||
self.assertTrue(self.validate(self.pack(directory, extra=True)))
|
||||
|
||||
def test_manifest_tracks_amazon_and_shared_inputs(self):
|
||||
inputs = runpy.run_path(str(ROOT / 'tools/stores/amazon-manifest.py'))['inputs']
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
amazon = root / 'app/src/main/rust/stores/src/amazon/example.rs'
|
||||
amazon.parent.mkdir(parents=True)
|
||||
amazon.write_text('amazon')
|
||||
common = root / 'app/src/main/rust/stores/src/fetch_core.rs'
|
||||
common.write_text('common')
|
||||
before = inputs(root)
|
||||
common.write_text('changed')
|
||||
self.assertNotEqual(before, inputs(root))
|
||||
self.assertIn(amazon.relative_to(root).as_posix(), before)
|
||||
|
||||
def test_apk_and_publication_use_optional_pack(self):
|
||||
gradle = (ROOT / 'app/build.gradle').read_text(encoding='utf-8')
|
||||
self.assertIn("'--no-default-features', '--features', 'gog,epic'", gradle)
|
||||
self.assertIn('comet amazon; do', (ROOT / '.github/workflows/publish-packs.yml').read_text())
|
||||
self.assertIn('pack amazon', (ROOT / 'tools/packs/build.sh').read_text())
|
||||
@@ -1,6 +1,7 @@
|
||||
import importlib.util
|
||||
import gzip
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path, PureWindowsPath
|
||||
import subprocess
|
||||
import tarfile
|
||||
@@ -44,10 +45,13 @@ class RuntimeInputsTest(unittest.TestCase):
|
||||
|
||||
def packs(self):
|
||||
self.pack('tracking', {library.rsplit('/', 1)[1]: b'library' for library in runtime.PACKED_LIBRARIES})
|
||||
proot = {}
|
||||
packed = {}
|
||||
outputs = runtime.outputs(self.root)
|
||||
for name, (pack, rel) in runtime.PACKED_FILES.items():
|
||||
proot[rel] = (self.root / runtime.outputs(self.root)[name]).read_bytes()
|
||||
self.pack('proot', proot)
|
||||
if name in outputs:
|
||||
packed.setdefault(pack, {})[rel] = (self.root / outputs[name]).read_bytes()
|
||||
for name, files in packed.items():
|
||||
self.pack(name, files)
|
||||
for prefix, name in runtime.PACKED.items():
|
||||
base = self.root / 'app/src/main' / prefix
|
||||
self.pack(name, {path.relative_to(base).as_posix(): path.read_bytes() for path in base.rglob('*') if path.is_file()})
|
||||
@@ -71,6 +75,14 @@ class RuntimeInputsTest(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, 'proot pack provides'):
|
||||
runtime.check_apk(apk, self.root)
|
||||
|
||||
def test_local_rust_skip_cannot_disable_the_amazon_pack_in_ci(self):
|
||||
with mock.patch.dict(os.environ, {'DROIDDECK_SKIP_RUST': '1', 'CI': 'false'}):
|
||||
self.assertFalse(runtime.amazon_enabled())
|
||||
self.assertNotIn('lib/arm64-v8a/libdroiddeckamazon.so', runtime.outputs(self.root))
|
||||
with mock.patch.dict(os.environ, {'DROIDDECK_SKIP_RUST': '1', 'CI': 'true'}):
|
||||
self.assertTrue(runtime.amazon_enabled())
|
||||
self.assertIn('lib/arm64-v8a/libdroiddeckamazon.so', runtime.required())
|
||||
|
||||
def test_missing_proot_cannot_be_attested(self):
|
||||
(self.root / 'app/src/main/jniLibs/arm64-v8a/libproot.so').unlink()
|
||||
with self.assertRaisesRegex(ValueError, 'libproot.so'):
|
||||
|
||||
Reference in new issue
Block a user