Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
3594055
Support building natively on Android (bionic) hosts
hpr Aug 12, 2026
c04a356
Add Android cross-compilation CI job
hpr Aug 16, 2026
ea77ece
Add workflow_dispatch trigger to build workflow
hpr Aug 16, 2026
784095f
Fix toolchain extraction: tarballs are gzip, not xz
hpr Aug 16, 2026
60de7d7
Locate installed Android SDK bundle robustly (cache vs swift-sdks)
hpr Aug 16, 2026
ed8d23b
Search all SwiftPM state dirs for the installed Android bundle
hpr Aug 16, 2026
86043c2
Install Android SDK from local file so it registers in swift-sdks
hpr Aug 16, 2026
25994b9
Use the Android SDK's API-level-suffixed triple (android28)
hpr Aug 16, 2026
bedbcd6
Cross-build native libs for Android CI; validate installed SDKs
hpr Aug 16, 2026
96f5f02
OpenSSL: build_libs/install_dev, not build_swift
hpr Aug 16, 2026
4fc9b0f
CI: temporarily mirror xtool-core to the bionic-guards fork
hpr Aug 16, 2026
1791c17
Cross-build libcurl for libimobiledevice (mobileactivation)
hpr Aug 16, 2026
4f68930
Build libcurl before libtatsu (tatsu configure requires it)
hpr Aug 16, 2026
f05f0a3
Cross-build zlib before curl (NDK ships no zlib.pc)
hpr Aug 16, 2026
6b499d4
Fetch zlib from GitHub releases (zlib.net flakes on CI)
hpr Aug 16, 2026
6bd1a47
Build zlib with -fPIC (tatsu links it into a shared lib)
hpr Aug 16, 2026
d239808
Provide .tarball-version for libimobiledevice git snapshot
hpr Aug 16, 2026
7e7b9e5
libimobiledevice: bionic has pthread_once in libc, not libpthread
hpr Aug 16, 2026
880392c
libimobiledevice: fix cache var name for the pthread_once probe
hpr Aug 16, 2026
cb6c956
Provide empty libpthread.a (bionic pthreads live in libc)
hpr Aug 16, 2026
d6cfa24
Export CPPFLAGS/LDFLAGS so bare link probes find the prefix
hpr Aug 16, 2026
39f5aa8
CI: mirror unxip fork; generate pkg-config files for the sysroot
hpr Aug 16, 2026
af5f878
CI: rewrite unxip pin to fork (mirrors don't retarget pinned deps)
hpr Aug 16, 2026
fc2bbb8
CI: re-apply unxip pin after resolution (update recomputes it)
hpr Aug 16, 2026
fe91a96
Build xz before installing headers into the sysroot
hpr Aug 16, 2026
f5c58cf
CI: re-apply xtool-core pin after resolution too
hpr Aug 16, 2026
a5557f7
Pass --target to the final link (Swift driver omits it)
hpr Aug 16, 2026
a922792
Link fix: explicit lld aarch64 emulation; verbose retry on failure
hpr Aug 16, 2026
bc055e5
Wrap -Xclang-linker in -Xswiftc (swift build rejects it bare)
hpr Aug 16, 2026
3b68c4e
Shim ld.lld on PATH to inject aarch64 emulation for the final link
hpr Aug 17, 2026
0af2a11
Inject aarch64 lld emulation via the SDK toolset, not a PATH shim
hpr Aug 17, 2026
b7b3716
Toolset linker opts go verbatim to lld: use raw -m aarch64elf
hpr Aug 17, 2026
fad064c
DIAG: probe ld.lld -m handling (rsp vs argv) + verbose link dump
hpr Aug 17, 2026
3e6632a
DIAG: wrap ld.lld to capture the actual response file bytes
hpr Aug 17, 2026
d4af07f
DIAG: rerun real lld on captured rsp + token-prefix bisect
hpr Aug 17, 2026
9ff98f3
DIAG: preserve argv[0] in ld.lld wrapper and reruns
hpr Aug 17, 2026
ac3d154
Fix Android cross link: scope pkg-config to the cross sysroot
hpr Aug 17, 2026
955652f
DIAG: dump -L tokens of captured response file + pkg-config env
hpr Aug 17, 2026
c8b19ea
android CI: fix pkgconfig output dir to match PKG_CONFIG_PATH
hpr Aug 17, 2026
9e13cda
android CI: fix libtatsu link flag in generated .pc
hpr Aug 17, 2026
1066ff1
android CI: strip zstd debug sections from NDK static archives
hpr Aug 17, 2026
afc4d34
android CI: verify zstd debug strip took effect
hpr Aug 17, 2026
c64806d
android CI: follow symlinks when stripping NDK static archives
hpr Aug 17, 2026
bdab97a
android CI: tolerate unstrippable NDK archives
hpr Aug 17, 2026
3cef627
android CI: drop link diagnostics, verify product ELF
hpr Aug 17, 2026
163780c
Drop redundant libstdc++->libc++ mapping
hpr Aug 20, 2026
d92c3a2
Cleanup
kabiroberai Sep 3, 2026
f55d32e
Bump unxip
kabiroberai Sep 5, 2026
c303c3e
Try some simplifications
kabiroberai Sep 5, 2026
29c5dc3
Dockerize
kabiroberai Sep 5, 2026
e6b2395
More cleanup
kabiroberai Sep 5, 2026
1df67b1
More changes
kabiroberai Sep 5, 2026
768fabf
Dynamic limd
kabiroberai Sep 5, 2026
fa12cde
More dynamic libs
kabiroberai Sep 5, 2026
2339d05
Semantic platform guards
kabiroberai Sep 5, 2026
2c4e1be
Cleanup
kabiroberai Sep 5, 2026
7205fd7
Improvements
kabiroberai Sep 6, 2026
7f58b4c
Maybe fix
kabiroberai Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
!/Sources
!/Tests
!/Linux
!/Android
/Android/output
/Linux/packages
/Linux/staging
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand Down Expand Up @@ -49,3 +50,10 @@ jobs:
-skipMacroValidation -skipPackagePluginValidation \
-scheme XKit -destination generic/platform=iOS \
| xcbeautify
build-android:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Cross-compile for Android
run: Android/build.sh --debug
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.ipa
.DS_Store
/.build
/Android/output/
/Packages
xcuserdata/
DerivedData/
Expand Down
16 changes: 16 additions & 0 deletions Android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Android development

Run `make android-dev` to open a development
shell with the Android Swift SDK + dependencies configured.

Build xtool for aarch64 Android (API 28) from the repository root:

```sh
make android [RELEASE=1]
```

Run the Android runtime smoke checks with an existing ARM64 AVD:

```sh
Android/smoke-test.sh Pixel_9a
```
138 changes: 138 additions & 0 deletions Android/build-deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash

# Usage: Android/build-deps.sh <install-prefix>
#
# Cross-compiles necessary native libraries for aarch64 Android.
#
# ANDROID_NDK_HOME must point at an unpacked NDK (>= r27).
# Native clang, clang++, ld.lld, and LLVM archive tools must be on PATH.
#
# After running this, point PKG_CONFIG_PATH and PKG_CONFIG_LIBDIR at
# `<install-prefix>/lib/pkgconfig` so that SwiftPM can find them
# during build.sh.

set -euo pipefail
shopt -s extglob

: "${ANDROID_ARCH:?ANDROID_ARCH must be set}"

TRIPLE=$ANDROID_ARCH-linux-android
PREFIX=${1:?usage: build-deps.sh <install-prefix>}
: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}"

rm -rf "$PREFIX"
mkdir -p "$PREFIX"
PREFIX=$(cd "$PREFIX" && pwd)

cd "$(dirname "${BASH_SOURCE[0]}")"
source defs.sh

PROCS=$(nproc)

# Only use the NDK's target headers and libraries, not its host executables.
# The Linux archive labels these directories linux-x86_64 even on ARM hosts.
NDK=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64
RESOURCE_DIR=("$NDK"/lib/clang/*)
export CC="clang --target=$TRIPLE$ANDROID_API_LEVEL --sysroot=$NDK/sysroot -resource-dir=${RESOURCE_DIR[0]}"
export CXX="clang++ --target=$TRIPLE$ANDROID_API_LEVEL --sysroot=$NDK/sysroot -resource-dir=${RESOURCE_DIR[0]}"
export AR=llvm-ar RANLIB=llvm-ranlib NM=llvm-nm
export STRIP="llvm-objcopy --strip-all"

WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
# Point pkg-config exclusively at the cross prefix so the autotools builds
# find each other instead of the host's libraries.
export PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig
export PKG_CONFIG_LIBDIR=$PREFIX/lib/pkgconfig
# Make all configure probes (not just pkg-config ones) find the prefix
export CPPFLAGS="-I$PREFIX/include"
export LDFLAGS="-fuse-ld=lld -L$PREFIX/lib"

fetch() {
curl -sfL --retry 3 -o "$WORK/$2" "$1"
}

echo "==> OpenSSL"
fetch \
https://github.com/openssl/openssl/releases/download/openssl-3.3.2/openssl-3.3.2.tar.gz \
openssl.tar.gz
tar -C "$WORK" -xzf "$WORK/openssl.tar.gz"
(
cd "$WORK/openssl-3.3.2"
./Configure linux-$ANDROID_ARCH no-tests --prefix="$PREFIX"
make -j"$PROCS" build_libs
make install_dev
)

build_autotools() { # <tarball-url> <src-dir> [configure args...]
local url=$1 dir=$2
shift 2
fetch "$url" "$dir.tar"
tar -C "$WORK" -xf "$WORK/$dir.tar"
(
cd "$WORK/$dir"
./configure --host="$TRIPLE" --prefix="$PREFIX" --enable-shared --disable-static "$@"
make -j"$PROCS" install
)
}

# bionic's pthreads are in libc and modern NDKs ship no libpthread;
# provide an empty static lib so -lpthread probes and links resolve.
"$AR" cr "$PREFIX/lib/libpthread.a"

echo "==> curl"
fetch https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz zlib.tar.gz
tar -C "$WORK" -xzf "$WORK/zlib.tar.gz"
(
cd "$WORK/zlib-1.3.1"
CHOST="$TRIPLE" CFLAGS="-fPIC" ./configure --prefix="$PREFIX"
make -j"$PROCS" install
)
build_autotools \
https://github.com/curl/curl/releases/download/curl-8_16_0/curl-8.16.0.tar.bz2 \
curl-8.16.0 --enable-shared --disable-static --with-openssl --without-libpsl \
--without-libidn2 --without-brotli --without-zstd --without-nghttp2 \
--disable-ldap --disable-ldaps --with-ca-bundle=/system/etc/security/cacerts

echo "==> libimobiledevice stack"
build_autotools \
https://github.com/libimobiledevice/libplist/releases/download/2.6.0/libplist-2.6.0.tar.bz2 \
libplist-2.6.0 --without-cython
build_autotools \
https://github.com/libimobiledevice/libimobiledevice-glue/releases/download/1.3.1/libimobiledevice-glue-1.3.1.tar.bz2 \
libimobiledevice-glue-1.3.1
build_autotools \
https://github.com/libimobiledevice/libusbmuxd/releases/download/2.1.0/libusbmuxd-2.1.0.tar.bz2 \
libusbmuxd-2.1.0 --without-udev
build_autotools \
https://github.com/libimobiledevice/libtatsu/releases/download/1.0.4/libtatsu-1.0.4.tar.bz2 \
libtatsu-1.0.4
# libimobiledevice has no release tarball with the API SwiftyMobileDevice
# needs; use master like the Linux Docker image does.
fetch \
https://codeload.github.com/libimobiledevice/libimobiledevice/tar.gz/refs/heads/master \
libimobiledevice.tar.gz
tar -C "$WORK" -xzf "$WORK/libimobiledevice.tar.gz"
(
cd "$WORK/libimobiledevice-master"
# git-archive tarballs have no version info; provide one for bootstrap
git init -q . && git add -A && git -c user.email=ci@localhost -c user.name=ci commit -qm "libimobiledevice master snapshot"
echo "2.0.1-git" > .tarball-version
./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython --enable-shared --disable-static
make -j"$PROCS" install
)

# unxip links liblzma
echo "==> xz"
fetch https://github.com/tukaani-project/xz/releases/download/v5.6.4/xz-5.6.4.tar.gz xz.tar
tar -C "$WORK" -xf "$WORK/xz.tar"
(
cd "$WORK/xz-5.6.4"
./configure --host="$TRIPLE" --prefix="$PREFIX" --enable-shared --disable-static
make -j"$PROCS" install
)

echo "==> cleaning up $PREFIX"
rm -rf "$PREFIX"/!(include|lib) "$PREFIX"/lib/!(*.so*|pkgconfig)

echo "==> done: native libs installed into $PREFIX"
85 changes: 85 additions & 0 deletions Android/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env bash

set -euo pipefail

configuration=release
if [[ $# == 1 && $1 == --debug ]]; then
configuration=debug
elif [[ $# != 0 ]]; then
echo "Usage: $0 [--debug]" >&2
exit 2
fi

cd "$(dirname "${BASH_SOURCE[0]}")/.."

if [[ "${XTL_ANDROID_ENV:-}" != 1 ]]; then
# re-exec inside the Android build environment
exec docker compose run --build --rm xtool-android env XTL_ANDROID_NESTED=1 Android/build.sh "$@"
fi

source Android/defs.sh

output="$PWD/Android/output"
rm -rf "$output"
mkdir -p "$output"

swift build --product xtool --swift-sdk $ANDROID_ARCH-unknown-linux-android$ANDROID_API_LEVEL -c "$configuration"
cp -a ".build/$ANDROID_ARCH-unknown-linux-android$ANDROID_API_LEVEL/$configuration/xtool" "$output/"

android_libroot="$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/$ANDROID_ARCH-linux-android

# these contain libraries that are already present on device
system_libdirs=(
"$android_libroot/$ANDROID_API_LEVEL"
)

# these contain libraries that we need to bundle
libdirs=(
"$android_libroot"
"$ANDROID_NATIVE_PREFIX"/lib
"$ANDROID_SWIFT_SDK/swift-android/swift-resources/usr/lib/swift-$ANDROID_ARCH/android"
)

declare -A handled_objs

function copy_deps() {
for soname in $(readelf -d "$1" | awk '/(NEEDED)/ {print $NF}' | tr -d '[]'); do
if [[ -v handled_objs[$soname] ]]; then
continue
fi

handled_objs[$soname]=1
found=0

for libdir in "${system_libdirs[@]}"; do
lib="$libdir/$soname"
[[ -f "$lib" ]] || continue
found=1
break
done

if [[ $found = 0 ]]; then
for libdir in "${libdirs[@]}"; do
lib="$libdir/$soname"
[[ -f "$lib" ]] || continue
cp -L "$lib" "$output/"
copy_deps "$lib"
found=1
break
done
fi

if [[ $found = 0 ]]; then
echo "error: Could not find $soname" >&2
exit 1
fi
done
}

copy_deps "$output/xtool"

if [[ "$configuration" == release ]]; then
llvm-objcopy --strip-all "$output/xtool"
fi

echo "Built at ./Android/output/xtool"
3 changes: 3 additions & 0 deletions Android/defs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

export ANDROID_API_LEVEL=28
90 changes: 90 additions & 0 deletions Android/smoke-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash

# Build xtool and smoke-test it on an Android Virtual Device.

set -euo pipefail

if [[ $# != 1 || ${1:-} == --help || ${1:-} == -h ]]; then
echo "Usage: $0 <avd-name>" >&2
exit 2
fi

cd "$(dirname "${BASH_SOURCE[0]}")/.."

sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}
if [[ -z $sdk ]]; then
case $(uname -s) in
Darwin) sdk=$HOME/Library/Android/sdk ;;
*) sdk=$HOME/Android/Sdk ;;
esac
fi
adb=$sdk/platform-tools/adb
emulator=$sdk/emulator/emulator
for tool in "$adb" "$emulator"; do
[[ -x $tool ]] || { echo "Missing $tool; set ANDROID_HOME to your Android SDK." >&2; exit 1; }
done

avd=$1
port=${ANDROID_EMULATOR_PORT:-5580}
boot_timeout=${ANDROID_BOOT_TIMEOUT:-180}
[[ $port =~ ^[0-9]+$ && $boot_timeout =~ ^[0-9]+$ ]] \
|| { echo "Port and timeout must be integers." >&2; exit 2; }
(( port >= 5554 && port <= 5682 && port % 2 == 0 && boot_timeout > 0 )) \
|| { echo "Use an even emulator port from 5554 to 5682 and a positive timeout." >&2; exit 2; }
serial=emulator-$port
avds=$("$emulator" -list-avds)
if ! printf '%s\n' "$avds" | grep -Fx -- "$avd" >/dev/null; then
printf 'Unknown AVD: %s\nAvailable AVDs:\n%s\n' "$avd" "$avds" >&2
exit 1
fi
"$adb" start-server
devices=$("$adb" devices)
if printf '%s\n' "$devices" | grep -E "^$serial[[:space:]]" >/dev/null; then
echo "$serial is already in use; set ANDROID_EMULATOR_PORT to another port." >&2
exit 1
fi

work=$(mktemp -d "${TMPDIR:-/tmp}/xtool-android-smoke.XXXXXX")
emulator_pid=
cleanup() {
local status=$?
trap - EXIT
if [[ -n $emulator_pid ]] && kill -0 "$emulator_pid" 2>/dev/null; then
kill "$emulator_pid" 2>/dev/null || true
wait "$emulator_pid" 2>/dev/null || true
fi
if (( status == 0 )); then
rm -rf "$work"
else
echo "Smoke test failed; logs and staged files: $work" >&2
tail -n 30 "$work/emulator.log" 2>/dev/null || true
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

echo "==> Building xtool"
./Android/build.sh --debug 2>&1 | tee "$work/build.log"

echo "==> Booting $avd ($serial)"
"$emulator" -avd "$avd" -port "$port" -read-only -no-snapshot -no-window -no-audio \
>"$work/emulator.log" 2>&1 &
emulator_pid=$!
deadline=$((SECONDS + boot_timeout))
while [[ $("$adb" -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') != 1 ]]; do
kill -0 "$emulator_pid" 2>/dev/null || { echo "Emulator exited before boot." >&2; exit 1; }
(( SECONDS < deadline )) || { echo "Timed out waiting for Android to boot." >&2; exit 1; }
sleep 1
done
abi=$("$adb" -s "$serial" shell getprop ro.product.cpu.abi | tr -d '\r')
api=$("$adb" -s "$serial" shell getprop ro.build.version.sdk | tr -d '\r')
[[ $abi == arm64-v8a && $api =~ ^[0-9]+$ ]] && (( api >= 28 )) \
|| { echo "Expected ARM64 Android API 28+; found $abi API $api." >&2; exit 1; }

echo "==> Running smoke checks on $abi / API $api"

remote=/data/local/tmp/xtool-smoke-${work##*.}
"$adb" -s "$serial" push Android/output "$remote"
"$adb" -s "$serial" shell "$remote/xtool --version" 2>&1 | tee "$work/smoke.log"
Loading