Build the store engines with cargo-ndk, binaries out of tree

Gradle task buildRustStores runs cargo ndk for arm64-v8a (API 26) into
app/build/rustJniLibs, one more jniLibs source, before preBuild; -PskipRust=true
leaves the library out for a checkout without a Rust toolchain. cargo-ndk exports
the NDK clang/llvm-ar for ring (rustls), the step Bannerlator's CI does by hand.

CI adds the Android target and cargo-ndk, caches the cargo registry, cargo-ndk
and the crate's target tree keyed on rustc + Cargo.lock, and after the build
checks the exports in the apk's libdroiddeckstores.so and that the lockfile did
not move (uploading it when it did). tools/build_local.sh gets the same step,
DROIDDECK_SKIP_RUST=1 to opt out.
This commit is contained in:
The412Banner committed 2026-10-08 16:17:21 -04:00
1 parent 401a650b5b
commit 05097b0eea
4 files changed
+124 -1

No files matched your search

+66
View File
@@ -405,6 +405,39 @@ jobs:
test -f "$D/usr/local/lib/droiddeck-msitools/$f" || { echo "::error::droiddeck-msitools/$f missing"; exit 1; }
done
# The store download engines (app/src/main/rust/stores) are Rust, built by Gradle's
# buildRustStores task through cargo-ndk. rustup and cargo are on the runner image; the Android
# target and cargo-ndk are added here. Cache = cargo registry + cargo-ndk + the crate's target
# tree, keyed on the rustc version and Cargo.lock, so a source-only change rebuilds this crate
# on top of the cached dependency objects, and a lockfile change still starts from the
# previous objects (restore-keys).
- name: Rust toolchain for the store engines (aarch64-linux-android)
id: rust
run: |
set -euo pipefail
rustup target add aarch64-linux-android
echo "rustc=$(rustc -V | awk '{print $2}')" >> "$GITHUB_OUTPUT"
rustc -V; cargo -V
- name: Cache cargo registry and the store engines' target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin/cargo-ndk
app/src/main/rust/stores/target
key: stores-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}
restore-keys: |
stores-${{ steps.rust.outputs.rustc }}-
- name: cargo-ndk
run: |
set -euo pipefail
command -v cargo-ndk >/dev/null || cargo install cargo-ndk --locked
cargo ndk --version
- name: Cache the CMake build tree
uses: actions/cache@v4
with:
@@ -424,6 +457,39 @@ jobs:
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
python3 tools/release/check_session_assets.py app/build/outputs/apk/release/app-release.apk
- name: Check the store engines' library
# The exports Kotlin binds must be in the apk's libdroiddeckstores.so, or the stores fail on
# the device with an UnsatisfiedLinkError instead of here. The lockfile check is the
# reproducibility guard: cargo must not have had to re-resolve anything.
run: |
set -euo pipefail
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
NM="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm"
SO=app/build/rustJniLibs/arm64-v8a/libdroiddeckstores.so
test -s "$SO"
ls -la "$SO"
unzip -l app/build/outputs/apk/release/app-release.apk | grep -q 'lib/arm64-v8a/libdroiddeckstores.so' \
|| { echo "::error::libdroiddeckstores.so is not in the apk"; exit 1; }
for sym in \
Java_com_droiddeck_launcher_stores_StoresNative_nativeVersion \
Java_com_droiddeck_launcher_stores_gog_GogNative_nativeStart \
Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStart \
Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeStart; do
"$NM" -D --defined-only "$SO" | grep -q " ${sym}\$" \
|| { echo "::error::libdroiddeckstores.so does not export $sym"; exit 1; }
done
git diff --stat -- app/src/main/rust/stores/Cargo.lock
git diff --exit-code -- app/src/main/rust/stores/Cargo.lock \
|| { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; }
- name: Upload the regenerated Cargo.lock
if: failure()
uses: actions/upload-artifact@v4
with:
name: stores-cargo-lock
path: app/src/main/rust/stores/Cargo.lock
if-no-files-found: ignore
- name: Steam import and game environment tests
run: python3 -m unittest discover -s tools/tests -p 'test_*.py'
+3
View File
@@ -15,6 +15,9 @@ app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/.proot-inputs
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so
# The store download engines (app/src/main/rust/stores) are cross-compiled by Gradle into
# app/build/rustJniLibs; cargo's own build tree stays next to the crate. Neither is source.
app/src/main/rust/stores/target/
app/src/main/assets/droiddeck-esync/
# Kotlin compiler session files (build-tool state, never source).
+33
View File
@@ -188,6 +188,39 @@ def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
}
tasks.named('preBuild').configure { dependsOn(stageSessionScripts) }
// The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores, ported from
// Bannerlator's bl-steam-client crate). cargo-ndk cross-compiles them for arm64-v8a into the build
// directory, which is one more jniLibs source below: nothing compiled is committed, the same rule
// the preloads, proot and the DirectAudio relay follow. cargo-ndk exports the NDK clang/llvm-ar as
// the C compiler for the one C-compiling crate (ring, under rustls) - the step Bannerlator's CI
// does by hand - and reads the NDK from ANDROID_NDK_HOME, which is set to the NDK this build pins.
// -PskipRust=true builds without the library, for a checkout with no Rust toolchain: the stores
// then report themselves unavailable instead of the build failing.
def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores')
def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs')
def skipRust = (project.findProperty('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.files(rustCrateDir.file('Cargo.toml'), rustCrateDir.file('Cargo.lock'))
outputs.dir(rustJniLibsDir)
workingDir rustCrateDir
// The configuration cache serialises the task, so everything it needs is resolved here, not
// in doFirst. ~/.cargo/bin is where rustup puts cargo and cargo-ndk; an IDE-launched Gradle
// does not always have it on PATH.
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'
// --platform 26 = minSdkVersion: the engines link against the API 26 libc, like the C++ code.
commandLine 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26',
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
'build', '--release'
}
android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
dependencies {
// The runtime tarball is a zstd-compressed tar.
implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
+22 -1
View File
@@ -331,8 +331,29 @@ tar -cf - -C "${bundle_dir}" . | zstd -19 -T0 -c > "${staging_dir}/pulseaudio.tz
bundle_replaced=1
mv "${staging_dir}/pulseaudio.tzst" "${bundle_asset}"
# The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores); Gradle's
# buildRustStores task cross-compiles them with cargo-ndk, which needs the Android target and the
# cargo-ndk binary installed once. DROIDDECK_SKIP_RUST=1 builds an apk without the engines (the
# stores then report themselves unavailable) for a machine without a Rust toolchain.
gradle_rust_arg=""
if [[ "${DROIDDECK_SKIP_RUST:-0}" == 1 ]]; then
gradle_rust_arg="-PskipRust=true"
else
export PATH="${HOME}/.cargo/bin:${PATH}"
for tool in rustup cargo; do
if ! command -v "${tool}" >/dev/null 2>&1; then
echo "${tool} is required for the store engines (https://rustup.rs); set DROIDDECK_SKIP_RUST=1 to build without them." >&2
exit 1
fi
done
rustup target add aarch64-linux-android
if ! command -v cargo-ndk >/dev/null 2>&1; then
cargo install cargo-ndk --locked
fi
fi
cd "${repo_root}"
./gradlew "${gradle_task}" --console=plain -PndkVersion="${ndk_version}"
./gradlew "${gradle_task}" --console=plain -PndkVersion="${ndk_version}" ${gradle_rust_arg}
python3 tools/release/check_session_assets.py "app/build/outputs/apk/${build_variant}/app-${build_variant}.apk"
cp -p "${bundle_backup}" "${bundle_asset}"
bundle_replaced=0