Files
Kurt Himebauch 3c96df4ab1 feat(ubisoft): Ubisoft Connect store with native sign-in, library, downloads and launch (#2322)
* feat(ubisoft): sign in to Ubisoft Connect and list the owned library

* feat(ubisoft): download games natively through Demux and Ubisoft's CDN

* fix(ubisoft): show the owned games on the store page

* fix(ctl): reach the agent bridge by broadcast when the shell has no content command

* feat(ubisoft): run Ubisoft games in one prefix with Ubisoft Connect installed and the game registered

* feat(ubisoft): launch games through a native stand-in for Ubisoft Connect's local service

* feat(ubisoft): document the launcher pipes, log why owned games have no download, drop the installer after extraction

* feat(ubisoft): Steam-owned Ubisoft games install through Steam, and the launch session carries the game's application id

* feat(ubisoft): answer Denuvo activations through Ubisoft's activation service and log unanswered pipe traffic

* feat(ubisoft): report installed chunks to the game and skip add-ons the account does not own

* fix(ubisoft): the overlay stays off, Ubisoft's install steps pick Windows components, and shared-prefix games keep their own settings

* fix(ubisoft): write the manifest's registry values, and pick the game's exe over Ubisoft's installer stubs

* fix(ubisoft): stream download slices, guard the launch hook, and keep per-game components in the shared prefix

* fix(ubisoft): direct launches in the shared prefix keep their own game id

* ci(ubisoft): build the launcher service after the prepared components are verified

* fix(ubisoft): verify reused files, drop syncs from a signed-out account, and clear activation requests before launch

* fix(ubisoft): fetch again a reused file whose slices cannot be checked
2026-11-10 15:08:37 -05:00

520 lines
26 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
if tree_status=$(git -C "${repo_root}" status --porcelain 2>/dev/null); then
if [[ -n "${tree_status}" ]]; then
export DROIDDECK_BUILD_TREE_STATE=dirty
else
export DROIDDECK_BUILD_TREE_STATE=clean
fi
else
export DROIDDECK_BUILD_TREE_STATE=unknown
fi
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}"
[[ -x "$java_dir/bin/java" ]] || {
mise_java="$(mise -C "${repo_root}" which java 2>/dev/null)"
if [[ -n "$mise_java" ]]; then
mise_home="$(dirname "$mise_java")"
[[ "$mise_home" == */bin ]] && mise_home="$(dirname "$mise_home")"
[[ -x "$mise_home/bin/java" ]] && java_dir="$mise_home"
fi
}
[[ -x "$java_dir/bin/java" ]] || java_dir="${HOME}/.local/share/mise/installs/java/temurin-17"
[[ "$java_dir" == */homebrew/* ]] && echo "using homebrew java path: $java_dir"
[[ "$java_dir" == */mise/* ]] && echo "using mise java path: $java_dir"
. "${repo_root}/tools/docker_platform.sh"
image_name=${DROIDDECK_BUILD_IMAGE}
build_variant=${DROIDDECK_BUILD_VARIANT:-release}
case "$build_variant" in
debug) gradle_task=assembleDebug ;;
release) gradle_task=assembleRelease ;;
*) echo "DROIDDECK_BUILD_VARIANT must be debug or release" >&2; exit 1 ;;
esac
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 [[ ! -f "${repo_root}/external/MaliBridge/scripts/build-client-linux.sh" ]]; then
echo "external/MaliBridge is empty; run: git submodule update --init" >&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
for tool in curl tar zstd shasum unzip python3 go; do
if ! command -v "${tool}" >/dev/null 2>&1; then
echo "${tool} is required to build the CI-equivalent APK." >&2
exit 1
fi
done
if [[ -f "${repo_root}/tools/gamescope/release.env" \
|| -f "${repo_root}/tools/droiddeck-esync/release.env" ]] && ! command -v gh >/dev/null 2>&1; then
echo "GitHub CLI is required to download the pinned Gamescope and droiddeck-esync release assets." >&2
exit 1
fi
ndk_version=${DROIDDECK_NDK_VERSION:-}
if [[ -z "${ndk_version}" ]]; then
ndk_path=$(find "${sdk_dir}/ndk" -mindepth 2 -maxdepth 2 -name source.properties -print | xargs -n1 dirname | 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 DROIDDECK_NDK_VERSION." >&2
exit 1
fi
export ANDROID_HOME="${sdk_dir}"
export ANDROID_SDK_ROOT="${sdk_dir}"
export JAVA_HOME="${java_dir}"
export NDK="${sdk_dir}/ndk/${ndk_version}"
staging_dir=$(mktemp -d "${TMPDIR:-/tmp}/droiddeck-build.XXXXXX")
bundle_asset="${repo_root}/app/src/main/assets/pulseaudio.tzst"
linuxfs_dir="${repo_root}/app/src/main/assets/linuxfs"
cleanup() {
local exit_code=$?
trap - EXIT
rm -rf -- "${staging_dir}" || exit_code=1
exit "${exit_code}"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
cache_dir=${DROIDDECK_BUILD_CACHE:-"${HOME}/.cache/droiddeck-build"}
mkdir -p "${cache_dir}"
inputs_hash() {
(cd "${repo_root}" && { printf '%s\n' "$1"; shift; find "$@" -type f ! -name '*.pyc' | LC_ALL=C sort | xargs shasum -a 256; }) \
| shasum -a 256 | cut -d' ' -f1
}
restore_step() {
local stored="${cache_dir}/steps/$1/$2"
[[ -f "${stored}/.complete" ]] || return 1
mkdir -p "$3"
(cd "${stored}" && tar -cf - --exclude .complete .) | tar -xf - -C "$3" || return 1
echo "$1: inputs unchanged, reused ${stored}"
}
save_step() {
local stored="${cache_dir}/steps/$1/$2" part
mkdir -p "${cache_dir}/steps/$1"
part=$(mktemp -d "${stored}.part.XXXXXX")
(cd "$3" && tar -cf - .) | tar -xf - -C "${part}"
touch "${part}/.complete"
if [[ -e "${stored}" ]]; then rm -rf "${part}"; else mv "${part}" "${stored}" || rm -rf "${part}"; fi
}
python3 "${repo_root}/tools/docker_preflight.py" "${image_name}"
image_id=$(docker image inspect --format '{{.Id}}' "${image_name}")
rm -rf -- "${linuxfs_dir}"
mkdir -p "${linuxfs_dir}"
git_common=$(git -C "${repo_root}" rev-parse --path-format=absolute --git-common-dir)
docker run --rm --platform "${docker_platform}" \
--user "$(id -u):$(id -g)" \
-v "${repo_root}:/src" -v "${git_common}:${git_common}" -w /src "${image_name}" bash -lc '
set -euo pipefail
d=app/src/main/assets/linuxfs
mkdir -p "$d"
put() { mkdir -p "$(dirname "$2")" && cat "$1" > "$2"; }
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
-pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \
-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"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$d/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/libblfastpath.so"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -nostdlib -ffreestanding -fno-stack-protector \
-fno-tree-loop-distribute-patterns -Wl,-z,defs -o "$d/libblaudit.so" tools/proot/fastpath/audit.c
aarch64-linux-gnu-strip --strip-unneeded "$d/libblaudit.so"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$d/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/libssbs.so"
mkdir -p "$d/usr/local/lib/droiddeck-xclipse"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wextra -Wno-unused-parameter -fvisibility=hidden -pthread \
-o "$d/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so" tools/linuxfs/xclipse-layer/xclipse_layer.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so"
install -m644 tools/linuxfs/xclipse-layer/VkLayer_droiddeck_xclipse.json "$d/usr/local/lib/droiddeck-xclipse/"
mkdir -p "$d/usr/local/bin"
external/MaliBridge/scripts/build-client-linux.sh "$d" aarch64-linux-gnu-gcc
aarch64-linux-gnu-gcc -O2 -Wall -o "$d/usr/local/bin/droiddeck-glprobe" tools/venus/glprobe.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/bin/droiddeck-glprobe"
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$d/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/bin/droiddeck-clipboard"
for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do
put "$script" "$d/usr/local/bin/$(basename "$script")"
done
for f in tools/linuxfs/overlay/usr/bin/* tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/*; do
[ -f "$f" ] && put "$f" "$d/${f#tools/linuxfs/overlay/}"
done
put tools/linuxfs/desktop/droiddeck-desktop "$d/usr/local/bin/droiddeck-desktop"
put tools/linuxfs/desktop/droiddeck-gpu "$d/usr/local/bin/droiddeck-gpu"
put tools/linuxfs/desktop/droiddeck-desktop-gpu "$d/usr/local/bin/droiddeck-desktop-gpu"
put tools/linuxfs/desktop/kwin_wayland_wrapper "$d/usr/local/bin/kwin_wayland_wrapper"
put tools/linuxfs/desktop/firefox-droiddeck.js \
"$d/usr/lib/firefox/defaults/pref/droiddeck.js"
READELF=aarch64-linux-gnu-readelf bash tools/linuxfs/check-preloads.sh "$d"
'
x86_key=$(inputs_hash bullseye tools/linuxfs/build-x86-preloads.sh tools/linuxfs/preload tools/linuxfs/fex \
app/src/main/cpp/fakeinput_steam.cpp)
x86_stage="${staging_dir}/x86-preloads"
if restore_step x86-preloads "${x86_key}" "${linuxfs_dir}"; then :; else
docker run --rm --platform linux/amd64 \
-v "${repo_root}:/src" -v "${git_common}:${git_common}" -w /src public.ecr.aws/docker/library/debian:bullseye bash -c '
set -euo pipefail
printf "deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian-security bullseye-security main\n" > /etc/apt/sources.list
apt-get -o Acquire::Check-Valid-Until=false update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends gcc g++ gcc-multilib g++-multilib binutils >/dev/null
tools/linuxfs/build-x86-preloads.sh app/src/main/assets/linuxfs
chown -R '"$(id -u):$(id -g)"' app/src/main/assets/linuxfs
'
mkdir -p "${x86_stage}"
cp -R "${linuxfs_dir}/x86_64" "${linuxfs_dir}/i386" "${x86_stage}/"
save_step x86-preloads "${x86_key}" "${x86_stage}"
fi
vr_out="${linuxfs_dir}/usr/local/lib/droiddeck-vr"
vr_key=$(inputs_hash "${image_id} ${ndk_version} ${VK_INCLUDE:-}" tools/vr app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c)
if ! restore_step vr "${vr_key}" "${vr_out}"; then
DROIDDECK_BUILD_IMAGE="${image_name}" "${repo_root}/tools/vr/build.sh" "${vr_out}"
save_step vr "${vr_key}" "${vr_out}"
fi
gpuload_out="${linuxfs_dir}/usr/local/lib/droiddeck-gpuload"
gpuload_key=$(inputs_hash "${image_id} ${ndk_version} ${VK_INCLUDE:-}" tools/gpuload)
if ! restore_step gpuload "${gpuload_key}" "${gpuload_out}"; then
DROIDDECK_BUILD_IMAGE="${image_name}" "${repo_root}/tools/gpuload/build.sh" "${gpuload_out}"
save_step gpuload "${gpuload_key}" "${gpuload_out}"
fi
github_repo=${DROIDDECK_GITHUB_REPOSITORY:-}
if [[ -z "${github_repo}" ]]; then
origin_url=$(git -C "${repo_root}" remote get-url origin)
case "${origin_url}" in
https://github.com/*) github_repo=${origin_url#https://github.com/} ;;
ssh://git@github.com/*) github_repo=${origin_url#ssh://git@github.com/} ;;
git@github.com:*) github_repo=${origin_url#git@github.com:} ;;
*)
echo "Cannot determine the GitHub repository from origin: ${origin_url}" >&2
echo "Set DROIDDECK_GITHUB_REPOSITORY=owner/repo." >&2
exit 1
;;
esac
github_repo=${github_repo%.git}
fi
cached() {
local sha=$1 name=$2 out
shift 2
out="${cache_dir}/${sha}-${name}"
if [[ -f "${out}" ]] && printf '%s %s\n' "${sha}" "${out}" | shasum -a 256 -c - >/dev/null 2>&1; then
echo "${out}"
return 0
fi
rm -f "${out}.part"
out="${out}.part" "$@" >&2
printf '%s %s\n' "${sha}" "${out}.part" | shasum -a 256 -c - >&2
mv "${out}.part" "${out}"
echo "${out}"
}
if [[ -f "${repo_root}/tools/gamescope/release.env" ]]; then
. "${repo_root}/tools/gamescope/release.env"
gamescope_archive=$(cached "${GAMESCOPE_SHA256}" gamescope.tzst \
bash -c 'gh release download "$0" -R "$1" -p gamescope.tzst -O "$out"' "${GAMESCOPE_TAG}" "${GAMESCOPE_REPO:-${github_repo}}")
zstd -dc "${gamescope_archive}" | tar -xf - -C "${linuxfs_dir}"
test -f "${linuxfs_dir}/usr/local/bin/gamescope"
fi
. "${repo_root}/tools/linuxfs/uruntime.env"
uruntime_binary=$(cached "${URUNTIME_SHA256}" "${URUNTIME_ASSET}" \
bash -c 'curl -fsSL --retry 5 --retry-all-errors --connect-timeout 20 -o "$out" "$0"' "https://github.com/VHSgunzo/uruntime/releases/download/${URUNTIME_VERSION}/${URUNTIME_ASSET}")
mkdir -p "${linuxfs_dir}/usr/local/lib/droiddeck" "${linuxfs_dir}/usr/local/share/licenses/uruntime"
install -m644 "${uruntime_binary}" "${linuxfs_dir}/usr/local/lib/droiddeck/uruntime"
install -m644 "${repo_root}/tools/linuxfs/licenses/uruntime-LICENSE" "${linuxfs_dir}/usr/local/share/licenses/uruntime/LICENSE"
sync_assets="${repo_root}/app/src/main/assets/droiddeck-esync"
if [[ -f "${repo_root}/tools/droiddeck-esync/release.env" ]]; then
. "${repo_root}/tools/droiddeck-esync/release.env"
sync_archive=$(cached "${SYNC_BUNDLE_SHA256}" "${SYNC_BUNDLE_ASSET}" \
bash -c 'gh release download "$0" -R "$2" -p "$1" -O "$out"' "${SYNC_BUNDLE_TAG}" "${SYNC_BUNDLE_ASSET}" "${SYNC_BUNDLE_REPO}")
rm -rf "${sync_assets}"
mkdir -p "${sync_assets}"
zstd -dc "${sync_archive}" | tar -xf - -C "${sync_assets}"
test -f "${sync_assets}/index.json"
test -f "${sync_assets}/index.json.sig"
sync_index=$(mktemp)
gh release download "${SYNC_BUNDLE_TAG}" -R "${SYNC_BUNDLE_REPO}" -p index.json -O "${sync_index}" --clobber
revoked=$(python3 -c 'import json, sys; print(" ".join(p["id"] for p in json.load(open(sys.argv[1]))["packs"] if p.get("revoked") is True))' "${sync_index}")
for id in ${revoked}; do
if [[ -e "${sync_assets}/packs/${id}.tzst" ]]; then
echo "${SYNC_BUNDLE_ASSET} carries revoked pack ${id}; it is left out of the APK" >&2
rm -f "${sync_assets}/packs/${id}.tzst"
fi
done
rm -f "${sync_index}"
elif [[ -d "${sync_assets}" ]]; then
echo "No tools/droiddeck-esync/release.env: the APK bundles the droiddeck-esync packs already in ${sync_assets}." >&2
fi
msi_pkgs="${staging_dir}/msi-pkgs"
msi_dir="${linuxfs_dir}/usr/local/lib/droiddeck-msitools"
mkdir -p "${msi_pkgs}" "${msi_dir}"
while read -r package_sha256 package_url; do
[[ -n "${package_url}" ]] || continue
package_archive=$(cached "${package_sha256}" "$(basename "${package_url}")" \
bash -c 'curl -fsSL --retry 5 --retry-all-errors --connect-timeout 20 -o "$out" "$0"' "${package_url}")
tar -xJf "${package_archive}" -C "${msi_pkgs}"
done < <(grep -v '^#' "${repo_root}/tools/msitools/packages.txt")
for tool in msiinfo cabextract; do install -m644 "${msi_pkgs}/usr/bin/${tool}" "${msi_dir}/${tool}"; done
for tool in 7z 7z.so; do install -m644 "${msi_pkgs}/usr/lib/7zip/${tool}" "${msi_dir}/${tool}"; done
for library in libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do
cp -L "${msi_pkgs}/usr/lib/${library}" "${msi_dir}/${library}"
done
mkdir -p "${linuxfs_dir}/usr/local/share/licenses/droiddeck-msitools"
for license in NOTICE GPL-2 GPL-3 LGPL-2.1 7zip-License 7zip-unRarLicense; do
install -m644 "${repo_root}/tools/msitools/licenses/${license}" \
"${linuxfs_dir}/usr/local/share/licenses/droiddeck-msitools/${license}"
done
mango_dir="${linuxfs_dir}/usr/local/lib/mangoapp"
mango_pkgs="${staging_dir}/mango-pkgs"
mkdir -p "${mango_dir}" "${mango_pkgs}"
while read -r package_sha256 package_url; do
[[ -n "${package_url}" ]] || continue
package_archive=$(cached "${package_sha256}" "$(basename "${package_url}")" \
bash -c 'curl -fsSL --retry 5 --retry-all-errors --connect-timeout 20 -o "$out" "$0"' "${package_url}")
zstd -dc "${package_archive}" | tar -xf - -C "${mango_pkgs}"
done < <(grep -v '^#' "${repo_root}/tools/mangoapp/packages.txt")
install -m644 "${mango_pkgs}/usr/bin/mangoapp" "${mango_dir}/mangoapp"
for library in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do
cp -L "${mango_pkgs}/usr/lib/${library}" "${mango_dir}/${library}"
done
docker run --rm --platform "${docker_platform}" --user "$(id -u):$(id -g)" -v "${repo_root}:/src" -w /src "${image_name}" \
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \
-o app/src/main/assets/linuxfs/usr/local/lib/mangoapp/libtracefs.so.1 tools/mangoapp/libtracefs-shim.c
mkdir -p "${linuxfs_dir}/usr/local/bin"
install -m644 "${repo_root}/tools/mangoapp/mangoapp" "${linuxfs_dir}/usr/local/bin/mangoapp"
pw_pkgs="${staging_dir}/pw-pkgs"
mkdir -p "${pw_pkgs}"
while read -r package_sha256 package_url; do
[[ -n "${package_url}" ]] || continue
package_archive=$(cached "${package_sha256}" "$(basename "${package_url}")" \
bash -c 'curl -fsSL --retry 5 --retry-all-errors --connect-timeout 20 -o "$out" "$0"' "${package_url}")
zstd -dc "${package_archive}" | tar -xf - -C "${pw_pkgs}"
done < <(grep -v '^#' "${repo_root}/tools/pipewire/packages.txt")
"${repo_root}/tools/pipewire/assemble.sh" "${pw_pkgs}" "${linuxfs_dir}/usr/local/lib/droiddeck-pipewire.tzst"
pa_source=${DROIDDECK_PA13_SOURCE_DIR:-"${staging_dir}/pulseaudio-13.0"}
if [[ -z "${DROIDDECK_PA13_SOURCE_DIR:-}" ]]; then
pa_tarball="${cache_dir}/pulseaudio-13.0.tar.gz"
if [[ ! -s "${pa_tarball}" ]] || ! tar -tzf "${pa_tarball}" >/dev/null 2>&1; then
curl -fsSL --retry 5 --retry-all-errors --connect-timeout 20 -o "${pa_tarball}.part" \
https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
mv "${pa_tarball}.part" "${pa_tarball}"
fi
mkdir -p "${pa_source}"
tar -xzf "${pa_tarball}" -C "${pa_source}" --strip-components=1
fi
if [[ ! -f "${pa_source}/src/pulse/version.h.in" ]]; then
echo "PulseAudio 13.0 source not found at ${pa_source}; set DROIDDECK_PA13_SOURCE_DIR." >&2
exit 1
fi
sink_output="${staging_dir}/sink-out"
"${repo_root}/tools/directaudio/fetch.sh" "${repo_root}" "${sink_output}"
"${repo_root}/tools/aaudio-sink/build.sh" "${pa_source}" "${sink_output}"
proot_out="${repo_root}/app/src/main/jniLibs/arm64-v8a"
proot_sources=$(cd "${repo_root}/tools/proot" && find . -type f ! -name '*.pyc' | LC_ALL=C sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1)
proot_inputs=$(printf '%s\n' "${proot_sources}" "${ndk_version}" | shasum -a 256 | cut -d' ' -f1)
if [[ -f "${proot_out}/libproot.so" && -f "${proot_out}/libproot-loader.so" \
&& "$(cat "${proot_out}/.proot-inputs" 2>/dev/null)" = "${proot_inputs}" ]] \
&& (cd "${proot_out}" && shasum -a 256 -c .proot-outputs >/dev/null 2>&1); then
echo "proot: sources unchanged, keeping ${proot_out}/libproot.so"
else
proot_stage="${staging_dir}/proot"
if ! restore_step proot "${proot_inputs}" "${proot_stage}"; then
"${repo_root}/tools/proot/build.sh" "${proot_stage}"
save_step proot "${proot_inputs}" "${proot_stage}"
fi
install -m755 "${proot_stage}/libproot.so" "${proot_stage}/libproot-loader.so" "${proot_out}/"
echo "${proot_inputs}" > "${proot_out}/.proot-inputs"
(cd "${proot_out}" && shasum -a 256 libproot.so libproot-loader.so > .proot-outputs)
fi
syncthing_src="${repo_root}/tools/syncthing/syncthing-android/syncthing/src/github.com/syncthing/syncthing"
git -C "${repo_root}" submodule update --init --recursive tools/syncthing/syncthing-android
syncthing_inputs=$(printf '%s\n' "$(cd "${repo_root}" && shasum -a 256 tools/syncthing/build.sh \
tools/syncthing/syncthing-android/gradle/libs.versions.toml)" \
"$(git -C "${syncthing_src}" rev-parse HEAD)" \
"$(git -C "${syncthing_src}" diff --binary HEAD | shasum -a 256)" \
"$(cd "${syncthing_src}" && git ls-files -z --others --exclude-standard | xargs -0 shasum -a 256)" \
"${ndk_version}" | shasum -a 256 | cut -d' ' -f1)
if [[ -f "${proot_out}/libsyncthingnative.so" \
&& "$(cat "${proot_out}/.syncthing-inputs" 2>/dev/null)" = "${syncthing_inputs}" ]]; then
echo "syncthing: sources unchanged, keeping ${proot_out}/libsyncthingnative.so"
else
syncthing_stage="${staging_dir}/syncthing"
if ! restore_step syncthing "${syncthing_inputs}" "${syncthing_stage}"; then
"${repo_root}/tools/syncthing/build.sh" "${syncthing_stage}"
save_step syncthing "${syncthing_inputs}" "${syncthing_stage}"
fi
install -m755 "${syncthing_stage}/libsyncthingnative.so" "${proot_out}/"
echo "${syncthing_inputs}" > "${proot_out}/.syncthing-inputs"
fi
bundle_dir="${staging_dir}/pulseaudio-bundle"
mkdir -p "${bundle_dir}"
zstd -dc "${bundle_asset}" | tar -xf - -C "${bundle_dir}"
if [[ -e "${bundle_dir}/modules/arm64/module-aaudio-sink.so" \
|| -e "${bundle_dir}/modules/arm64/module-directaudio-native-sink.so" ]]; then
echo "The committed audio bundle already contains a built ARM64 sink." >&2
exit 1
fi
install -m755 "${sink_output}/module-aaudio-sink.so" \
"${bundle_dir}/modules/arm64/module-aaudio-sink.so"
install -m755 "${sink_output}/module-directaudio-native-sink.so" \
"${bundle_dir}/modules/arm64/module-directaudio-native-sink.so"
tar -cf - -C "${bundle_dir}" . | zstd -19 -T0 -c > "${staging_dir}/pulseaudio.tzst"
mkdir -p "${repo_root}/app/build/prepared-assets"
mv "${staging_dir}/pulseaudio.tzst" "${repo_root}/app/build/prepared-assets/pulseaudio.tzst"
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 --toolchain stable aarch64-linux-android
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-${cache_dir}/stores-target}"
if ! command -v cargo-ndk >/dev/null 2>&1; then
cargo install cargo-ndk --locked
fi
fi
cd "${repo_root}"
python3 tools/quest/build-driver.py "${NDK}"
xbox_out="${linuxfs_dir}/usr/local/lib/droiddeck-xbox"
xbox_key=$(inputs_hash "native-arm64-ubuntu24.04" tools/xbox)
if ! restore_step xbox "${xbox_key}" "${xbox_out}"; then
bash tools/xbox/build-local.sh
save_step xbox "${xbox_key}" "${xbox_out}"
fi
comet_out="${linuxfs_dir}/usr/local/lib/droiddeck-comet"
comet_key=$(inputs_hash "${image_id}" tools/comet)
if ! restore_step comet "${comet_key}" "${comet_out}"; then
DROIDDECK_BUILD_IMAGE="${image_name}" bash tools/comet/build-local.sh
save_step comet "${comet_key}" "${comet_out}"
fi
if [[ "${DROIDDECK_SKIP_RUST:-0}" != 1 ]]; then
upc_out="${linuxfs_dir}/usr/local/lib/droiddeck-ubisoft"
upc_key=$(inputs_hash "x86_64-pc-windows-gnu" tools/ubisoft-launcher/src tools/ubisoft-launcher/Cargo.toml tools/ubisoft-launcher/Cargo.lock tools/ubisoft-launcher/build.sh)
if ! restore_step ubisoft "${upc_key}" "${upc_out}"; then
command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 || { echo "x86_64-w64-mingw32-gcc is required for the Ubisoft launcher service (droiddeck-upc.exe)." >&2; exit 1; }
CARGO_TARGET_DIR="${cache_dir}/ubisoft-target" bash tools/ubisoft-launcher/build.sh "${upc_out}"
save_step ubisoft "${upc_key}" "${upc_out}"
fi
fi
python3 tools/powervr/fetch.py
bash tools/packs/build.sh app/build/packs
python3 tools/release/runtime_inputs.py record
./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"
python3 tools/release/runtime_inputs.py apk "app/build/outputs/apk/${build_variant}/app-${build_variant}.apk"
apk="${repo_root}/app/build/outputs/apk/${build_variant}/app-${build_variant}.apk"
audio_check="${staging_dir}/audio-check"
mkdir -p "${audio_check}"
unzip -p "${apk}" assets/pulseaudio.tzst | zstd -dc | tar -xf - -C "${audio_check}"
for audio_file in \
pactl \
modules/arm64/module-aaudio-sink.so \
modules/arm64/module-directaudio-native-sink.so; do
if [[ ! -f "${audio_check}/${audio_file}" ]]; then
echo "APK audio bundle is missing ${audio_file}." >&2
exit 1
fi
done
readelf_path=$(find "${NDK}/toolchains/llvm/prebuilt" -name llvm-readelf | head -1)
[[ -x "${readelf_path}" ]] || { echo "NDK llvm-readelf is missing" >&2; exit 1; }
stores_check=()
[[ "${DROIDDECK_SKIP_RUST:-0}" != 1 ]] || stores_check+=(--without-stores)
python3 tools/release/check_android_libraries.py "${apk}" --readelf "${readelf_path}" "${stores_check[@]}"
git diff --exit-code -- app/src/main/rust/stores/Cargo.lock
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
"${build_tools}/zipalign" -p -f 4 "${apk}" "${apk}.aligned"
"${build_tools}/apksigner" sign \
--ks "${repo_root}/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}" "${apk}.aligned"
rm -f "${apk}.aligned" "${apk}.idsig"
signature_output=$("${build_tools}/apksigner" verify --min-sdk-version 21 --verbose --print-certs "${apk}")
printf '%s\n' "${signature_output}"
if ! unzip -l "${apk}" | grep -E 'META-INF/.*\.(SF|RSA|DSA)$' >/dev/null; then
echo "APK signature check failed: JAR signature files are missing." >&2
exit 1
fi
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 -qi 'CN=Android, OU=Android, O=Android' <<<"${signature_output}" || {
echo "APK signature check failed: the signer is not the AOSP testkey." >&2
exit 1
}
signing_env=${DROIDDECK_SIGNING_ENV:-}
if [[ -z "${signing_env}" ]]; then
for candidate in "${repo_root}/.signing.env" \
"$(dirname "$(git -C "${repo_root}" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || echo .)")/.signing.env"; do
[[ -f "${candidate}" ]] && { signing_env=${candidate}; break; }
done
fi
if [[ -n "${signing_env}" ]]; then
echo "Signing with DroidDeck's key (${signing_env})"
(
set -a
cd "$(dirname "${signing_env}")"
. "${signing_env}"
set +a
BUILD_TOOLS="${build_tools}" "${repo_root}/tools/release/sign-apk.sh" "${apk}" standard "${apk}.release"
)
mv "${apk}.release" "${apk}"
fi
printf 'APK: %s\n' "${apk}"
printf 'SHA-256: '
shasum -a 256 "${apk}" | awk '{print $1}'