From 071289b56ec507016ece55e0e2e1f6251144641d Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Tue, 22 Sep 2026 22:31:10 -0400 Subject: [PATCH 1/4] Add reproducible local build and deploy helpers --- .gitignore | 1 + README.md | 25 +++++- app/build.gradle | 2 +- gradle.properties | 2 +- tools/build_local.sh | 147 +++++++++++++++++++++++++++++++++++ tools/deploy_local.sh | 37 +++++++++ tools/local-cross.Dockerfile | 14 ++++ 7 files changed, 222 insertions(+), 6 deletions(-) create mode 100755 tools/build_local.sh create mode 100755 tools/deploy_local.sh create mode 100644 tools/local-cross.Dockerfile diff --git a/.gitignore b/.gitignore index 4a3450942..e07b49901 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build/ captures/ .externalNativeBuild/ .cxx/ +.kotlin/ # Built in CI from tools/linuxfs and app/src/main/cpp/fakeinput.cpp, never committed, so the two # copies cannot drift. app/src/main/assets/linuxfs/ diff --git a/README.md b/README.md index d637cd860..d053c6a81 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,27 @@ first use. ## Building -Nothing is built locally. Push, and the `Build APK` workflow produces `steamdeck-apk`, a -debug-signed arm64 apk. It cross-compiles the two glibc preloads the session needs -(`libfakeinput.so`, `libblsession.so`) from the sources in this repo before the app build, so -those can never drift from what ships. +The repeatable local build is `tools/build_local.sh`. It uses Docker's Ubuntu cross-toolchain for +the two glibc ARM64 preloads (`libfakeinput.so`, `libblsession.so`), the Android SDK/NDK already +installed on the machine, and the same release signing and native dependency checks as CI. The +APK is written to `app/build/outputs/apk/release/app-release.apk`. + +```sh +tools/build_local.sh +``` + +CI remains available through the `Build APK` workflow. Both paths cross-compile the preloads from +the sources in this repo before the app build, so those cannot drift from what ships. + +With an authorized Android device attached over USB or ADB, install the locally built APK with: + +```sh +tools/deploy_local.sh +``` + +Set `ADB_SERIAL` when more than one device is attached. The deploy helper installs over the +existing `com.steamdeck.launcher` package using the repository's stable test key and verifies the +package path afterward; it does not launch a session automatically. ## If Bannerlator is already installed diff --git a/app/build.gradle b/app/build.gradle index 5f6d6a372..063616100 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { compileSdk 34 // The NDK is pinned by property so CI can hand in whatever the runner already has rather than // downloading a second copy; see gradle.properties. - ndkVersion project.findProperty('ndkVersion') ?: '27.0.12077973' + ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724' defaultConfig { applicationId "com.steamdeck.launcher" diff --git a/gradle.properties b/gradle.properties index c9aa3abf6..58de70a52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,4 +10,4 @@ android.useAndroidX=true android.nonTransitiveRClass=true # The NDK the compositor is built with. CI passes the one the runner has installed # (-PndkVersion=…); this is the fallback for a local build. -ndkVersion=27.0.12077973 +ndkVersion=27.3.13750724 diff --git a/tools/build_local.sh b/tools/build_local.sh new file mode 100755 index 000000000..1dda74347 --- /dev/null +++ b/tools/build_local.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +sdk_dir=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-"${HOME}/Library/Android/sdk"}} +java_dir=${JAVA_HOME:-"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"} +image_name=${STEAMDECK_BUILD_IMAGE:-steamdeck-local-cross:24.04-v2} + +if [[ ! -x "${sdk_dir}/platform-tools/adb" ]]; then + echo "Android SDK not found at ${sdk_dir}; set ANDROID_HOME or ANDROID_SDK_ROOT." >&2 + exit 1 +fi +if [[ ! -x "${java_dir}/bin/java" ]]; then + echo "Java 17 not found at ${java_dir}; set JAVA_HOME." >&2 + exit 1 +fi +if ! command -v docker >/dev/null 2>&1; then + echo "Docker is required to cross-compile the glibc ARM64 preload libraries." >&2 + exit 1 +fi + +if ! docker image inspect "${image_name}" >/dev/null 2>&1; then + docker build --platform linux/amd64 -t "${image_name}" \ + -f "${repo_root}/tools/local-cross.Dockerfile" "${repo_root}" +fi + +docker run --rm --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + -v "${repo_root}:/src" -w /src "${image_name}" bash -lc ' + set -euo pipefail + d=app/src/main/assets/linuxfs + mkdir -p "$d" + aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \ + -pthread -std=c++17 -static-libstdc++ -static-libgcc \ + -o "$d/libfakeinput.so" app/src/main/cpp/fakeinput_steam.cpp -ldl + aarch64-linux-gnu-strip --strip-unneeded "$d/libfakeinput.so" + aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \ + -o "$d/libblsession.so" tools/linuxfs/preload/*.c -ldl + aarch64-linux-gnu-strip --strip-unneeded "$d/libblsession.so" + for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do + install -Dm644 "$script" "$d/usr/local/bin/$(basename "$script")" + done + install -Dm644 tools/linuxfs/desktop/steamdeck-desktop "$d/usr/local/bin/steamdeck-desktop" + install -Dm644 tools/linuxfs/desktop/autostart "$d/etc/xdg/labwc/autostart" + install -Dm644 tools/linuxfs/desktop/rc.xml "$d/etc/xdg/labwc/rc.xml" + install -Dm644 tools/linuxfs/desktop/panel.conf "$d/etc/xdg/lxqt/panel.conf" + install -Dm644 tools/linuxfs/desktop/firefox-steamdeck.js \ + "$d/usr/lib/firefox/defaults/pref/steamdeck.js" + + need=$(aarch64-linux-gnu-readelf -d "$d/libfakeinput.so" | sed -n "s/.*NEEDED.*\\[\\(.*\\)\\]/\\1/p") + for bad in libstdc++.so.6 libgcc_s.so.1; do + if printf "%s\\n" "$need" | grep -qx "$bad"; then + echo "libfakeinput.so links $bad; the C++ runtime must stay static" >&2 + exit 1 + fi + done + syms() { aarch64-linux-gnu-readelf -Ws "$1" | awk '\''$4 == "FUNC" && $5 == "GLOBAL" {sub(/@.*/, "", $8); print $8}'\''; } + fake=$(syms "$d/libfakeinput.so") + for sym in open openat ioctl read close poll ppoll select stat fstat access scandir; do + printf "%s\\n" "$fake" | grep -qx "$sym" || { + echo "libfakeinput.so does not export $sym" >&2 + exit 1 + } + done + session=$(syms "$d/libblsession.so") + for sym in socket bind getsockname setsockopt statfs statvfs; do + printf "%s\\n" "$session" | grep -qx "$sym" || { + echo "libblsession.so does not export $sym" >&2 + exit 1 + } + done + test -f "$d/usr/local/bin/bannerlator-session" + test -f "$d/usr/local/bin/bannerlator-proton-extra" + ' + +export ANDROID_HOME="${sdk_dir}" +export ANDROID_SDK_ROOT="${sdk_dir}" +export JAVA_HOME="${java_dir}" +ndk_version=${STEAMDECK_NDK_VERSION:-} +if [[ -z "${ndk_version}" ]]; then + ndk_path=$(find "${sdk_dir}/ndk" -mindepth 1 -maxdepth 1 -type d -print | sort -V | tail -1) + ndk_version=${ndk_path##*/} +fi +if [[ -z "${ndk_version}" || ! -d "${sdk_dir}/ndk/${ndk_version}" ]]; then + echo "No Android NDK found under ${sdk_dir}/ndk; set STEAMDECK_NDK_VERSION." >&2 + exit 1 +fi + +cd "${repo_root}" +./gradlew assembleRelease -PndkVersion="${ndk_version}" + +apk="${repo_root}/app/build/outputs/apk/release/app-release.apk" +build_tools=$(find "${sdk_dir}/build-tools" -mindepth 1 -maxdepth 1 -type d -print | sort -V | tail -1) +if [[ ! -x "${build_tools}/zipalign" || ! -x "${build_tools}/apksigner" ]]; then + echo "Android build-tools with zipalign/apksigner are required under ${sdk_dir}/build-tools." >&2 + exit 1 +fi + +staging_dir=$(mktemp -d "${TMPDIR:-/tmp}/steamdeck-apk.XXXXXX") +trap 'rm -rf -- "${staging_dir}"' EXIT +"${build_tools}/zipalign" -p -f 4 "${apk}" "${staging_dir}/app-release.aligned.apk" +"${build_tools}/apksigner" sign \ + --ks keystore/testkey.p12 --ks-type PKCS12 --ks-pass pass:android \ + --ks-key-alias testkey --key-pass pass:android \ + --v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true \ + --out "${apk}" "${staging_dir}/app-release.aligned.apk" + +signature_output=$("${build_tools}/apksigner" verify --min-sdk-version 21 --verbose --print-certs "${apk}") +printf '%s\n' "${signature_output}" +for scheme in \ + 'Verified using v1 scheme (JAR signing): true' \ + 'Verified using v2 scheme (APK Signature Scheme v2): true' \ + 'Verified using v3 scheme (APK Signature Scheme v3): true'; do + grep -qF "${scheme}" <<<"${signature_output}" || { + echo "APK signature check failed: ${scheme}" >&2 + exit 1 + } +done +grep -qF 'Signer #1 certificate DN: EMAILADDRESS=android@android.com, CN=Android, OU=Android, O=Android' \ + <<<"${signature_output}" + +docker run --rm --platform linux/amd64 -v "${repo_root}:/src:ro" -w /src "${image_name}" \ + bash -lc ' + set -euo pipefail + apk=app/build/outputs/apk/release/app-release.apk + work=$(mktemp -d) + unzip -q "$apk" "lib/arm64-v8a/*" -d "$work" + cd "$work/lib/arm64-v8a" + system="libc.so libm.so libdl.so liblog.so libandroid.so libz.so libvulkan.so + libGLESv2.so libEGL.so libnativewindow.so libjnigraphics.so libaaudio.so + libOpenSLES.so libmediandk.so libcamera2ndk.so libsync.so libneuralnetworks.so" + fail=0 + for so in *.so; do + for need in $(readelf -d "$so" | sed -n "s/.*NEEDED.*\\[\\(.*\\)\\]/\\1/p"); do + [ -f "$need" ] && continue + case " $(echo $system) " in *" $need "*) continue ;; esac + echo "missing: $so -> $need" + fail=1 + done + done + [ "$fail" -eq 0 ] + echo "every NEEDED resolves" + ' + +printf 'APK: %s\n' "${apk}" +printf 'SHA-256: ' +shasum -a 256 "${apk}" | awk '{print $1}' diff --git a/tools/deploy_local.sh b/tools/deploy_local.sh new file mode 100755 index 000000000..8b8eb8942 --- /dev/null +++ b/tools/deploy_local.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +sdk_dir=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-"${HOME}/Library/Android/sdk"}} +adb_bin=${ADB:-${sdk_dir}/platform-tools/adb} +apk=${1:-"${repo_root}/app/build/outputs/apk/release/app-release.apk"} + +if [[ ! -x "${adb_bin}" ]]; then + echo "adb not found at ${adb_bin}; set ADB or ANDROID_HOME." >&2 + exit 1 +fi +if [[ ! -f "${apk}" ]]; then + echo "APK not found at ${apk}; run tools/build_local.sh first." >&2 + exit 1 +fi + +if [[ -n "${ADB_SERIAL:-}" ]]; then + serial=${ADB_SERIAL} +else + devices=$("${adb_bin}" devices | awk '$2 == "device" {print $1}') + device_count=$(printf '%s\n' "${devices}" | sed '/^$/d' | wc -l | tr -d ' ') + if [[ "${device_count}" == 0 ]]; then + echo "No authorized Android device found. Connect one or set ADB_SERIAL." >&2 + exit 2 + fi + if [[ "${device_count}" != 1 ]]; then + echo "Multiple Android devices found; set ADB_SERIAL explicitly." >&2 + printf '%s\n' "${devices}" >&2 + exit 2 + fi + serial=${devices} +fi + +"${adb_bin}" -s "${serial}" install -r --no-incremental "${apk}" +installed_path=$("${adb_bin}" -s "${serial}" shell pm path com.steamdeck.launcher | tr -d '\r') +printf 'Installed on %s: %s\n' "${serial}" "${installed_path}" diff --git a/tools/local-cross.Dockerfile b/tools/local-cross.Dockerfile new file mode 100644 index 000000000..ccfd37f76 --- /dev/null +++ b/tools/local-cross.Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y \ + binutils \ + binutils-aarch64-linux-gnu \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + unzip \ + && mkdir -p /src + +WORKDIR /src From 7df694fe49348d24c3da4e862ac1ca7bafde208b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:32:10 +0000 Subject: [PATCH 2/4] README: contributions ledger --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 589a4240c..874e3dd40 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,9 @@ _From the repository's pull requests; rewritten when one is opened, merged or cl | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#1](https://github.com/The412Banner/SteamDeck/pull/1) initial decky installer stuff | 2026-09-23 | draft | | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#3](https://github.com/The412Banner/SteamDeck/pull/3) Fix hardware back in Steam session | 2026-09-23 | merged 2026-09-23 | | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#4](https://github.com/The412Banner/SteamDeck/pull/4) Update app launcher icon | 2026-09-23 | merged 2026-09-23 | +| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#5](https://github.com/The412Banner/SteamDeck/pull/5) Add reproducible local build and deploy helpers | 2026-09-23 | open | -- **@xXJSONDeruloXx**: 3 submitted, 2 merged +- **@xXJSONDeruloXx**: 4 submitted, 2 merged ## Requirements From c63af629ace56c1319f0e0704ab5cbcc2e61b41a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:43:13 +0000 Subject: [PATCH 3/4] README: contributions ledger --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d763384c..167af27d0 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ _From the repository's pull requests; rewritten when one is opened, merged or cl | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#1](https://github.com/The412Banner/SteamDeck/pull/1) initial decky installer stuff | 2026-09-23 | draft | | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#3](https://github.com/The412Banner/SteamDeck/pull/3) Fix hardware back in Steam session | 2026-09-23 | merged 2026-09-23 | | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#4](https://github.com/The412Banner/SteamDeck/pull/4) Update app launcher icon | 2026-09-23 | merged 2026-09-23 | -| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#5](https://github.com/The412Banner/SteamDeck/pull/5) Add reproducible local build and deploy helpers | 2026-09-23 | open | +| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#5](https://github.com/The412Banner/SteamDeck/pull/5) Add reproducible local build and deploy helpers | 2026-09-23 | merged 2026-09-23 | -- **@xXJSONDeruloXx**: 4 submitted, 2 merged +- **@xXJSONDeruloXx**: 4 submitted, 3 merged ## Requirements From d1602710a8121d034fae19ba0c1aa2d655757527 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:44:16 +0000 Subject: [PATCH 4/4] README: contributions ledger --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 167af27d0..39a49db11 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ _From the repository's pull requests; rewritten when one is opened, merged or cl | Contributor | Pull request | Opened | State | |---|---|---|---| -| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#1](https://github.com/The412Banner/SteamDeck/pull/1) initial decky installer stuff | 2026-09-23 | draft | +| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#1](https://github.com/The412Banner/SteamDeck/pull/1) initial decky installer stuff | 2026-09-23 | closed | | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#3](https://github.com/The412Banner/SteamDeck/pull/3) Fix hardware back in Steam session | 2026-09-23 | merged 2026-09-23 | | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#4](https://github.com/The412Banner/SteamDeck/pull/4) Update app launcher icon | 2026-09-23 | merged 2026-09-23 | | [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#5](https://github.com/The412Banner/SteamDeck/pull/5) Add reproducible local build and deploy helpers | 2026-09-23 | merged 2026-09-23 |