From 3594055c171b24bf3125578f2d111ddcf4fa60d9 Mon Sep 17 00:00:00 2001 From: Harry P Date: Wed, 12 Aug 2026 19:25:38 -0400 Subject: [PATCH 01/58] Support building natively on Android (bionic) hosts With the Swift Android toolchain (e.g. under Termux), xtool can build and run on-device. Changes are additive platform guards: - Package.swift: enable the Subprocess, AsyncHTTPClient, OpenAPIAsyncHTTPClient, WebSocketKit, and XADI dependencies on Android, matching Linux. - XKit: use the AsyncHTTPClient-based HTTPClient on Android (the URLSession-based client relies on Darwin-only URLSessionWebSocketTask); bionic's FILE is an incomplete type, so stdoutSafe is an OpaquePointer there. - XToolSupport/XUtils: import the Android overlay where needed (SIG_IGN, flock, errno); bionic marks signal() warn_unused_result. --- Package.resolved | 11 ++++++++++- Package.swift | 10 +++++----- .../HTTPClientProtocol/AsyncHTTPClient+HTTP.swift | 2 +- Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift | 2 +- Sources/XKit/Utilities/CHelpers.swift | 10 ++++++++++ Sources/XToolSupport/XTool.swift | 5 ++++- Sources/XUtils/System+Utils.swift | 3 +++ 7 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Package.resolved b/Package.resolved index 0bcfc149..bab42f9e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "80db0cddb0e78cf6a4c79f94537cdfebd8004679161532fbae8a57ee9d1d8cd1", + "originHash" : "41970e2ef95ed2d95550d89758de9edde764c927d6316551e31f0e6b2c05a55a", "pins" : [ { "identity" : "aexml", @@ -55,6 +55,15 @@ "version" : "6.2.0" } }, + { + "identity" : "opencombine", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenCombine/OpenCombine.git", + "state" : { + "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", + "version" : "0.14.0" + } + }, { "identity" : "pathkit", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 8ea98c7f..dfdd6782 100644 --- a/Package.swift +++ b/Package.swift @@ -100,7 +100,7 @@ let package = Package( .product( name: "Subprocess", package: "swift-subprocess", - condition: .when(platforms: [.linux, .macOS]) + condition: .when(platforms: [.linux, .macOS, .android]) ), ] ), @@ -110,7 +110,7 @@ let package = Package( "DeveloperAPI", "CXKit", "XUtils", - .byName(name: "XADI", condition: .when(platforms: [.linux])), + .byName(name: "XADI", condition: .when(platforms: [.linux, .android])), .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "SwiftyMobileDevice", package: "SwiftyMobileDevice"), @@ -128,17 +128,17 @@ let package = Package( .product( name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client", - condition: .when(platforms: [.linux]) + condition: .when(platforms: [.linux, .android]) ), .product( name: "AsyncHTTPClient", package: "async-http-client", - condition: .when(platforms: [.linux]) + condition: .when(platforms: [.linux, .android]) ), .product( name: "WebSocketKit", package: "websocket-kit", - condition: .when(platforms: [.linux]) + condition: .when(platforms: [.linux, .android]) ), ], cSettings: cSettings diff --git a/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift b/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift index eaa8f607..bb717312 100644 --- a/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift +++ b/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift @@ -5,7 +5,7 @@ // Created by Kabir Oberai on 05/05/21. // -#if os(Linux) +#if os(Linux) || os(Android) import Foundation import AsyncHTTPClient import NIO diff --git a/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift b/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift index 74527bfc..b0c3eda7 100644 --- a/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift +++ b/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift @@ -5,7 +5,7 @@ // Created by Kabir Oberai on 05/05/21. // -#if !os(Linux) +#if !os(Linux) && !os(Android) import Foundation import ConcurrencyExtras import OpenAPIRuntime diff --git a/Sources/XKit/Utilities/CHelpers.swift b/Sources/XKit/Utilities/CHelpers.swift index afd1e8ea..3b89cbde 100644 --- a/Sources/XKit/Utilities/CHelpers.swift +++ b/Sources/XKit/Utilities/CHelpers.swift @@ -8,10 +8,20 @@ import Foundation import CXKit +#if os(Android) +import Android +#endif +#if os(Android) +// bionic's FILE is an incomplete type, imported as OpaquePointer +package var stdoutSafe: OpaquePointer { + get_stdout() +} +#else package var stdoutSafe: UnsafeMutablePointer { get_stdout() } +#endif extension Data { init?(deallocator: Deallocator = .free, acceptor: (inout Int) -> UnsafeMutableRawPointer?) { diff --git a/Sources/XToolSupport/XTool.swift b/Sources/XToolSupport/XTool.swift index 2cec336e..1f86969d 100644 --- a/Sources/XToolSupport/XTool.swift +++ b/Sources/XToolSupport/XTool.swift @@ -1,4 +1,7 @@ import Foundation +#if os(Android) +import Android +#endif import XKit import ArgumentParser import XUtils @@ -74,7 +77,7 @@ extension ParsableCommand where Self: SendableMetatype { } } - signal(SIGINT, SIG_IGN) + _ = signal(SIGINT, SIG_IGN) let source = DispatchSource.makeSignalSource(signal: SIGINT) source.setEventHandler { task.cancel() } source.resume() diff --git a/Sources/XUtils/System+Utils.swift b/Sources/XUtils/System+Utils.swift index df47da9f..530f030c 100644 --- a/Sources/XUtils/System+Utils.swift +++ b/Sources/XUtils/System+Utils.swift @@ -1,4 +1,7 @@ import Foundation +#if os(Android) +import Android +#endif #if canImport(System) import System From c04a35620b55509d1c247f29ef6588c6ca049aa7 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 08:45:23 -0400 Subject: [PATCH 02/58] Add Android cross-compilation CI job Per review: cross-compile the package for Android (bionic) from a Linux host using the official Swift SDK artifact bundle for Android. Pins the 6.3.2 toolchain to match the bundle, installs the Android NDK to populate the SDK's ndk-sysroot (the bundle's setup-android-sdk.sh hardlinks it in), and builds the xtool product for aarch64-unknown-linux-android. --- .github/workflows/build.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c6bfb77..4be33e6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,3 +49,37 @@ jobs: -skipMacroValidation -skipPackagePluginValidation \ -scheme XKit -destination generic/platform=iOS \ | xcbeautify + build-android: + # Cross-compile for Android (bionic) from Linux with the Swift SDK, + # validating the platform guards used for native Android hosts. + runs-on: ubuntu-24.04 + env: + SWIFT_VERSION: 6.3.2 + # Keep in sync with the toolchain version above. + ANDROID_SDK_CHECKSUM: 939e933549d12d28f2e0bf71019d734d309859e9773c572657ce565a81f85d68 + NDK_VERSION: 27c + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install Swift toolchain + run: | + curl -sfL "https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2404/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04.tar.gz" \ + | tar -xJ -C "$HOME" + echo "$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" >> "$GITHUB_PATH" + - name: Install Android NDK + run: | + curl -sfL -o ndk.zip "https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip" + unzip -q ndk.zip -d "$HOME" + echo "ANDROID_NDK_HOME=$HOME/android-ndk-r${NDK_VERSION}" >> "$GITHUB_ENV" + - name: Install Swift SDK for Android + run: | + swift sdk install \ + "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" \ + --checksum "$ANDROID_SDK_CHECKSUM" + bundle=$(ls -d "$HOME"/.swiftpm/swift-sdks/swift-*-RELEASE_android.artifactbundle) + # Populate the SDK's ndk-sysroot from the NDK (hardlinks; no + # SELinux-style restrictions on CI runners). + (cd "$bundle/swift-android" && bash scripts/setup-android-sdk.sh) + - name: Cross-compile for Android + run: | + swift build --product xtool --swift-sdk aarch64-unknown-linux-android From ea77ece818f2be5fa787925f7a851e7fc88be3a8 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 08:48:14 -0400 Subject: [PATCH 03/58] Add workflow_dispatch trigger to build workflow --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4be33e6f..07fd40f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,7 @@ on: branches: - main pull_request: + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 784095f3a996105e35014ead08733047f3cd1418 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 08:58:07 -0400 Subject: [PATCH 04/58] Fix toolchain extraction: tarballs are gzip, not xz --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07fd40f2..826d0c7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,7 +65,7 @@ jobs: - name: Install Swift toolchain run: | curl -sfL "https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2404/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04.tar.gz" \ - | tar -xJ -C "$HOME" + | tar -xzf - -C "$HOME" echo "$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" >> "$GITHUB_PATH" - name: Install Android NDK run: | From 60de7d7f912e73497a64f8861f7ab3779e666a80 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 09:21:07 -0400 Subject: [PATCH 05/58] Locate installed Android SDK bundle robustly (cache vs swift-sdks) --- .github/workflows/build.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 826d0c7d..0dce6f55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,9 +77,12 @@ jobs: swift sdk install \ "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" \ --checksum "$ANDROID_SDK_CHECKSUM" - bundle=$(ls -d "$HOME"/.swiftpm/swift-sdks/swift-*-RELEASE_android.artifactbundle) - # Populate the SDK's ndk-sysroot from the NDK (hardlinks; no - # SELinux-style restrictions on CI runners). + # Depending on SwiftPM version, the unpacked bundle lives under + # ~/.swiftpm/swift-sdks or the download cache — find either. + bundle=$(find "$HOME/.swiftpm" -maxdepth 3 -type d -name '*_android.artifactbundle' | head -1) + test -n "$bundle" || { echo "SDK bundle not found" >&2; exit 1; } + # Populate the SDK's ndk-sysroot from the NDK (the bundle's + # setup-android-sdk.sh hardlinks it in). (cd "$bundle/swift-android" && bash scripts/setup-android-sdk.sh) - name: Cross-compile for Android run: | From ed8d23b84e5f1bbffff05e2dc4a244ccb43e96ac Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 09:24:08 -0400 Subject: [PATCH 06/58] Search all SwiftPM state dirs for the installed Android bundle --- .github/workflows/build.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0dce6f55..a6d62077 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,9 +77,23 @@ jobs: swift sdk install \ "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" \ --checksum "$ANDROID_SDK_CHECKSUM" - # Depending on SwiftPM version, the unpacked bundle lives under - # ~/.swiftpm/swift-sdks or the download cache — find either. - bundle=$(find "$HOME/.swiftpm" -maxdepth 3 -type d -name '*_android.artifactbundle' | head -1) + # Depending on SwiftPM version/platform, the unpacked bundle + # lives under ~/.swiftpm/swift-sdks, the XDG data dir, or the + # cache — check the known candidates, then fall back to a + # HOME-wide search. + bundle="" + for d in \ + "$HOME/.swiftpm/swift-sdks" \ + "$HOME/.local/share/swiftpm/swift-sdks" \ + "$HOME/.cache/swiftpm/swift-sdks"; do + if [ -d "$d/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" ]; then + bundle="$d/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" + break + fi + done + if [ -z "$bundle" ]; then + bundle=$(find "$HOME" -maxdepth 6 -type d -name "swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" 2>/dev/null | head -1) + fi test -n "$bundle" || { echo "SDK bundle not found" >&2; exit 1; } # Populate the SDK's ndk-sysroot from the NDK (the bundle's # setup-android-sdk.sh hardlinks it in). From 86043c284c2429ccc5e1a049edf4b7a8c71994d2 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 09:27:14 -0400 Subject: [PATCH 07/58] Install Android SDK from local file so it registers in swift-sdks --- .github/workflows/build.yml | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6d62077..6de775d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,30 +74,21 @@ jobs: echo "ANDROID_NDK_HOME=$HOME/android-ndk-r${NDK_VERSION}" >> "$GITHUB_ENV" - name: Install Swift SDK for Android run: | - swift sdk install \ - "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" \ - --checksum "$ANDROID_SDK_CHECKSUM" - # Depending on SwiftPM version/platform, the unpacked bundle - # lives under ~/.swiftpm/swift-sdks, the XDG data dir, or the - # cache — check the known candidates, then fall back to a - # HOME-wide search. - bundle="" - for d in \ - "$HOME/.swiftpm/swift-sdks" \ - "$HOME/.local/share/swiftpm/swift-sdks" \ - "$HOME/.cache/swiftpm/swift-sdks"; do - if [ -d "$d/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" ]; then - bundle="$d/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" - break - fi - done - if [ -z "$bundle" ]; then - bundle=$(find "$HOME" -maxdepth 6 -type d -name "swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" 2>/dev/null | head -1) - fi + # Install from a local file: URL installs land in a cache dir on + # some SwiftPM versions and the SDK then isn't found by + # `swift build --swift-sdk `; local-file installs + # register in ~/.swiftpm/swift-sdks. + curl -sfL --retry 3 -o /tmp/android-sdk.tar.gz \ + "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" + echo "${ANDROID_SDK_CHECKSUM} /tmp/android-sdk.tar.gz" | sha256sum -c - + swift sdk install /tmp/android-sdk.tar.gz --checksum "$ANDROID_SDK_CHECKSUM" + bundle="$HOME/.swiftpm/swift-sdks/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" + test -d "$bundle" || bundle=$(find "$HOME" -maxdepth 6 -type d -name "swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" 2>/dev/null | head -1) test -n "$bundle" || { echo "SDK bundle not found" >&2; exit 1; } # Populate the SDK's ndk-sysroot from the NDK (the bundle's # setup-android-sdk.sh hardlinks it in). (cd "$bundle/swift-android" && bash scripts/setup-android-sdk.sh) - name: Cross-compile for Android run: | + swift sdk list || true swift build --product xtool --swift-sdk aarch64-unknown-linux-android From 25994b95479032ceb8cdd7f73ffe8984cfa564ec Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 09:35:11 -0400 Subject: [PATCH 08/58] Use the Android SDK's API-level-suffixed triple (android28) --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6de775d9..b8228283 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,4 +91,6 @@ jobs: - name: Cross-compile for Android run: | swift sdk list || true - swift build --product xtool --swift-sdk aarch64-unknown-linux-android + # The Android SDK registers API-level-suffixed triples, not the + # bare aarch64-unknown-linux-android. + swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 From bedbcd6a2ad1ee2621500921fa2674d31ada8c7b Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 15:11:38 -0400 Subject: [PATCH 09/58] Cross-build native libs for Android CI; validate installed SDKs - Android/build-native-libs.sh: cross-build OpenSSL and the libimobiledevice stack (libplist, -glue, libusbmuxd, libtatsu, libimobiledevice; same set as the Linux Docker image) for aarch64-android, installing into the Swift SDK's ndk-sysroot so the zsign/SwiftyMobileDevice systemLibrary targets compile and link. libxadi is not needed: XADIProvider is os(Linux)-only. - CI: run the script after SDK install (adds autoconf/automake/ libtool). - SDKBuilder: validate a sample of known XIP-hardlink-victim files after copying (libc++ __config/vector/string, Swift shims HeapObject.h/KeyPath.h). When linkat(2) fails during extraction (e.g. SELinux hosts) these vanish silently and the SDK produces confusing 'file not found' compile errors long after install; fail at install time instead. --- .github/workflows/build.yml | 6 ++ Android/build-native-libs.sh | 99 +++++++++++++++++++++++++++ Sources/XToolSupport/SDKBuilder.swift | 34 +++++++++ 3 files changed, 139 insertions(+) create mode 100755 Android/build-native-libs.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8228283..e6482707 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,9 +85,15 @@ jobs: bundle="$HOME/.swiftpm/swift-sdks/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" test -d "$bundle" || bundle=$(find "$HOME" -maxdepth 6 -type d -name "swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" 2>/dev/null | head -1) test -n "$bundle" || { echo "SDK bundle not found" >&2; exit 1; } + echo "ANDROID_SWIFT_SDK=$bundle" >> "$GITHUB_ENV" # Populate the SDK's ndk-sysroot from the NDK (the bundle's # setup-android-sdk.sh hardlinks it in). (cd "$bundle/swift-android" && bash scripts/setup-android-sdk.sh) + - name: Cross-build native libraries for Android + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends autoconf automake libtool + Android/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" - name: Cross-compile for Android run: | swift sdk list || true diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh new file mode 100755 index 00000000..b7d5f9ff --- /dev/null +++ b/Android/build-native-libs.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Cross-builds the native libraries xtool links against (OpenSSL and the +# libimobiledevice stack) for aarch64 Android, installing them into the +# Swift SDK for Android's NDK sysroot so that +# swift build --swift-sdk aarch64-unknown-linux-android28 +# can compile and link against them. +# +# Usage: Android/build-native-libs.sh +# ANDROID_NDK_HOME must point at an unpacked NDK (>= r27). +# +# The library set mirrors the Linux Docker image (see Dockerfile): OpenSSL +# plus libplist/libimobiledevice-glue/libusbmuxd/libtatsu/libimobiledevice +# from the libimobiledevice project, all built statically. libxadi is not +# needed: XADIProvider is os(Linux)-only (on macOS/Android anisette uses +# Omnisette), so the XADI system library never enters the link. +set -euo pipefail + +API=28 +TRIPLE=aarch64-linux-android +SDK=${1:?usage: build-native-libs.sh } +: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" + +TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin +export PATH="$TOOLCHAIN:$PATH" +export CC="$TRIPLE$API-clang" +export CXX="$TRIPLE$API-clang++" +export AR=llvm-ar RANLIB=llvm-ranlib STRIP=llvm-strip +export ANDROID_NDK_ROOT=$ANDROID_NDK_HOME + +WORK=$(mktemp -d) +PREFIX=$WORK/prefix +mkdir -p "$PREFIX" +# 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 + +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 android-arm64 -D__ANDROID_API__=$API no-shared no-tests \ + --prefix="$PREFIX" + make -j"$(nproc)" build_swift + make install_swift install_dev +) + +build_autotools() { # [configure args...] + local url=$1 dir=$2 + shift 2 + fetch "$url" "$dir.tar.bz2" + tar -C "$WORK" -xjf "$WORK/$dir.tar.bz2" + ( + cd "$WORK/$dir" + ./configure --host="$TRIPLE" --prefix="$PREFIX" "$@" + make -j"$(nproc)" install + ) +} + +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" + ./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython + make -j"$(nproc)" install +) + +echo "==> installing into SDK sysroot" +INC_DST=$SDK/ndk-sysroot/usr/include +LIB_DST=$SDK/ndk-sysroot/usr/lib/$TRIPLE +mkdir -p "$INC_DST" "$LIB_DST" +cp -R "$PREFIX/include/." "$INC_DST/" +cp -a "$PREFIX/lib/"*.a "$LIB_DST/" + +echo "==> done: native libs installed into $SDK" diff --git a/Sources/XToolSupport/SDKBuilder.swift b/Sources/XToolSupport/SDKBuilder.swift index 96e7f6f6..2df92a4b 100644 --- a/Sources/XToolSupport/SDKBuilder.swift +++ b/Sources/XToolSupport/SDKBuilder.swift @@ -305,6 +305,9 @@ struct SDKBuilder { } print() + print("[Validating SDKs]") + try Self.validateInstalledSDKs(in: dev) + print("[Cleaning up]") if let cleanupStageDir { try? FileManager.default.removeItem(at: cleanupStageDir) @@ -357,6 +360,37 @@ struct SDKBuilder { return dev } + /// Files expected in every installed SDK. When the XIP extractor + /// fails to recreate hard-linked duplicates (e.g. unxip's linkat(2) + /// failing on hosts that restrict hard links, such as SELinux- + /// enforced Android), these files silently vanish from the staged + /// tree; the resulting SDK then produces confusing compile errors + /// ("'__config' file not found") long after installation. Validating + /// a sample of known victims turns that silent corruption into a + /// clear failure at install time. + private static let sdkValidationPaths = [ + // libc++ headers are stored as hardlink duplicates in the XIP + "Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/c++/v1/__config", + "Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/c++/v1/vector", + "Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/c++/v1/string", + // so are the Swift runtime shims headers + "Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift/shims/HeapObject.h", + "Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift/shims/KeyPath.h", + ] + + private static func validateInstalledSDKs(in dev: URL) throws { + let manager = FileManager.default + for path in sdkValidationPaths + where !manager.fileExists(atPath: dev.appending(path: path).path) { + throw Console.Error(""" + SDK validation failed: \(path) is missing. The SDK bundle is \ + corrupt; this can happen when XIP extraction silently fails \ + to recreate hard-linked files. Please try installing the SDK \ + again (and report a bug if it persists). + """) + } + } + // returns the number of files we actually want to keep, // useful for computing progress % during fs traversal private func extractXIP(inputPath: String, outDir: String) async throws -> Int { From 96f5f023a8d5af6d76dd67f4f25f5fb101659cf4 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 16:13:23 -0400 Subject: [PATCH 10/58] OpenSSL: build_libs/install_dev, not build_swift --- Android/build-native-libs.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index b7d5f9ff..509f22e9 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -48,8 +48,8 @@ tar -C "$WORK" -xzf "$WORK/openssl.tar.gz" cd "$WORK/openssl-3.3.2" ./Configure android-arm64 -D__ANDROID_API__=$API no-shared no-tests \ --prefix="$PREFIX" - make -j"$(nproc)" build_swift - make install_swift install_dev + make -j"$(nproc)" build_libs + make install_dev ) build_autotools() { # [configure args...] From 4fc9b0f35ac3e4c9b6ce670bf6f5b4c5ca87a010 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 16:15:25 -0400 Subject: [PATCH 11/58] CI: temporarily mirror xtool-core to the bionic-guards fork --- .github/workflows/build.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6482707..3d6257c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,6 +94,23 @@ jobs: sudo apt-get update sudo apt-get install -y --no-install-recommends autoconf automake libtool Android/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" + - name: Use bionic-guarded xtool-core + # TEMPORARY until xtool-org/xtool-core#2 is released: Superutils + # needs the Android guards for bionic Foundation. + run: | + mkdir -p ~/.swiftpm/configuration + cat > ~/.swiftpm/configuration/mirrors.json <<'EOF' + { + "object": [ + { + "original": "https://github.com/xtool-org/xtool-core", + "mirror": "https://github.com/hpr/xtool-core" + } + ], + "version": 1 + } + EOF + swift package update xtool-core - name: Cross-compile for Android run: | swift sdk list || true From 1791c17c28ed149437ff7f875c32c3bdde8d8dd6 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 16:19:06 -0400 Subject: [PATCH 12/58] Cross-build libcurl for libimobiledevice (mobileactivation) --- Android/build-native-libs.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 509f22e9..b4005b8f 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -77,6 +77,13 @@ build_autotools \ build_autotools \ https://github.com/libimobiledevice/libtatsu/releases/download/1.0.4/libtatsu-1.0.4.tar.bz2 \ libtatsu-1.0.4 +# libimobiledevice needs libcurl (mobileactivation talks to Apple's +# activation servers). +build_autotools \ + https://github.com/curl/curl/releases/download/curl-8_16_0/curl-8.16.0.tar.bz2 \ + curl-8.16.0 --disable-shared --enable-static --with-openssl --without-libpsl \ + --without-libidn2 --without-brotli --without-zstd --without-nghttp2 \ + --disable-ldap --disable-ldaps --with-ca-bundle=/system/etc/security/cacerts # libimobiledevice has no release tarball with the API SwiftyMobileDevice # needs; use master like the Linux Docker image does. fetch \ From 4f689303205b58a9343a569eec1dc2daf2351981 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 16:25:56 -0400 Subject: [PATCH 13/58] Build libcurl before libtatsu (tatsu configure requires it) --- Android/build-native-libs.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index b4005b8f..69652550 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -74,16 +74,16 @@ build_autotools \ 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 needs libcurl (mobileactivation talks to Apple's -# activation servers). +# libtatsu and libimobiledevice need libcurl (they talk to Apple's TSS +# and activation servers), so build it before them. build_autotools \ https://github.com/curl/curl/releases/download/curl-8_16_0/curl-8.16.0.tar.bz2 \ curl-8.16.0 --disable-shared --enable-static --with-openssl --without-libpsl \ --without-libidn2 --without-brotli --without-zstd --without-nghttp2 \ --disable-ldap --disable-ldaps --with-ca-bundle=/system/etc/security/cacerts +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 \ From f05f0a3397da4886b29a6526fe2790625b0cb8fe Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 16:31:13 -0400 Subject: [PATCH 14/58] Cross-build zlib before curl (NDK ships no zlib.pc) --- Android/build-native-libs.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 69652550..fe920fa2 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -75,7 +75,16 @@ build_autotools \ https://github.com/libimobiledevice/libusbmuxd/releases/download/2.1.0/libusbmuxd-2.1.0.tar.bz2 \ libusbmuxd-2.1.0 --without-udev # libtatsu and libimobiledevice need libcurl (they talk to Apple's TSS -# and activation servers), so build it before them. +# and activation servers), so build it before them; curl needs zlib, +# which the NDK does not ship pkg-config files for. +echo "==> zlib" +fetch https://zlib.net/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" ./configure --prefix="$PREFIX" --static + make -j"$(nproc)" install +) build_autotools \ https://github.com/curl/curl/releases/download/curl-8_16_0/curl-8.16.0.tar.bz2 \ curl-8.16.0 --disable-shared --enable-static --with-openssl --without-libpsl \ From 6b499d4502bb7161ade693fbab572c74ebc0f65d Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 16:36:00 -0400 Subject: [PATCH 15/58] Fetch zlib from GitHub releases (zlib.net flakes on CI) --- Android/build-native-libs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index fe920fa2..350ad8ff 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -78,7 +78,7 @@ build_autotools \ # and activation servers), so build it before them; curl needs zlib, # which the NDK does not ship pkg-config files for. echo "==> zlib" -fetch https://zlib.net/zlib-1.3.1.tar.gz zlib.tar.gz +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" From 6bd1a47f5c90119392f18e9611520829c188a744 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 16:41:20 -0400 Subject: [PATCH 16/58] Build zlib with -fPIC (tatsu links it into a shared lib) --- Android/build-native-libs.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 350ad8ff..efcecf4c 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -82,7 +82,8 @@ fetch https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz tar -C "$WORK" -xzf "$WORK/zlib.tar.gz" ( cd "$WORK/zlib-1.3.1" - CHOST="$TRIPLE" ./configure --prefix="$PREFIX" --static + # position-independent, like everything else we build + CHOST="$TRIPLE" CFLAGS="-fPIC" ./configure --prefix="$PREFIX" --static make -j"$(nproc)" install ) build_autotools \ From d2398085144d586ca13d27f7762dc5225525fd07 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 17:05:34 -0400 Subject: [PATCH 17/58] Provide .tarball-version for libimobiledevice git snapshot --- Android/build-native-libs.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index efcecf4c..24083f2f 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -102,6 +102,9 @@ fetch \ 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 make -j"$(nproc)" install ) From 7e7b9e5df77d336dde0ac5770bc0e0cef2aeada3 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 17:14:14 -0400 Subject: [PATCH 18/58] libimobiledevice: bionic has pthread_once in libc, not libpthread --- Android/build-native-libs.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 24083f2f..3dbde229 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -105,7 +105,9 @@ tar -C "$WORK" -xzf "$WORK/libimobiledevice.tar.gz" # 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 + # bionic's pthreads are in libc; there is no libpthread, so the + # acx_pthread -lpthread probe fails spuriously. pthread_once exists. + ac_cv_func_pthread_once=yes ./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython make -j"$(nproc)" install ) From 880392cb9753ddd5b0db2aaba0b3d268e755e6f8 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 17:19:01 -0400 Subject: [PATCH 19/58] libimobiledevice: fix cache var name for the pthread_once probe --- Android/build-native-libs.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 3dbde229..e8ec5197 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -106,8 +106,8 @@ tar -C "$WORK" -xzf "$WORK/libimobiledevice.tar.gz" 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 # bionic's pthreads are in libc; there is no libpthread, so the - # acx_pthread -lpthread probe fails spuriously. pthread_once exists. - ac_cv_func_pthread_once=yes ./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython + # AC_CHECK_LIB(pthread, pthread_once) probe fails spuriously. + ac_cv_lib_pthread_pthread_once=yes ./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython make -j"$(nproc)" install ) From cb6c9562c98063ccee68ed3f0a9cf8f3198333b0 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 17:25:20 -0400 Subject: [PATCH 20/58] Provide empty libpthread.a (bionic pthreads live in libc) --- Android/build-native-libs.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index e8ec5197..d9f336fb 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -64,6 +64,10 @@ build_autotools() { # [configure args...] ) } +# 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 "==> libimobiledevice stack" build_autotools \ https://github.com/libimobiledevice/libplist/releases/download/2.6.0/libplist-2.6.0.tar.bz2 \ @@ -105,9 +109,7 @@ tar -C "$WORK" -xzf "$WORK/libimobiledevice.tar.gz" # 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 - # bionic's pthreads are in libc; there is no libpthread, so the - # AC_CHECK_LIB(pthread, pthread_once) probe fails spuriously. - ac_cv_lib_pthread_pthread_once=yes ./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython + ./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython make -j"$(nproc)" install ) From d6cfa24865f963e950d65625c20d197493f3af92 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 17:43:55 -0400 Subject: [PATCH 21/58] Export CPPFLAGS/LDFLAGS so bare link probes find the prefix --- Android/build-native-libs.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index d9f336fb..17b49025 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -34,6 +34,10 @@ mkdir -p "$PREFIX" # 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: +# AC_CHECK_LIB link tests need -L, header checks need -I. +export CPPFLAGS="-I$PREFIX/include" +export LDFLAGS="-L$PREFIX/lib" fetch() { curl -sfL --retry 3 -o "$WORK/$2" "$1" From 39f5aa8f58d0721f1b1828fabaeca5335aa26ab4 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 18:33:53 -0400 Subject: [PATCH 22/58] CI: mirror unxip fork; generate pkg-config files for the sysroot - unxip upstream's zlib/getopt shims (included when the manifest compiles on a Linux host) clash with the NDK's own modules during Android cross builds; mirror to the fork whose manifest models the shims with .when(platforms:) until saagarjha/unxip#41 lands. - build-native-libs.sh: cross-build xz (unxip links liblzma) and emit pkg-config files into the SDK so SwiftPM's systemLibrary targets resolve cflags/libs against the sysroot instead of the host. --- .github/workflows/build.yml | 17 +++++++++++---- Android/build-native-libs.sh | 41 ++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d6257c3..9c525c59 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,9 +94,11 @@ jobs: sudo apt-get update sudo apt-get install -y --no-install-recommends autoconf automake libtool Android/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" - - name: Use bionic-guarded xtool-core - # TEMPORARY until xtool-org/xtool-core#2 is released: Superutils - # needs the Android guards for bionic Foundation. + - name: Use bionic-guarded forks of xtool-core and unxip + # TEMPORARY until xtool-org/xtool-core#2 is released and + # saagarjha/unxip#41 is merged: Superutils needs the Android + # guards for bionic Foundation, and unxip's zlib/getopt shims + # clash with the NDK's own modules when cross compiling. run: | mkdir -p ~/.swiftpm/configuration cat > ~/.swiftpm/configuration/mirrors.json <<'EOF' @@ -105,15 +107,22 @@ jobs: { "original": "https://github.com/xtool-org/xtool-core", "mirror": "https://github.com/hpr/xtool-core" + }, + { + "original": "https://github.com/saagarjha/unxip", + "mirror": "https://github.com/hpr/unxip" } ], "version": 1 } EOF - swift package update xtool-core + swift package update xtool-core unxip - name: Cross-compile for Android run: | swift sdk list || true + # point SwiftPM's systemLibrary pkg-config lookups at the + # cross-compiled libraries (see build-native-libs.sh) + export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 17b49025..ec4e1f85 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -59,8 +59,8 @@ tar -C "$WORK" -xzf "$WORK/openssl.tar.gz" build_autotools() { # [configure args...] local url=$1 dir=$2 shift 2 - fetch "$url" "$dir.tar.bz2" - tar -C "$WORK" -xjf "$WORK/$dir.tar.bz2" + fetch "$url" "$dir.tar" + tar -C "$WORK" -xf "$WORK/$dir.tar" ( cd "$WORK/$dir" ./configure --host="$TRIPLE" --prefix="$PREFIX" "$@" @@ -124,4 +124,41 @@ mkdir -p "$INC_DST" "$LIB_DST" cp -R "$PREFIX/include/." "$INC_DST/" cp -a "$PREFIX/lib/"*.a "$LIB_DST/" +# unxip links liblzma; build it too. +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" --disable-shared --enable-static + make -j"$(nproc)" install +) +cp -a "$PREFIX/lib/liblzma.a" "$LIB_DST/" + +# Generate pkg-config files pointing at the sysroot. SwiftPM's +# systemLibrary targets query pkg-config for cflags/libs; on this host +# pkg-config would otherwise resolve to host (x86_64) libraries. +echo "==> generating pkg-config files" +PC_DST=$SDK/swift-android/pkgconfig +mkdir -p "$PC_DST" +pc() { # [requires] + cat > "$PC_DST/$1.pc" < done: native libs installed into $SDK" From af5f878eac8387c84ddd408b8c895aca08970ef4 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 18:41:06 -0400 Subject: [PATCH 23/58] CI: rewrite unxip pin to fork (mirrors don't retarget pinned deps) --- .github/workflows/build.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c525c59..824ef9c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,6 +99,9 @@ jobs: # saagarjha/unxip#41 is merged: Superutils needs the Android # guards for bionic Foundation, and unxip's zlib/getopt shims # clash with the NDK's own modules when cross compiling. + # Mirrors alone don't retarget version-pinned deps, so also + # rewrite the unxip pin to the fork's 3.3 (which has the + # .when(platforms:) manifest). run: | mkdir -p ~/.swiftpm/configuration cat > ~/.swiftpm/configuration/mirrors.json <<'EOF' @@ -116,6 +119,14 @@ jobs: "version": 1 } EOF + python3 - <<'EOF2' + import json + p = json.load(open('Package.resolved')) + for pin in p['pins']: + if pin['identity'] == 'unxip': + pin['state']['revision'] = '7de3610da39c7cfa2635affab52fba169a94f4b4' + json.dump(p, open('Package.resolved', 'w'), indent=2) + EOF2 swift package update xtool-core unxip - name: Cross-compile for Android run: | From fc2bbb82dcf8fed73a28320f485d54317a1d0e82 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 18:50:33 -0400 Subject: [PATCH 24/58] CI: re-apply unxip pin after resolution (update recomputes it) --- .github/workflows/build.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 824ef9c4..970a8978 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -127,9 +127,23 @@ jobs: pin['state']['revision'] = '7de3610da39c7cfa2635affab52fba169a94f4b4' json.dump(p, open('Package.resolved', 'w'), indent=2) EOF2 - swift package update xtool-core unxip + # `swift package update unxip` recomputes the version and would + # overwrite the rewritten pin; only update xtool-core here, then + # re-apply the unxip pin afterwards (see below). + swift package update xtool-core - name: Cross-compile for Android run: | + # Re-apply the unxip fork pin AFTER any resolution (above): + # pinned-revision checkout goes through the mirror, which + # serves our 3.3 tag with the .when(platforms:) manifest. + python3 - <<'EOF3' + import json + p = json.load(open('Package.resolved')) + for pin in p['pins']: + if pin['identity'] == 'unxip': + pin['state']['revision'] = '7de3610da39c7cfa2635affab52fba169a94f4b4' + json.dump(p, open('Package.resolved', 'w'), indent=2) + EOF3 swift sdk list || true # point SwiftPM's systemLibrary pkg-config lookups at the # cross-compiled libraries (see build-native-libs.sh) From fe91a96771f78e8604f8160fcaa036e04ce7a44f Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 18:59:21 -0400 Subject: [PATCH 25/58] Build xz before installing headers into the sysroot --- Android/build-native-libs.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index ec4e1f85..868854c7 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -117,13 +117,6 @@ tar -C "$WORK" -xzf "$WORK/libimobiledevice.tar.gz" make -j"$(nproc)" install ) -echo "==> installing into SDK sysroot" -INC_DST=$SDK/ndk-sysroot/usr/include -LIB_DST=$SDK/ndk-sysroot/usr/lib/$TRIPLE -mkdir -p "$INC_DST" "$LIB_DST" -cp -R "$PREFIX/include/." "$INC_DST/" -cp -a "$PREFIX/lib/"*.a "$LIB_DST/" - # unxip links liblzma; build it too. echo "==> xz" fetch https://github.com/tukaani-project/xz/releases/download/v5.6.4/xz-5.6.4.tar.gz xz.tar @@ -133,7 +126,13 @@ tar -C "$WORK" -xf "$WORK/xz.tar" ./configure --host="$TRIPLE" --prefix="$PREFIX" --disable-shared --enable-static make -j"$(nproc)" install ) -cp -a "$PREFIX/lib/liblzma.a" "$LIB_DST/" + +echo "==> installing into SDK sysroot" +INC_DST=$SDK/ndk-sysroot/usr/include +LIB_DST=$SDK/ndk-sysroot/usr/lib/$TRIPLE +mkdir -p "$INC_DST" "$LIB_DST" +cp -R "$PREFIX/include/." "$INC_DST/" +cp -a "$PREFIX/lib/"*.a "$LIB_DST/" # Generate pkg-config files pointing at the sysroot. SwiftPM's # systemLibrary targets query pkg-config for cflags/libs; on this host From f5c58cf2a0483ca65c87b9f0e6205db0d31b1667 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 19:10:18 -0400 Subject: [PATCH 26/58] CI: re-apply xtool-core pin after resolution too --- .github/workflows/build.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 970a8978..b081fd7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -133,15 +133,18 @@ jobs: swift package update xtool-core - name: Cross-compile for Android run: | - # Re-apply the unxip fork pin AFTER any resolution (above): - # pinned-revision checkout goes through the mirror, which - # serves our 3.3 tag with the .when(platforms:) manifest. + # Re-apply the fork pins AFTER any resolution (above): + # `swift package update` recomputes versions from the canonical + # (cached) repos and overwrites rewritten pins. Pinned-revision + # checkouts go through the mirror, which serves the fork tags. python3 - <<'EOF3' import json p = json.load(open('Package.resolved')) for pin in p['pins']: if pin['identity'] == 'unxip': pin['state']['revision'] = '7de3610da39c7cfa2635affab52fba169a94f4b4' + if pin['identity'] == 'xtool-core': + pin['state']['revision'] = '58d5b679fa93f6e2a81901a54f43686ef3f53264' json.dump(p, open('Package.resolved', 'w'), indent=2) EOF3 swift sdk list || true From a5557f75ed78476f25795f6ff8d68ec8301eb7d8 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 19:21:49 -0400 Subject: [PATCH 27/58] Pass --target to the final link (Swift driver omits it) --- .github/workflows/build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b081fd7b..4bdf72b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,4 +153,9 @@ jobs: export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. - swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 + # The final executable link line omits --target, so clang + # defaults to the host triple and rejects the aarch64-only + # --fix-cortex-a53-843419 the Swift driver passes; set it + # explicitly. + swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 \ + -Xclang-linker --target=aarch64-unknown-linux-android28 From a922792b1170cc8071ab1b65225d4260d1ac968a Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 19:38:51 -0400 Subject: [PATCH 28/58] Link fix: explicit lld aarch64 emulation; verbose retry on failure --- .github/workflows/build.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bdf72b1..88468993 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,9 +153,14 @@ jobs: export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. - # The final executable link line omits --target, so clang - # defaults to the host triple and rejects the aarch64-only - # --fix-cortex-a53-843419 the Swift driver passes; set it - # explicitly. + # The final executable link: clang (Android aarch64 default) adds + # --fix-cortex-a53-843419, but generic ld.lld defaults to x86-64 + # emulation without an explicit -m and rejects the flag. Pass the + # emulation explicitly. swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 \ - -Xclang-linker --target=aarch64-unknown-linux-android28 + -Xclang-linker --target=aarch64-unknown-linux-android28 \ + -Xclang-linker -Wl,-m,aarch64elf \ + || { echo '==> link failed; retrying verbosely for diagnosis'; \ + swift build -v --product xtool --swift-sdk aarch64-unknown-linux-android28 \ + -Xclang-linker --target=aarch64-unknown-linux-android28 \ + -Xclang-linker -Wl,-m,aarch64elf 2>&1 | tail -80; exit 1; } From bc055e56643efc9cca7d2ef6096c9631bed3ea4a Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 19:39:54 -0400 Subject: [PATCH 29/58] Wrap -Xclang-linker in -Xswiftc (swift build rejects it bare) --- .github/workflows/build.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88468993..ccf8d6a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -156,11 +156,10 @@ jobs: # The final executable link: clang (Android aarch64 default) adds # --fix-cortex-a53-843419, but generic ld.lld defaults to x86-64 # emulation without an explicit -m and rejects the flag. Pass the - # emulation explicitly. + # emulation explicitly. (-Xclang-linker is a swiftc flag, so it + # has to be wrapped in -Xswiftc for swift build.) swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 \ - -Xclang-linker --target=aarch64-unknown-linux-android28 \ - -Xclang-linker -Wl,-m,aarch64elf \ + -Xswiftc -Xclang-linker -Xswiftc -Wl,-m,aarch64elf \ || { echo '==> link failed; retrying verbosely for diagnosis'; \ swift build -v --product xtool --swift-sdk aarch64-unknown-linux-android28 \ - -Xclang-linker --target=aarch64-unknown-linux-android28 \ - -Xclang-linker -Wl,-m,aarch64elf 2>&1 | tail -80; exit 1; } + -Xswiftc -Xclang-linker -Xswiftc -Wl,-m,aarch64elf 2>&1 | tail -80; exit 1; } From 3b68c4e2b3aebddcafe082eaf7ca45227c6ccef7 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 20:05:37 -0400 Subject: [PATCH 30/58] Shim ld.lld on PATH to inject aarch64 emulation for the final link --- .github/workflows/build.yml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ccf8d6a5..e80ca0c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -148,18 +148,19 @@ jobs: json.dump(p, open('Package.resolved', 'w'), indent=2) EOF3 swift sdk list || true - # point SwiftPM's systemLibrary pkg-config lookups at the - # cross-compiled libraries (see build-native-libs.sh) - export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" + # SwiftPM invokes the HOST toolchain's generic ld.lld for the + # final executable link (not the NDK clang wrapper). The NDK + # sysroot + aarch64 flags arrive via response file, but no + # emulation is selected, so lld defaults to x86-64 and rejects + # aarch64-only flags. Shim ld.lld on the PATH to inject it. + mkdir -p "$HOME/bin" + cat > "$HOME/bin/ld.lld" <<'EOF4' + #!/bin/sh + # Generic lld invocation for an Android/aarch64 target: pass + # through to the real host lld with the right emulation. + exec /home/runner/swift-6.3.2-RELEASE-ubuntu24.04/usr/bin/ld.lld -m aarch64elf "$@" + EOF4 + chmod +x "$HOME/bin/ld.lld" # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. - # The final executable link: clang (Android aarch64 default) adds - # --fix-cortex-a53-843419, but generic ld.lld defaults to x86-64 - # emulation without an explicit -m and rejects the flag. Pass the - # emulation explicitly. (-Xclang-linker is a swiftc flag, so it - # has to be wrapped in -Xswiftc for swift build.) - swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 \ - -Xswiftc -Xclang-linker -Xswiftc -Wl,-m,aarch64elf \ - || { echo '==> link failed; retrying verbosely for diagnosis'; \ - swift build -v --product xtool --swift-sdk aarch64-unknown-linux-android28 \ - -Xswiftc -Xclang-linker -Xswiftc -Wl,-m,aarch64elf 2>&1 | tail -80; exit 1; } + swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 From 0af2a117e1338c9488a1ca8e569ac7a493e931a4 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 20:42:04 -0400 Subject: [PATCH 31/58] Inject aarch64 lld emulation via the SDK toolset, not a PATH shim clang resolves ld.lld from its own program dir before PATH, so the shim could never intercept. The toolset's linker options provably reach the final link (its -z max-page-size=16384 appears in the response file), so append -Wl,-m,aarch64elf there instead. --- .github/workflows/build.yml | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e80ca0c0..77587fc7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -148,19 +148,26 @@ jobs: json.dump(p, open('Package.resolved', 'w'), indent=2) EOF3 swift sdk list || true - # SwiftPM invokes the HOST toolchain's generic ld.lld for the - # final executable link (not the NDK clang wrapper). The NDK - # sysroot + aarch64 flags arrive via response file, but no - # emulation is selected, so lld defaults to x86-64 and rejects - # aarch64-only flags. Shim ld.lld on the PATH to inject it. - mkdir -p "$HOME/bin" - cat > "$HOME/bin/ld.lld" <<'EOF4' - #!/bin/sh - # Generic lld invocation for an Android/aarch64 target: pass - # through to the real host lld with the right emulation. - exec /home/runner/swift-6.3.2-RELEASE-ubuntu24.04/usr/bin/ld.lld -m aarch64elf "$@" + # SwiftPM runs the final executable link through the HOST + # toolchain's clang, which resolves the generic ld.lld from its + # own program dir (PATH is searched only after -B dirs and the + # program dir, so a PATH shim cannot intercept). The link args + # arrive via response file with no emulation selected, so lld + # defaults to x86-64 and rejects aarch64-only flags. The SDK + # toolset's linker options DO reach this link (its + # -z max-page-size=16384 is in the response file), so inject the + # emulation there instead. + python3 - <<'EOF4' + import json, os + p = os.path.join(os.environ["ANDROID_SWIFT_SDK"], "swift-android", "swift-toolset.json") + j = json.load(open(p)) + linker = j.setdefault("linker", {}) + opts = linker.setdefault("extraCLIOptions", []) + if "-Wl,-m,aarch64elf" not in opts: + opts.append("-Wl,-m,aarch64elf") + json.dump(j, open(p, "w"), indent=2) + print("patched", p) EOF4 - chmod +x "$HOME/bin/ld.lld" # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 From b7b371648f3c450cab049d829de170ede324de22 Mon Sep 17 00:00:00 2001 From: Harry P Date: Sun, 16 Aug 2026 20:52:52 -0400 Subject: [PATCH 32/58] Toolset linker opts go verbatim to lld: use raw -m aarch64elf --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77587fc7..9f26e7da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -163,8 +163,11 @@ jobs: j = json.load(open(p)) linker = j.setdefault("linker", {}) opts = linker.setdefault("extraCLIOptions", []) - if "-Wl,-m,aarch64elf" not in opts: - opts.append("-Wl,-m,aarch64elf") + # toolset linker options are copied VERBATIM into the ld.lld + # response file (no -Wl, translation), so use raw lld syntax + # like the existing "-z" / "max-page-size=16384" entries. + if "-m" not in opts: + opts += ["-m", "aarch64elf"] json.dump(j, open(p, "w"), indent=2) print("patched", p) EOF4 From fad064cf5b7f80e1a29dbde336f6faa55ae4115a Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 06:14:08 -0400 Subject: [PATCH 33/58] DIAG: probe ld.lld -m handling (rsp vs argv) + verbose link dump --- .github/workflows/build.yml | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f26e7da..526e71c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,6 +72,25 @@ jobs: curl -sfL -o ndk.zip "https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip" unzip -q ndk.zip -d "$HOME" echo "ANDROID_NDK_HOME=$HOME/android-ndk-r${NDK_VERSION}" >> "$GITHUB_ENV" + # DIAGNOSTIC (temporary): the final swiftc link on CI fails with + # "ld.lld: error: --fix-cortex-a53-843419 is only supported on AArch64" + # even though clang's own dump shows "-m" "aarch64linux" in the + # response file. Probe whether this toolchain's ld.lld honors -m + # from a response file vs from argv. + - name: Probe ld.lld emulation handling + run: | + LD="$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin/ld.lld" + "$LD" --version | head -2 + echo 'int main(void){return 0;}' > /tmp/probe.c + clang --target=aarch64-linux-android28 \ + --sysroot="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot" \ + -c /tmp/probe.c -o /tmp/probe.o + printf '"-EL" "--fix-cortex-a53-843419" "-m" "aarch64elf" "-pie" "-o" "/tmp/probe-rsp.out" "/tmp/probe.o"\n' > /tmp/probe.rsp + echo '=== -m via response file ===' + "$LD" @/tmp/probe.rsp && echo RSP_OK + echo '=== -m via argv ===' + "$LD" -EL --fix-cortex-a53-843419 -m aarch64elf -pie -o /tmp/probe-argv.out /tmp/probe.o && echo ARGV_OK + true - name: Install Swift SDK for Android run: | # Install from a local file: URL installs land in a cache dir on @@ -173,4 +192,18 @@ jobs: EOF4 # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. - swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 + if swift build --product xtool --swift-sdk aarch64-unknown-linux-android28; then + exit 0 + fi + # DIAGNOSTIC (temporary): on link failure, re-run verbosely so + # clang dumps the ld.lld invocation + response file contents, + # then inspect any response files left on disk. + echo '=== verbose retry ===' + swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 -v 2>&1 | tail -100 || true + echo '=== response files on disk ===' + for f in /tmp/response-*.txt; do + [ -e "$f" ] || { echo '(none left on disk)'; break; } + echo "== $f" + grep -c 'aarch64elf' "$f" || true + done + exit 1 From 3e6632a1e7b66f24d3f996c59436fc58688971f5 Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 06:39:37 -0400 Subject: [PATCH 34/58] DIAG: wrap ld.lld to capture the actual response file bytes --- .github/workflows/build.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 526e71c4..89ee4e0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,6 +91,20 @@ jobs: echo '=== -m via argv ===' "$LD" -EL --fix-cortex-a53-843419 -m aarch64elf -pie -o /tmp/probe-argv.out /tmp/probe.o && echo ARGV_OK true + # DIAGNOSTIC (temporary): the probe above proves this ld.lld honors + # -m from a response file, yet the real link fails as if -m were + # absent even though clang's failure dump shows it. Capture the + # ACTUAL response file bytes lld receives. + - name: Wrap ld.lld to capture response files + run: | + BIN="$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" + mv "$BIN/ld.lld" "$BIN/ld.lld.real" + printf '%s\n' '#!/bin/sh' \ + 'for a in "$@"; do' \ + ' case "$a" in @*) cp "${a#@}" "/tmp/captured-rsp-$(date +%s%N).txt" ;; esac' \ + 'done' \ + 'exec "$(dirname "$0")/ld.lld.real" "$@"' > "$BIN/ld.lld" + chmod +x "$BIN/ld.lld" - name: Install Swift SDK for Android run: | # Install from a local file: URL installs land in a cache dir on @@ -206,4 +220,12 @@ jobs: echo "== $f" grep -c 'aarch64elf' "$f" || true done + echo '=== captured response files (via ld.lld wrapper) ===' + for f in /tmp/captured-rsp-*.txt; do + [ -e "$f" ] || { echo '(none captured)'; break; } + echo "== $f ($(wc -c < "$f") bytes)" + echo '-- first 200 bytes:'; head -c 200 "$f"; echo + echo '-- -m tokens:'; grep -o '"\?-m"\?[ "]*[a-z0-9_]*' "$f" | sort | uniq -c || true + echo '-- fix-cortex count:'; grep -c 'fix-cortex' "$f" || true + done exit 1 From d4af07fe051ed4770e7583df020484618d75fa4b Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 06:59:20 -0400 Subject: [PATCH 35/58] DIAG: rerun real lld on captured rsp + token-prefix bisect --- .github/workflows/build.yml | 70 +++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 89ee4e0b..479d512c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -209,23 +209,55 @@ jobs: if swift build --product xtool --swift-sdk aarch64-unknown-linux-android28; then exit 0 fi - # DIAGNOSTIC (temporary): on link failure, re-run verbosely so - # clang dumps the ld.lld invocation + response file contents, - # then inspect any response files left on disk. - echo '=== verbose retry ===' - swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 -v 2>&1 | tail -100 || true - echo '=== response files on disk ===' - for f in /tmp/response-*.txt; do - [ -e "$f" ] || { echo '(none left on disk)'; break; } - echo "== $f" - grep -c 'aarch64elf' "$f" || true - done - echo '=== captured response files (via ld.lld wrapper) ===' - for f in /tmp/captured-rsp-*.txt; do - [ -e "$f" ] || { echo '(none captured)'; break; } - echo "== $f ($(wc -c < "$f") bytes)" - echo '-- first 200 bytes:'; head -c 200 "$f"; echo - echo '-- -m tokens:'; grep -o '"\?-m"\?[ "]*[a-z0-9_]*' "$f" | sort | uniq -c || true - echo '-- fix-cortex count:'; grep -c 'fix-cortex' "$f" || true - done + # DIAGNOSTIC (temporary): the probe proved this ld.lld honors -m + # from a response file, yet the real link fails as if -m were + # absent while the captured response file demonstrably contains + # it. Re-run the real binary against the captured file and find + # the token region that neutralizes -m via prefix checkpoints. + echo '=== captured response file analysis ===' + BIN="$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" + f=$(ls /tmp/captured-rsp-*.txt | head -1) + [ -n "$f" ] || { echo '(none captured)'; exit 1; } + echo "file: $f ($(wc -c < "$f") bytes, $(wc -l < "$f") newlines)" + echo '-- first 16 bytes:'; head -c 16 "$f" | od -c | head -2 + echo '-- rerun real lld on captured file as-is:' + "$BIN/ld.lld.real" @"$f" 2>&1 | head -4 || true + echo '-- rerun with -m aarch64linux prepended on argv:' + "$BIN/ld.lld.real" -m aarch64linux @"$f" 2>&1 | head -4 || true + echo '=== token-prefix checkpoints ===' + python3 - "$f" "$BIN/ld.lld.real" <<'EOF' + import re, subprocess, sys + data = open(sys.argv[1]).read() + lld = sys.argv[2] + toks = re.findall(r'"[^"]*"|\S+', data) + print('token count:', len(toks)) + def check(n): + use = toks[:n] + ['"-o"', '"/tmp/b.out"', '"/tmp/probe.o"'] + open('/tmp/b.rsp', 'w').write(' '.join(use)) + r = subprocess.run([lld, '@/tmp/b.rsp'], + capture_output=True, text=True) + bad = 'only supported on AArch64' in r.stderr + first = (r.stderr.strip().splitlines() or ['(no stderr)'])[0] + print('prefix', n, '->', 'BAD' if bad else 'ok', '|', first[:110]) + return bad + mi = toks.index('"-m"') + print('first -m token index:', mi) + check(mi) + early = check(mi + 2) + full = check(len(toks)) + if not full: + print('FULL FILE OK ON RERUN: failure is environmental (wrapper?)') + elif early: + print('broken right after first -m: culprit in the head tokens') + else: + lo, hi = mi + 2, len(toks) + while lo < hi: + mid = (lo + hi) // 2 + if check(mid): + hi = mid + else: + lo = mid + 1 + print('first bad prefix length:', lo) + print('culprit region:', toks[max(0, lo - 5):lo + 2]) + EOF exit 1 From 9ff98f3a1059b70b59a391ef489ad24303f04756 Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 07:14:12 -0400 Subject: [PATCH 36/58] DIAG: preserve argv[0] in ld.lld wrapper and reruns --- .github/workflows/build.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 479d512c..b5b26571 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,11 +99,11 @@ jobs: run: | BIN="$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" mv "$BIN/ld.lld" "$BIN/ld.lld.real" - printf '%s\n' '#!/bin/sh' \ + printf '%s\n' '#!/bin/bash' \ 'for a in "$@"; do' \ ' case "$a" in @*) cp "${a#@}" "/tmp/captured-rsp-$(date +%s%N).txt" ;; esac' \ 'done' \ - 'exec "$(dirname "$0")/ld.lld.real" "$@"' > "$BIN/ld.lld" + 'exec -a ld.lld "$(dirname "$0")/ld.lld.real" "$@"' > "$BIN/ld.lld" chmod +x "$BIN/ld.lld" - name: Install Swift SDK for Android run: | @@ -218,14 +218,18 @@ jobs: BIN="$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" f=$(ls /tmp/captured-rsp-*.txt | head -1) [ -n "$f" ] || { echo '(none captured)'; exit 1; } + # ld.lld dispatches on argv[0]; use a copy named ld.lld so the + # rerun selects the GNU/ELF flavor like the real invocation. + mkdir -p /tmp/lldbin && cp "$BIN/ld.lld.real" /tmp/lldbin/ld.lld + LLD=/tmp/lldbin/ld.lld echo "file: $f ($(wc -c < "$f") bytes, $(wc -l < "$f") newlines)" echo '-- first 16 bytes:'; head -c 16 "$f" | od -c | head -2 - echo '-- rerun real lld on captured file as-is:' - "$BIN/ld.lld.real" @"$f" 2>&1 | head -4 || true + echo '-- rerun lld on captured file as-is:' + "$LLD" @"$f" 2>&1 | head -4 || true echo '-- rerun with -m aarch64linux prepended on argv:' - "$BIN/ld.lld.real" -m aarch64linux @"$f" 2>&1 | head -4 || true + "$LLD" -m aarch64linux @"$f" 2>&1 | head -4 || true echo '=== token-prefix checkpoints ===' - python3 - "$f" "$BIN/ld.lld.real" <<'EOF' + python3 - "$f" "$LLD" <<'EOF' import re, subprocess, sys data = open(sys.argv[1]).read() lld = sys.argv[2] From ac3d1547c9f6ed1c537dc1c69fff6a237b3fb148 Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 07:50:47 -0400 Subject: [PATCH 37/58] Fix Android cross link: scope pkg-config to the cross sysroot The link failure was not a missing emulation flag: host -L/usr/lib/x86_64-linux-gnu (from host pkg-config .pc files) let lld resolve -lm to Ubuntu's libm.so linker script, whose OUTPUT_FORMAT(elf64-x86-64) silently overrides -m in lld, dropping the link to x86-64. Point PKG_CONFIG_LIBDIR at the .pc files generated for the cross sysroot instead, and map SwiftPM's hardcoded -lstdc++ to the NDK's libc++ via a libstdc++.so script. --- .github/workflows/build.yml | 35 ++++++++++++----------------------- Android/build-native-libs.sh | 6 ++++++ 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5b26571..9033f293 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,29 +181,18 @@ jobs: json.dump(p, open('Package.resolved', 'w'), indent=2) EOF3 swift sdk list || true - # SwiftPM runs the final executable link through the HOST - # toolchain's clang, which resolves the generic ld.lld from its - # own program dir (PATH is searched only after -B dirs and the - # program dir, so a PATH shim cannot intercept). The link args - # arrive via response file with no emulation selected, so lld - # defaults to x86-64 and rejects aarch64-only flags. The SDK - # toolset's linker options DO reach this link (its - # -z max-page-size=16384 is in the response file), so inject the - # emulation there instead. - python3 - <<'EOF4' - import json, os - p = os.path.join(os.environ["ANDROID_SWIFT_SDK"], "swift-android", "swift-toolset.json") - j = json.load(open(p)) - linker = j.setdefault("linker", {}) - opts = linker.setdefault("extraCLIOptions", []) - # toolset linker options are copied VERBATIM into the ld.lld - # response file (no -Wl, translation), so use raw lld syntax - # like the existing "-z" / "max-page-size=16384" entries. - if "-m" not in opts: - opts += ["-m", "aarch64elf"] - json.dump(j, open(p, "w"), indent=2) - print("patched", p) - EOF4 + # SwiftPM's systemLibrary targets (xtool-core: openssl, + # libplist-2.0, ...) query pkg-config for cflags/libs. Without + # this, the host pkg-config resolves host (x86_64) .pc files and + # -L/usr/lib/x86_64-linux-gnu leaks into the link ahead of the + # NDK paths; lld then reads Ubuntu's libm.so *linker script*, + # whose OUTPUT_FORMAT(elf64-x86-64) silently overrides the -m + # emulation (lld/ELF/ScriptParser.cpp readOutputFormat), dropping + # the link to x86-64 so the Android-only --fix-cortex-a53-843419 + # is rejected. Point pkg-config at the .pc files generated by + # build-native-libs.sh so only the cross sysroot is visible. + export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" + export PKG_CONFIG_LIBDIR="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. if swift build --product xtool --swift-sdk aarch64-unknown-linux-android28; then diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 868854c7..3cb7752d 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -160,4 +160,10 @@ pc libcurl 8.16.0 "-lcurl -lssl -lcrypto -lz" pc libtatsu-1.0 1.0.4 "-ltatsu-1.0 -lcurl -lssl -lcrypto -lz" "libplist-2.0" pc libimobiledevice-1.0 2.0.0 "-limobiledevice-1.0" "libplist-2.0 libusbmuxd-2.0 libimobiledevice-glue-1.0 libtatsu-1.0 libcurl" +# SwiftPM links products containing C++ targets (zsign) with -lstdc++ for +# every non-Darwin/FreeBSD/Windows triple (BuildPlan+Product.swift), but +# Android's C++ runtime is libc++ and the NDK ships no libstdc++. Map the +# name onto the NDK's C++ runtime so the link resolves. +echo 'INPUT(-lc++)' > "$LIB_DST/libstdc++.so" + echo "==> done: native libs installed into $SDK" From 955652fb11169d2bfd692701dcc2412be49137ee Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 10:03:42 -0400 Subject: [PATCH 38/58] DIAG: dump -L tokens of captured response file + pkg-config env --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9033f293..239827d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -193,6 +193,8 @@ jobs: # build-native-libs.sh so only the cross sysroot is visible. export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" export PKG_CONFIG_LIBDIR="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" + echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH" + ls "$PKG_CONFIG_PATH" # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. if swift build --product xtool --swift-sdk aarch64-unknown-linux-android28; then @@ -213,6 +215,8 @@ jobs: LLD=/tmp/lldbin/ld.lld echo "file: $f ($(wc -c < "$f") bytes, $(wc -l < "$f") newlines)" echo '-- first 16 bytes:'; head -c 16 "$f" | od -c | head -2 + echo '-- -L tokens:' + grep -o '"-L[^"]*"' "$f" | sort | uniq -c echo '-- rerun lld on captured file as-is:' "$LLD" @"$f" 2>&1 | head -4 || true echo '-- rerun with -m aarch64linux prepended on argv:' From c8b19ea50bdf936c8849397ce7f1e6b539278377 Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 10:46:48 -0400 Subject: [PATCH 39/58] android CI: fix pkgconfig output dir to match PKG_CONFIG_PATH build-native-libs.sh receives the SDK's swift-android dir as $SDK and wrote .pc files to $SDK/swift-android/pkgconfig, but the build step exports PKG_CONFIG_PATH=$ANDROID_SWIFT_SDK/swift-android/pkgconfig. The exported dir didn't exist (job14: ls failed, killing the step under bash -e), so SwiftPM fell through to the host's .pc files and -L/usr/lib/x86_64-linux-gnu kept leaking into the link. Write to $SDK/pkgconfig instead; guard the diagnostic ls with || true. --- .github/workflows/build.yml | 2 +- Android/build-native-libs.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 239827d8..02377b04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -194,7 +194,7 @@ jobs: export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" export PKG_CONFIG_LIBDIR="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH" - ls "$PKG_CONFIG_PATH" + ls "$PKG_CONFIG_PATH" || true # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. if swift build --product xtool --swift-sdk aarch64-unknown-linux-android28; then diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 3cb7752d..878c217c 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -138,7 +138,7 @@ cp -a "$PREFIX/lib/"*.a "$LIB_DST/" # systemLibrary targets query pkg-config for cflags/libs; on this host # pkg-config would otherwise resolve to host (x86_64) libraries. echo "==> generating pkg-config files" -PC_DST=$SDK/swift-android/pkgconfig +PC_DST=$SDK/pkgconfig mkdir -p "$PC_DST" pc() { # [requires] cat > "$PC_DST/$1.pc" < Date: Mon, 17 Aug 2026 10:59:44 -0400 Subject: [PATCH 40/58] android CI: fix libtatsu link flag in generated .pc libtatsu's pkg-config file is versioned (libtatsu-1.0.pc) but its libtool target is not: it installs libtatsu.a, so -ltatsu-1.0 can never resolve. Use -ltatsu, matching upstream's own libtatsu-1.0.pc. --- Android/build-native-libs.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 878c217c..8b668a05 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -157,7 +157,9 @@ pc libplist-2.0 2.6.0 "-lplist-2.0" pc libusbmuxd-2.0 2.1.0 "-lusbmuxd-2.0" "libplist-2.0" pc libimobiledevice-glue-1.0 1.3.1 "-limobiledevice-glue-1.0" "libplist-2.0" pc libcurl 8.16.0 "-lcurl -lssl -lcrypto -lz" -pc libtatsu-1.0 1.0.4 "-ltatsu-1.0 -lcurl -lssl -lcrypto -lz" "libplist-2.0" +# libtatsu's .pc is versioned (libtatsu-1.0) but its libtool target is not: +# it installs libtatsu.a, so the link flag must be -ltatsu. +pc libtatsu-1.0 1.0.4 "-ltatsu -lcurl -lssl -lcrypto -lz" "libplist-2.0" pc libimobiledevice-1.0 2.0.0 "-limobiledevice-1.0" "libplist-2.0 libusbmuxd-2.0 libimobiledevice-glue-1.0 libtatsu-1.0 libcurl" # SwiftPM links products containing C++ targets (zsign) with -lstdc++ for From 1066ff1ada3f14b3fde0658f7f75148aae0b3237 Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 11:43:43 -0400 Subject: [PATCH 41/58] android CI: strip zstd debug sections from NDK static archives NDK r27's prebuilt libc.a et al. ship debug sections compressed with ELFCOMPRESS_ZSTD, and the swift.org Ubuntu toolchain's lld is built without zstd support, failing the final xtool link. Strip debug info from every static archive in the SDK's ndk-sysroot using the NDK's own llvm-strip (verified on-device: stripped archives link cleanly). --- Android/build-native-libs.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 8b668a05..ec16fb41 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -168,4 +168,11 @@ pc libimobiledevice-1.0 2.0.0 "-limobiledevice-1.0" "libplist-2.0 libusbmuxd-2.0 # name onto the NDK's C++ runtime so the link resolves. echo 'INPUT(-lc++)' > "$LIB_DST/libstdc++.so" +# The NDK's prebuilt static libraries (libc.a & co.) carry zstd-compressed +# debug sections, but the swift.org toolchain's lld is built without zstd +# support and errors out reading them ("is compressed with ELFCOMPRESS_ZSTD, +# but lld is not built with zstd support"). Strip debug sections from every +# static archive in the sysroot so lld can consume them. +find "$SDK/ndk-sysroot" -name '*.a' -exec llvm-strip --strip-debug {} + + echo "==> done: native libs installed into $SDK" From afc4d34c08d2e63a03b880a50e0981f55f468d83 Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 12:16:29 -0400 Subject: [PATCH 42/58] android CI: verify zstd debug strip took effect Job17 still hit ELFCOMPRESS_ZSTD errors on the sysroot libc.a despite the strip sweep, so instrument it: count stripped archives and fail the step if readelf still reports compressed sections in libc.a. --- Android/build-native-libs.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index ec16fb41..e150184f 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -174,5 +174,11 @@ echo 'INPUT(-lc++)' > "$LIB_DST/libstdc++.so" # but lld is not built with zstd support"). Strip debug sections from every # static archive in the sysroot so lld can consume them. find "$SDK/ndk-sysroot" -name '*.a' -exec llvm-strip --strip-debug {} + +# Verify the strip actually took: fail here, not at the final Swift link. +archives=$(find "$SDK/ndk-sysroot" -name '*.a' | wc -l) +libc="$SDK/ndk-sysroot/usr/lib/$TRIPLE/libc.a" +remaining=$(readelf -SW "$libc" | grep -E '^[[:space:]]+\[[[:space:]0-9]+\]' | grep -c ' C ' || true) +echo "stripped debug sections from $archives archives; compressed sections left in $libc: $remaining" +[ "$remaining" = 0 ] || { echo "ERROR: zstd-compressed sections remain in $libc" >&2; exit 1; } echo "==> done: native libs installed into $SDK" From c64806d881a9142da833a33022a231de1bf8ec2e Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 12:28:18 -0400 Subject: [PATCH 43/58] android CI: follow symlinks when stripping NDK static archives setup-android-sdk.sh's default SWIFT_ANDROID_NDK_LINK=1 symlinks ndk-sysroot/usr/lib/ into the NDK, and find's default -P never traverses symlinked dirs, so the strip sweep matched 0 archives (job18: 'stripped debug sections from 0 archives; compressed sections left: 2592'). Use find -L. Reproduced the exact layout on-device: -P finds 0, -L finds and strips through the symlink, lld links with no zstd errors. --- Android/build-native-libs.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index e150184f..c7f1b7a7 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -172,10 +172,12 @@ echo 'INPUT(-lc++)' > "$LIB_DST/libstdc++.so" # debug sections, but the swift.org toolchain's lld is built without zstd # support and errors out reading them ("is compressed with ELFCOMPRESS_ZSTD, # but lld is not built with zstd support"). Strip debug sections from every -# static archive in the sysroot so lld can consume them. -find "$SDK/ndk-sysroot" -name '*.a' -exec llvm-strip --strip-debug {} + +# static archive in the sysroot so lld can consume them. Note -L: the SDK's +# setup-android-sdk.sh symlinks ndk-sysroot/usr/lib/ into the NDK by +# default (SWIFT_ANDROID_NDK_LINK=1), and find's default -P won't traverse it. +find -L "$SDK/ndk-sysroot" -name '*.a' -exec llvm-strip --strip-debug {} + # Verify the strip actually took: fail here, not at the final Swift link. -archives=$(find "$SDK/ndk-sysroot" -name '*.a' | wc -l) +archives=$(find -L "$SDK/ndk-sysroot" -name '*.a' | wc -l) libc="$SDK/ndk-sysroot/usr/lib/$TRIPLE/libc.a" remaining=$(readelf -SW "$libc" | grep -E '^[[:space:]]+\[[[:space:]0-9]+\]' | grep -c ' C ' || true) echo "stripped debug sections from $archives archives; compressed sections left in $libc: $remaining" From bdab97a7b1fd12a9d748dc4722f8ccc86b5ad9e5 Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 12:37:15 -0400 Subject: [PATCH 44/58] android CI: tolerate unstrippable NDK archives The NDK ships some *.a files that are GNU ld scripts rather than object archives (e.g. i686-linux-android/23/libc++.a); llvm-strip errors on them ('not recognized as a valid object file') and set -e aborted the sweep before libc.a was stripped. Strip per-file with failures ignored; the readelf verification on the target-triple libc.a remains the gate. --- Android/build-native-libs.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index c7f1b7a7..09f29365 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -175,7 +175,12 @@ echo 'INPUT(-lc++)' > "$LIB_DST/libstdc++.so" # static archive in the sysroot so lld can consume them. Note -L: the SDK's # setup-android-sdk.sh symlinks ndk-sysroot/usr/lib/ into the NDK by # default (SWIFT_ANDROID_NDK_LINK=1), and find's default -P won't traverse it. -find -L "$SDK/ndk-sysroot" -name '*.a' -exec llvm-strip --strip-debug {} + +# Skip failures: some NDK "archives" (e.g. libc++.a in the per-API dirs) are +# GNU ld scripts, not objects, and llvm-strip can't parse them. +find -L "$SDK/ndk-sysroot" -name '*.a' -print0 | + while IFS= read -r -d '' a; do + llvm-strip --strip-debug "$a" 2>/dev/null || true + done # Verify the strip actually took: fail here, not at the final Swift link. archives=$(find -L "$SDK/ndk-sysroot" -name '*.a' | wc -l) libc="$SDK/ndk-sysroot/usr/lib/$TRIPLE/libc.a" From 3cef6272a755f818ead9ed6237488c77b6c2a808 Mon Sep 17 00:00:00 2001 From: Harry P Date: Mon, 17 Aug 2026 12:50:21 -0400 Subject: [PATCH 45/58] android CI: drop link diagnostics, verify product ELF The Android link is green; remove the temporary ld.lld probe, the response-file capture wrapper, and the failure-handler bisect. Keep the real fixes (pkg-config scoping, native lib build) and end the job with a file(1) check that the product is an aarch64 Android ELF, matching the other jobs' smoke-test convention (the binary can't run on x86_64). --- .github/workflows/build.yml | 100 ++---------------------------------- 1 file changed, 3 insertions(+), 97 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 02377b04..f6a09a68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,39 +72,6 @@ jobs: curl -sfL -o ndk.zip "https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip" unzip -q ndk.zip -d "$HOME" echo "ANDROID_NDK_HOME=$HOME/android-ndk-r${NDK_VERSION}" >> "$GITHUB_ENV" - # DIAGNOSTIC (temporary): the final swiftc link on CI fails with - # "ld.lld: error: --fix-cortex-a53-843419 is only supported on AArch64" - # even though clang's own dump shows "-m" "aarch64linux" in the - # response file. Probe whether this toolchain's ld.lld honors -m - # from a response file vs from argv. - - name: Probe ld.lld emulation handling - run: | - LD="$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin/ld.lld" - "$LD" --version | head -2 - echo 'int main(void){return 0;}' > /tmp/probe.c - clang --target=aarch64-linux-android28 \ - --sysroot="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot" \ - -c /tmp/probe.c -o /tmp/probe.o - printf '"-EL" "--fix-cortex-a53-843419" "-m" "aarch64elf" "-pie" "-o" "/tmp/probe-rsp.out" "/tmp/probe.o"\n' > /tmp/probe.rsp - echo '=== -m via response file ===' - "$LD" @/tmp/probe.rsp && echo RSP_OK - echo '=== -m via argv ===' - "$LD" -EL --fix-cortex-a53-843419 -m aarch64elf -pie -o /tmp/probe-argv.out /tmp/probe.o && echo ARGV_OK - true - # DIAGNOSTIC (temporary): the probe above proves this ld.lld honors - # -m from a response file, yet the real link fails as if -m were - # absent even though clang's failure dump shows it. Capture the - # ACTUAL response file bytes lld receives. - - name: Wrap ld.lld to capture response files - run: | - BIN="$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" - mv "$BIN/ld.lld" "$BIN/ld.lld.real" - printf '%s\n' '#!/bin/bash' \ - 'for a in "$@"; do' \ - ' case "$a" in @*) cp "${a#@}" "/tmp/captured-rsp-$(date +%s%N).txt" ;; esac' \ - 'done' \ - 'exec -a ld.lld "$(dirname "$0")/ld.lld.real" "$@"' > "$BIN/ld.lld" - chmod +x "$BIN/ld.lld" - name: Install Swift SDK for Android run: | # Install from a local file: URL installs land in a cache dir on @@ -180,7 +147,6 @@ jobs: pin['state']['revision'] = '58d5b679fa93f6e2a81901a54f43686ef3f53264' json.dump(p, open('Package.resolved', 'w'), indent=2) EOF3 - swift sdk list || true # SwiftPM's systemLibrary targets (xtool-core: openssl, # libplist-2.0, ...) query pkg-config for cflags/libs. Without # this, the host pkg-config resolves host (x86_64) .pc files and @@ -193,68 +159,8 @@ jobs: # build-native-libs.sh so only the cross sysroot is visible. export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" export PKG_CONFIG_LIBDIR="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" - echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH" - ls "$PKG_CONFIG_PATH" || true # The Android SDK registers API-level-suffixed triples, not the # bare aarch64-unknown-linux-android. - if swift build --product xtool --swift-sdk aarch64-unknown-linux-android28; then - exit 0 - fi - # DIAGNOSTIC (temporary): the probe proved this ld.lld honors -m - # from a response file, yet the real link fails as if -m were - # absent while the captured response file demonstrably contains - # it. Re-run the real binary against the captured file and find - # the token region that neutralizes -m via prefix checkpoints. - echo '=== captured response file analysis ===' - BIN="$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" - f=$(ls /tmp/captured-rsp-*.txt | head -1) - [ -n "$f" ] || { echo '(none captured)'; exit 1; } - # ld.lld dispatches on argv[0]; use a copy named ld.lld so the - # rerun selects the GNU/ELF flavor like the real invocation. - mkdir -p /tmp/lldbin && cp "$BIN/ld.lld.real" /tmp/lldbin/ld.lld - LLD=/tmp/lldbin/ld.lld - echo "file: $f ($(wc -c < "$f") bytes, $(wc -l < "$f") newlines)" - echo '-- first 16 bytes:'; head -c 16 "$f" | od -c | head -2 - echo '-- -L tokens:' - grep -o '"-L[^"]*"' "$f" | sort | uniq -c - echo '-- rerun lld on captured file as-is:' - "$LLD" @"$f" 2>&1 | head -4 || true - echo '-- rerun with -m aarch64linux prepended on argv:' - "$LLD" -m aarch64linux @"$f" 2>&1 | head -4 || true - echo '=== token-prefix checkpoints ===' - python3 - "$f" "$LLD" <<'EOF' - import re, subprocess, sys - data = open(sys.argv[1]).read() - lld = sys.argv[2] - toks = re.findall(r'"[^"]*"|\S+', data) - print('token count:', len(toks)) - def check(n): - use = toks[:n] + ['"-o"', '"/tmp/b.out"', '"/tmp/probe.o"'] - open('/tmp/b.rsp', 'w').write(' '.join(use)) - r = subprocess.run([lld, '@/tmp/b.rsp'], - capture_output=True, text=True) - bad = 'only supported on AArch64' in r.stderr - first = (r.stderr.strip().splitlines() or ['(no stderr)'])[0] - print('prefix', n, '->', 'BAD' if bad else 'ok', '|', first[:110]) - return bad - mi = toks.index('"-m"') - print('first -m token index:', mi) - check(mi) - early = check(mi + 2) - full = check(len(toks)) - if not full: - print('FULL FILE OK ON RERUN: failure is environmental (wrapper?)') - elif early: - print('broken right after first -m: culprit in the head tokens') - else: - lo, hi = mi + 2, len(toks) - while lo < hi: - mid = (lo + hi) // 2 - if check(mid): - hi = mid - else: - lo = mid + 1 - print('first bad prefix length:', lo) - print('culprit region:', toks[max(0, lo - 5):lo + 2]) - EOF - exit 1 + swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 + # Sanity-check the product: must be an aarch64 Android ELF. + file .build/aarch64-unknown-linux-android28/debug/xtool From 163780ca4f2df581db3c1141d2e9bd703c7c0fbf Mon Sep 17 00:00:00 2001 From: Harry P Date: Thu, 20 Aug 2026 19:36:50 -0400 Subject: [PATCH 46/58] Drop redundant libstdc++->libc++ mapping The NDK's per-API sysroot lib dirs (on the clang driver link path) ship a legacy libstdc++.so stub, so SwiftPM's -lstdc++ resolves without help and --as-needed drops it; verified by CI run 32412113040 building the full dependency graph (incl. zsign's C++) with the mapping absent. --- Android/build-native-libs.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 09f29365..d37600b0 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -162,11 +162,10 @@ pc libcurl 8.16.0 "-lcurl -lssl -lcrypto -lz" pc libtatsu-1.0 1.0.4 "-ltatsu -lcurl -lssl -lcrypto -lz" "libplist-2.0" pc libimobiledevice-1.0 2.0.0 "-limobiledevice-1.0" "libplist-2.0 libusbmuxd-2.0 libimobiledevice-glue-1.0 libtatsu-1.0 libcurl" -# SwiftPM links products containing C++ targets (zsign) with -lstdc++ for -# every non-Darwin/FreeBSD/Windows triple (BuildPlan+Product.swift), but -# Android's C++ runtime is libc++ and the NDK ships no libstdc++. Map the -# name onto the NDK's C++ runtime so the link resolves. -echo 'INPUT(-lc++)' > "$LIB_DST/libstdc++.so" +# SwiftPM's -lstdc++ for C++ products needs no mapping on Android: the NDK +# ships a legacy libstdc++.so stub in each per-API-level sysroot lib dir, +# which the clang driver puts on the link search path; --as-needed then +# drops it since the real C++ runtime (libc++_shared) provides the symbols. # The NDK's prebuilt static libraries (libc.a & co.) carry zstd-compressed # debug sections, but the swift.org toolchain's lld is built without zstd From d92c3a2296918fc757b585c8a4c84610329515d3 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Thu, 3 Sep 2026 17:40:12 -0400 Subject: [PATCH 47/58] Cleanup --- .github/workflows/build.yml | 37 ------------------------------------- Package.resolved | 20 +++++--------------- Package.swift | 5 +++-- 3 files changed, 8 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6a09a68..338d7ca7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,43 +94,6 @@ jobs: sudo apt-get update sudo apt-get install -y --no-install-recommends autoconf automake libtool Android/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" - - name: Use bionic-guarded forks of xtool-core and unxip - # TEMPORARY until xtool-org/xtool-core#2 is released and - # saagarjha/unxip#41 is merged: Superutils needs the Android - # guards for bionic Foundation, and unxip's zlib/getopt shims - # clash with the NDK's own modules when cross compiling. - # Mirrors alone don't retarget version-pinned deps, so also - # rewrite the unxip pin to the fork's 3.3 (which has the - # .when(platforms:) manifest). - run: | - mkdir -p ~/.swiftpm/configuration - cat > ~/.swiftpm/configuration/mirrors.json <<'EOF' - { - "object": [ - { - "original": "https://github.com/xtool-org/xtool-core", - "mirror": "https://github.com/hpr/xtool-core" - }, - { - "original": "https://github.com/saagarjha/unxip", - "mirror": "https://github.com/hpr/unxip" - } - ], - "version": 1 - } - EOF - python3 - <<'EOF2' - import json - p = json.load(open('Package.resolved')) - for pin in p['pins']: - if pin['identity'] == 'unxip': - pin['state']['revision'] = '7de3610da39c7cfa2635affab52fba169a94f4b4' - json.dump(p, open('Package.resolved', 'w'), indent=2) - EOF2 - # `swift package update unxip` recomputes the version and would - # overwrite the rewritten pin; only update xtool-core here, then - # re-apply the unxip pin afterwards (see below). - swift package update xtool-core - name: Cross-compile for Android run: | # Re-apply the fork pins AFTER any resolution (above): diff --git a/Package.resolved b/Package.resolved index bab42f9e..6b3598ac 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "41970e2ef95ed2d95550d89758de9edde764c927d6316551e31f0e6b2c05a55a", + "originHash" : "ddb5df9b2d803d0b99eeae678e32b9891f069860d15b189456c30e87b3bf8083", "pins" : [ { "identity" : "aexml", @@ -55,15 +55,6 @@ "version" : "6.2.0" } }, - { - "identity" : "opencombine", - "kind" : "remoteSourceControl", - "location" : "https://github.com/OpenCombine/OpenCombine.git", - "state" : { - "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", - "version" : "0.14.0" - } - }, { "identity" : "pathkit", "kind" : "remoteSourceControl", @@ -409,10 +400,9 @@ { "identity" : "unxip", "kind" : "remoteSourceControl", - "location" : "https://github.com/saagarjha/unxip", + "location" : "https://github.com/hpr/unxip", "state" : { - "revision" : "6c3990517fcc4c1db6952fccf4c562fb14097601", - "version" : "3.3.0" + "revision" : "7de3610da39c7cfa2635affab52fba169a94f4b4" } }, { @@ -465,8 +455,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xtool-org/xtool-core", "state" : { - "revision" : "d412ac768d1e73d16ad7ccc6805f12fc701709f6", - "version" : "1.4.0" + "revision" : "f5be7f3db0207e58f90eb38739afb3994300db25", + "version" : "1.4.1" } }, { diff --git a/Package.swift b/Package.swift index dfdd6782..d5658ff2 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/xtool-org/xtool-core", .upToNextMinor(from: "1.4.0")), + .package(url: "https://github.com/xtool-org/xtool-core", .upToNextMinor(from: "1.4.1")), .package(url: "https://github.com/xtool-org/SwiftyMobileDevice", .upToNextMinor(from: "1.5.0")), .package(url: "https://github.com/xtool-org/zsign", .upToNextMinor(from: "1.7.0")), @@ -67,7 +67,8 @@ let package = Package( .package(url: "https://github.com/attaswift/BigInt", from: "5.5.0"), .package(url: "https://github.com/mxcl/Version", from: "2.1.0"), .package(url: "https://github.com/jpsim/Yams", from: "5.1.3"), - .package(url: "https://github.com/saagarjha/unxip", from: "3.2.0"), + // temp override until https://github.com/saagarjha/unxip/pull/41 is merged and tagged + .package(url: "https://github.com/hpr/unxip", revision: "7de3610da39c7cfa2635affab52fba169a94f4b4"), // TODO: just depend on tuist/XcodeProj instead .package(url: "https://github.com/yonaskolb/XcodeGen", from: "2.45.4"), From f55d32ea9064ef304cfed7c77c285ee501f16274 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 15:13:17 -0400 Subject: [PATCH 48/58] Bump unxip --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 6b3598ac..7764e658 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ddb5df9b2d803d0b99eeae678e32b9891f069860d15b189456c30e87b3bf8083", + "originHash" : "0a077c7756c6b28817aaa00274cba3d31faa7752fd33d161347b8ac5e3cfa21d", "pins" : [ { "identity" : "aexml", @@ -402,7 +402,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/hpr/unxip", "state" : { - "revision" : "7de3610da39c7cfa2635affab52fba169a94f4b4" + "revision" : "b194eab4cc71234a9e915cc368277ab2cb4b8fc8" } }, { diff --git a/Package.swift b/Package.swift index d5658ff2..0c157ecd 100644 --- a/Package.swift +++ b/Package.swift @@ -68,7 +68,7 @@ let package = Package( .package(url: "https://github.com/mxcl/Version", from: "2.1.0"), .package(url: "https://github.com/jpsim/Yams", from: "5.1.3"), // temp override until https://github.com/saagarjha/unxip/pull/41 is merged and tagged - .package(url: "https://github.com/hpr/unxip", revision: "7de3610da39c7cfa2635affab52fba169a94f4b4"), + .package(url: "https://github.com/hpr/unxip", revision: "b194eab4cc71234a9e915cc368277ab2cb4b8fc8"), // TODO: just depend on tuist/XcodeProj instead .package(url: "https://github.com/yonaskolb/XcodeGen", from: "2.45.4"), From c303c3e1cb09e79fede981da3fe4f587a5fc4591 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 15:25:12 -0400 Subject: [PATCH 49/58] Try some simplifications --- .github/workflows/build.yml | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 338d7ca7..c60b55c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,24 +71,15 @@ jobs: run: | curl -sfL -o ndk.zip "https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip" unzip -q ndk.zip -d "$HOME" + rm -f ndk.zip echo "ANDROID_NDK_HOME=$HOME/android-ndk-r${NDK_VERSION}" >> "$GITHUB_ENV" - name: Install Swift SDK for Android run: | - # Install from a local file: URL installs land in a cache dir on - # some SwiftPM versions and the SDK then isn't found by - # `swift build --swift-sdk `; local-file installs - # register in ~/.swiftpm/swift-sdks. - curl -sfL --retry 3 -o /tmp/android-sdk.tar.gz \ - "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" - echo "${ANDROID_SDK_CHECKSUM} /tmp/android-sdk.tar.gz" | sha256sum -c - - swift sdk install /tmp/android-sdk.tar.gz --checksum "$ANDROID_SDK_CHECKSUM" + swift sdk install "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" --checksum "$ANDROID_SDK_CHECKSUM" bundle="$HOME/.swiftpm/swift-sdks/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" - test -d "$bundle" || bundle=$(find "$HOME" -maxdepth 6 -type d -name "swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" 2>/dev/null | head -1) test -n "$bundle" || { echo "SDK bundle not found" >&2; exit 1; } echo "ANDROID_SWIFT_SDK=$bundle" >> "$GITHUB_ENV" - # Populate the SDK's ndk-sysroot from the NDK (the bundle's - # setup-android-sdk.sh hardlinks it in). - (cd "$bundle/swift-android" && bash scripts/setup-android-sdk.sh) + "$bundle"/swift-android/scripts/setup-android-sdk.sh - name: Cross-build native libraries for Android run: | sudo apt-get update @@ -96,20 +87,6 @@ jobs: Android/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" - name: Cross-compile for Android run: | - # Re-apply the fork pins AFTER any resolution (above): - # `swift package update` recomputes versions from the canonical - # (cached) repos and overwrites rewritten pins. Pinned-revision - # checkouts go through the mirror, which serves the fork tags. - python3 - <<'EOF3' - import json - p = json.load(open('Package.resolved')) - for pin in p['pins']: - if pin['identity'] == 'unxip': - pin['state']['revision'] = '7de3610da39c7cfa2635affab52fba169a94f4b4' - if pin['identity'] == 'xtool-core': - pin['state']['revision'] = '58d5b679fa93f6e2a81901a54f43686ef3f53264' - json.dump(p, open('Package.resolved', 'w'), indent=2) - EOF3 # SwiftPM's systemLibrary targets (xtool-core: openssl, # libplist-2.0, ...) query pkg-config for cflags/libs. Without # this, the host pkg-config resolves host (x86_64) .pc files and From 29c5dc33efede108e7e9a86df2ad89186aacfad1 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 16:14:20 -0400 Subject: [PATCH 50/58] Dockerize --- .dockerignore | 1 + .github/workflows/build.yml | 50 +++--------------------------------- Android/README.md | 11 ++++++++ Android/build-native-libs.sh | 27 ++++++++++--------- Dockerfile | 31 +++++++++++++++++++++- docker-compose.yml | 10 ++++++++ 6 files changed, 70 insertions(+), 60 deletions(-) create mode 100644 Android/README.md diff --git a/.dockerignore b/.dockerignore index df31b0fe..94d5fc43 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,5 +4,6 @@ !/Sources !/Tests !/Linux +!/Android /Linux/packages /Linux/staging diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c60b55c5..a2129139 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,56 +51,12 @@ jobs: -scheme XKit -destination generic/platform=iOS \ | xcbeautify build-android: - # Cross-compile for Android (bionic) from Linux with the Swift SDK, - # validating the platform guards used for native Android hosts. runs-on: ubuntu-24.04 - env: - SWIFT_VERSION: 6.3.2 - # Keep in sync with the toolchain version above. - ANDROID_SDK_CHECKSUM: 939e933549d12d28f2e0bf71019d734d309859e9773c572657ce565a81f85d68 - NDK_VERSION: 27c steps: - name: Checkout uses: actions/checkout@v6 - - name: Install Swift toolchain - run: | - curl -sfL "https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2404/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04.tar.gz" \ - | tar -xzf - -C "$HOME" - echo "$HOME/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/usr/bin" >> "$GITHUB_PATH" - - name: Install Android NDK - run: | - curl -sfL -o ndk.zip "https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip" - unzip -q ndk.zip -d "$HOME" - rm -f ndk.zip - echo "ANDROID_NDK_HOME=$HOME/android-ndk-r${NDK_VERSION}" >> "$GITHUB_ENV" - - name: Install Swift SDK for Android - run: | - swift sdk install "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" --checksum "$ANDROID_SDK_CHECKSUM" - bundle="$HOME/.swiftpm/swift-sdks/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle" - test -n "$bundle" || { echo "SDK bundle not found" >&2; exit 1; } - echo "ANDROID_SWIFT_SDK=$bundle" >> "$GITHUB_ENV" - "$bundle"/swift-android/scripts/setup-android-sdk.sh - - name: Cross-build native libraries for Android - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends autoconf automake libtool - Android/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" - name: Cross-compile for Android run: | - # SwiftPM's systemLibrary targets (xtool-core: openssl, - # libplist-2.0, ...) query pkg-config for cflags/libs. Without - # this, the host pkg-config resolves host (x86_64) .pc files and - # -L/usr/lib/x86_64-linux-gnu leaks into the link ahead of the - # NDK paths; lld then reads Ubuntu's libm.so *linker script*, - # whose OUTPUT_FORMAT(elf64-x86-64) silently overrides the -m - # emulation (lld/ELF/ScriptParser.cpp readOutputFormat), dropping - # the link to x86-64 so the Android-only --fix-cortex-a53-843419 - # is rejected. Point pkg-config at the .pc files generated by - # build-native-libs.sh so only the cross sysroot is visible. - export PKG_CONFIG_PATH="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" - export PKG_CONFIG_LIBDIR="$ANDROID_SWIFT_SDK/swift-android/pkgconfig" - # The Android SDK registers API-level-suffixed triples, not the - # bare aarch64-unknown-linux-android. - swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 - # Sanity-check the product: must be an aarch64 Android ELF. - file .build/aarch64-unknown-linux-android28/debug/xtool + docker compose run --build --rm xtool-android bash -o pipefail -c \ + "swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 && \ + readelf -h .build/aarch64-unknown-linux-android28/debug/xtool | grep 'Machine:.*AArch64' > /dev/null" diff --git a/Android/README.md b/Android/README.md new file mode 100644 index 00000000..3662c364 --- /dev/null +++ b/Android/README.md @@ -0,0 +1,11 @@ +# Android development + +Build xtool for aarch64 Android (API 28) from the repository root: + +```sh +docker compose run --build --rm xtool-android \ + swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 +``` + +Run `docker compose run --build --rm xtool-android` to open a development +shell. diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index d37600b0..71c75873 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -7,6 +7,7 @@ # # Usage: Android/build-native-libs.sh # ANDROID_NDK_HOME must point at an unpacked NDK (>= r27). +# Native clang, clang++, ld.lld, and LLVM archive tools must be on PATH. # # The library set mirrors the Linux Docker image (see Dockerfile): OpenSSL # plus libplist/libimobiledevice-glue/libusbmuxd/libtatsu/libimobiledevice @@ -20,14 +21,17 @@ TRIPLE=aarch64-linux-android SDK=${1:?usage: build-native-libs.sh } : "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" -TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin -export PATH="$TOOLCHAIN:$PATH" -export CC="$TRIPLE$API-clang" -export CXX="$TRIPLE$API-clang++" -export AR=llvm-ar RANLIB=llvm-ranlib STRIP=llvm-strip -export ANDROID_NDK_ROOT=$ANDROID_NDK_HOME +# 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$API --sysroot=$NDK/sysroot -resource-dir=${RESOURCE_DIR[0]}" +export CXX="clang++ --target=$TRIPLE$API --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 PREFIX=$WORK/prefix mkdir -p "$PREFIX" # Point pkg-config exclusively at the cross prefix so the autotools builds @@ -37,7 +41,7 @@ export PKG_CONFIG_LIBDIR=$PREFIX/lib/pkgconfig # Make all configure probes (not just pkg-config ones) find the prefix: # AC_CHECK_LIB link tests need -L, header checks need -I. export CPPFLAGS="-I$PREFIX/include" -export LDFLAGS="-L$PREFIX/lib" +export LDFLAGS="-fuse-ld=lld -L$PREFIX/lib" fetch() { curl -sfL --retry 3 -o "$WORK/$2" "$1" @@ -50,8 +54,7 @@ fetch \ tar -C "$WORK" -xzf "$WORK/openssl.tar.gz" ( cd "$WORK/openssl-3.3.2" - ./Configure android-arm64 -D__ANDROID_API__=$API no-shared no-tests \ - --prefix="$PREFIX" + ./Configure linux-aarch64 no-shared no-tests --prefix="$PREFIX" make -j"$(nproc)" build_libs make install_dev ) @@ -136,7 +139,7 @@ cp -a "$PREFIX/lib/"*.a "$LIB_DST/" # Generate pkg-config files pointing at the sysroot. SwiftPM's # systemLibrary targets query pkg-config for cflags/libs; on this host -# pkg-config would otherwise resolve to host (x86_64) libraries. +# pkg-config would otherwise resolve to host libraries. echo "==> generating pkg-config files" PC_DST=$SDK/pkgconfig mkdir -p "$PC_DST" @@ -175,10 +178,10 @@ pc libimobiledevice-1.0 2.0.0 "-limobiledevice-1.0" "libplist-2.0 libusbmuxd-2.0 # setup-android-sdk.sh symlinks ndk-sysroot/usr/lib/ into the NDK by # default (SWIFT_ANDROID_NDK_LINK=1), and find's default -P won't traverse it. # Skip failures: some NDK "archives" (e.g. libc++.a in the per-API dirs) are -# GNU ld scripts, not objects, and llvm-strip can't parse them. +# GNU ld scripts, not objects, and llvm-objcopy can't parse them. find -L "$SDK/ndk-sysroot" -name '*.a' -print0 | while IFS= read -r -d '' a; do - llvm-strip --strip-debug "$a" 2>/dev/null || true + llvm-objcopy --strip-debug "$a" 2>/dev/null || true done # Verify the strip actually took: fail here, not at the final Swift link. archives=$(find -L "$SDK/ndk-sysroot" -name '*.a' | wc -l) diff --git a/Dockerfile b/Dockerfile index 6e9c71dc..39a440e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,8 @@ # Note: We use 22.04 since AppImage recommends building on the # oldest configuration that you support -FROM swift:6.3-jammy AS build-base +ARG SWIFT_VERSION=6.3.2 +FROM swift:${SWIFT_VERSION}-jammy AS build-base RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ @@ -98,6 +99,34 @@ ENV USBMUXD_SOCKET_ADDRESS=host.docker.internal:27015 CMD [ "/bin/bash" ] +FROM build-base AS dev-android + +ARG SWIFT_VERSION +# Keep in sync with SWIFT_VERSION above. +ARG ANDROID_SDK_CHECKSUM=939e933549d12d28f2e0bf71019d734d309859e9773c572657ce565a81f85d68 +ARG NDK_VERSION=27c + +ENV ANDROID_NDK_HOME=/opt/android-ndk-r${NDK_VERSION} +ENV ANDROID_SWIFT_SDK=/root/.swiftpm/swift-sdks/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle + +RUN curl -fSL --retry 3 -o /tmp/ndk.zip "https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip" \ + && unzip -q /tmp/ndk.zip -d /opt \ + && rm /tmp/ndk.zip + +RUN swift sdk install "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" --checksum "$ANDROID_SDK_CHECKSUM" \ + && "$ANDROID_SWIFT_SDK/swift-android/scripts/setup-android-sdk.sh" + +COPY Android/build-native-libs.sh /tmp/build-native-libs.sh +RUN /tmp/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" \ + && rm /tmp/build-native-libs.sh + +# Keep SwiftPM's systemLibrary targets from finding host libraries. +ENV PKG_CONFIG_PATH=${ANDROID_SWIFT_SDK}/swift-android/pkgconfig +ENV PKG_CONFIG_LIBDIR=${ANDROID_SWIFT_SDK}/swift-android/pkgconfig + +WORKDIR /xtool +CMD [ "/bin/bash" ] + FROM build-xtool-base AS build-xtool ADD Package.swift Package.resolved /xtool/ diff --git a/docker-compose.yml b/docker-compose.yml index c1d48c0d..868bcb5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,3 +16,13 @@ services: extra_hosts: # host.docker.internal doesn't exist by default on Linux hosts - "host.docker.internal:host-gateway" + xtool-android: + profiles: [android] + build: + context: . + target: dev-android + volumes: + - .:/xtool + - .build/android:/xtool/.build + stdin_open: true + tty: true From e6b23952c2e0aa3a78bea5986301b614831068f8 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 16:21:05 -0400 Subject: [PATCH 51/58] More cleanup --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 39a440e6..b401386a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,8 +102,6 @@ CMD [ "/bin/bash" ] FROM build-base AS dev-android ARG SWIFT_VERSION -# Keep in sync with SWIFT_VERSION above. -ARG ANDROID_SDK_CHECKSUM=939e933549d12d28f2e0bf71019d734d309859e9773c572657ce565a81f85d68 ARG NDK_VERSION=27c ENV ANDROID_NDK_HOME=/opt/android-ndk-r${NDK_VERSION} @@ -113,7 +111,9 @@ RUN curl -fSL --retry 3 -o /tmp/ndk.zip "https://dl.google.com/android/repositor && unzip -q /tmp/ndk.zip -d /opt \ && rm /tmp/ndk.zip -RUN swift sdk install "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" --checksum "$ANDROID_SDK_CHECKSUM" \ +RUN curl -fSL --retry 3 -o /tmp/swift-android-sdk.tar.gz "https://download.swift.org/swift-${SWIFT_VERSION}-release/android-sdk/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle.tar.gz" \ + && swift sdk install /tmp/swift-android-sdk.tar.gz \ + && rm /tmp/swift-android-sdk.tar.gz \ && "$ANDROID_SWIFT_SDK/swift-android/scripts/setup-android-sdk.sh" COPY Android/build-native-libs.sh /tmp/build-native-libs.sh From 1df67b1b2ea6190f4c1147aca40e031ba5a7307d Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 16:48:48 -0400 Subject: [PATCH 52/58] More changes --- .github/workflows/build.yml | 5 ++--- Android/build-native-libs.sh | 41 ++++++++++++++---------------------- Dockerfile | 7 +++--- Package.resolved | 6 +++--- Package.swift | 2 +- 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2129139..f4508b0f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,6 +57,5 @@ jobs: uses: actions/checkout@v6 - name: Cross-compile for Android run: | - docker compose run --build --rm xtool-android bash -o pipefail -c \ - "swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 && \ - readelf -h .build/aarch64-unknown-linux-android28/debug/xtool | grep 'Machine:.*AArch64' > /dev/null" + docker compose run --build --rm xtool-android \ + swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 71c75873..9ee59509 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash # Cross-builds the native libraries xtool links against (OpenSSL and the -# libimobiledevice stack) for aarch64 Android, installing them into the -# Swift SDK for Android's NDK sysroot so that +# libimobiledevice stack) for aarch64 Android, installing them into a +# separate prefix. Point PKG_CONFIG_PATH and PKG_CONFIG_LIBDIR at its +# lib/pkgconfig directory so that # swift build --swift-sdk aarch64-unknown-linux-android28 # can compile and link against them. # -# Usage: Android/build-native-libs.sh +# Usage: Android/build-native-libs.sh # ANDROID_NDK_HOME must point at an unpacked NDK (>= r27). # Native clang, clang++, ld.lld, and LLVM archive tools must be on PATH. # @@ -18,7 +19,10 @@ set -euo pipefail API=28 TRIPLE=aarch64-linux-android -SDK=${1:?usage: build-native-libs.sh } +SDK=${1:?usage: build-native-libs.sh } +DEST=${2:?usage: build-native-libs.sh } +mkdir -p "$DEST" +DEST=$(cd "$DEST" && pwd) : "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" # Only use the NDK's target headers and libraries, not its host executables. @@ -130,18 +134,18 @@ tar -C "$WORK" -xf "$WORK/xz.tar" make -j"$(nproc)" install ) -echo "==> installing into SDK sysroot" -INC_DST=$SDK/ndk-sysroot/usr/include -LIB_DST=$SDK/ndk-sysroot/usr/lib/$TRIPLE +echo "==> installing into $DEST" +INC_DST=$DEST/include +LIB_DST=$DEST/lib mkdir -p "$INC_DST" "$LIB_DST" cp -R "$PREFIX/include/." "$INC_DST/" cp -a "$PREFIX/lib/"*.a "$LIB_DST/" -# Generate pkg-config files pointing at the sysroot. SwiftPM's +# Generate pkg-config files pointing at the installation prefix. SwiftPM's # systemLibrary targets query pkg-config for cflags/libs; on this host # pkg-config would otherwise resolve to host libraries. echo "==> generating pkg-config files" -PC_DST=$SDK/pkgconfig +PC_DST=$DEST/lib/pkgconfig mkdir -p "$PC_DST" pc() { # [requires] cat > "$PC_DST/$1.pc" </dev/null || true - done -# Verify the strip actually took: fail here, not at the final Swift link. -archives=$(find -L "$SDK/ndk-sysroot" -name '*.a' | wc -l) -libc="$SDK/ndk-sysroot/usr/lib/$TRIPLE/libc.a" -remaining=$(readelf -SW "$libc" | grep -E '^[[:space:]]+\[[[:space:]0-9]+\]' | grep -c ' C ' || true) -echo "stripped debug sections from $archives archives; compressed sections left in $libc: $remaining" -[ "$remaining" = 0 ] || { echo "ERROR: zstd-compressed sections remain in $libc" >&2; exit 1; } +find -L "$SDK/ndk-sysroot" -name '*.a' -print0 \ + | xargs -0 -n1 -P1 -I {} bash -c 'llvm-objcopy --strip-debug "$0" 2>/dev/null || true' {} -echo "==> done: native libs installed into $SDK" +echo "==> done: native libs installed into $DEST" diff --git a/Dockerfile b/Dockerfile index b401386a..cd7b77a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -106,6 +106,7 @@ ARG NDK_VERSION=27c ENV ANDROID_NDK_HOME=/opt/android-ndk-r${NDK_VERSION} ENV ANDROID_SWIFT_SDK=/root/.swiftpm/swift-sdks/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle +ENV ANDROID_NATIVE_PREFIX=/opt/android-native RUN curl -fSL --retry 3 -o /tmp/ndk.zip "https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip" \ && unzip -q /tmp/ndk.zip -d /opt \ @@ -117,12 +118,12 @@ RUN curl -fSL --retry 3 -o /tmp/swift-android-sdk.tar.gz "https://download.swift && "$ANDROID_SWIFT_SDK/swift-android/scripts/setup-android-sdk.sh" COPY Android/build-native-libs.sh /tmp/build-native-libs.sh -RUN /tmp/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" \ +RUN /tmp/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" "$ANDROID_NATIVE_PREFIX" \ && rm /tmp/build-native-libs.sh # Keep SwiftPM's systemLibrary targets from finding host libraries. -ENV PKG_CONFIG_PATH=${ANDROID_SWIFT_SDK}/swift-android/pkgconfig -ENV PKG_CONFIG_LIBDIR=${ANDROID_SWIFT_SDK}/swift-android/pkgconfig +ENV PKG_CONFIG_PATH=${ANDROID_NATIVE_PREFIX}/lib/pkgconfig +ENV PKG_CONFIG_LIBDIR=${ANDROID_NATIVE_PREFIX}/lib/pkgconfig WORKDIR /xtool CMD [ "/bin/bash" ] diff --git a/Package.resolved b/Package.resolved index 7764e658..be4a160c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "0a077c7756c6b28817aaa00274cba3d31faa7752fd33d161347b8ac5e3cfa21d", + "originHash" : "4255bd8e7916b8091f5da45b983dde8bab3f0dc0a4a10f9c7a42d7be32b74d45", "pins" : [ { "identity" : "aexml", @@ -400,9 +400,9 @@ { "identity" : "unxip", "kind" : "remoteSourceControl", - "location" : "https://github.com/hpr/unxip", + "location" : "https://github.com/kabiroberai/unxip", "state" : { - "revision" : "b194eab4cc71234a9e915cc368277ab2cb4b8fc8" + "revision" : "5dfd415917b16218363f0f2ee7872a522d6abb0a" } }, { diff --git a/Package.swift b/Package.swift index 0c157ecd..1cb03d2f 100644 --- a/Package.swift +++ b/Package.swift @@ -68,7 +68,7 @@ let package = Package( .package(url: "https://github.com/mxcl/Version", from: "2.1.0"), .package(url: "https://github.com/jpsim/Yams", from: "5.1.3"), // temp override until https://github.com/saagarjha/unxip/pull/41 is merged and tagged - .package(url: "https://github.com/hpr/unxip", revision: "b194eab4cc71234a9e915cc368277ab2cb4b8fc8"), + .package(url: "https://github.com/kabiroberai/unxip", revision: "5dfd415917b16218363f0f2ee7872a522d6abb0a"), // TODO: just depend on tuist/XcodeProj instead .package(url: "https://github.com/yonaskolb/XcodeGen", from: "2.45.4"), From 768fabf760a34c5d248ccd6b27e38a409ae68b2f Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 17:13:04 -0400 Subject: [PATCH 53/58] Dynamic limd --- Android/README.md | 4 ++++ Android/build-native-libs.sh | 29 +++++++++-------------------- Dockerfile | 2 +- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/Android/README.md b/Android/README.md index 3662c364..37a130ab 100644 --- a/Android/README.md +++ b/Android/README.md @@ -9,3 +9,7 @@ docker compose run --build --rm xtool-android \ Run `docker compose run --build --rm xtool-android` to open a development shell. + +When deploying the executable to Android, include the `.so` files from +`/opt/android-native/lib` in the container, alongside the Swift/Android +runtime libraries required by the app. diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 9ee59509..c20e6a9a 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -6,21 +6,21 @@ # swift build --swift-sdk aarch64-unknown-linux-android28 # can compile and link against them. # -# Usage: Android/build-native-libs.sh +# Usage: Android/build-native-libs.sh # ANDROID_NDK_HOME must point at an unpacked NDK (>= r27). # Native clang, clang++, ld.lld, and LLVM archive tools must be on PATH. # # The library set mirrors the Linux Docker image (see Dockerfile): OpenSSL # plus libplist/libimobiledevice-glue/libusbmuxd/libtatsu/libimobiledevice -# from the libimobiledevice project, all built statically. libxadi is not +# from the libimobiledevice project, built as shared libraries. OpenSSL, +# curl, zlib, and xz remain static. libxadi is not # needed: XADIProvider is os(Linux)-only (on macOS/Android anisette uses # Omnisette), so the XADI system library never enters the link. set -euo pipefail API=28 TRIPLE=aarch64-linux-android -SDK=${1:?usage: build-native-libs.sh } -DEST=${2:?usage: build-native-libs.sh } +DEST=${1:?usage: build-native-libs.sh } mkdir -p "$DEST" DEST=$(cd "$DEST" && pwd) : "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" @@ -70,7 +70,7 @@ build_autotools() { # [configure args...] tar -C "$WORK" -xf "$WORK/$dir.tar" ( cd "$WORK/$dir" - ./configure --host="$TRIPLE" --prefix="$PREFIX" "$@" + ./configure --host="$TRIPLE" --prefix="$PREFIX" --enable-shared --disable-static "$@" make -j"$(nproc)" install ) } @@ -120,7 +120,7 @@ tar -C "$WORK" -xzf "$WORK/libimobiledevice.tar.gz" # 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 + ./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython --enable-shared --disable-static make -j"$(nproc)" install ) @@ -140,6 +140,7 @@ LIB_DST=$DEST/lib mkdir -p "$INC_DST" "$LIB_DST" cp -R "$PREFIX/include/." "$INC_DST/" cp -a "$PREFIX/lib/"*.a "$LIB_DST/" +cp -a "$PREFIX/lib/"*.so* "$LIB_DST/" # Generate pkg-config files pointing at the installation prefix. SwiftPM's # systemLibrary targets query pkg-config for cflags/libs; on this host @@ -166,19 +167,7 @@ pc libimobiledevice-glue-1.0 1.3.1 "-limobiledevice-glue-1.0" "libplist-2.0" pc libcurl 8.16.0 "-lcurl -lssl -lcrypto -lz" # libtatsu's .pc is versioned (libtatsu-1.0) but its libtool target is not: # it installs libtatsu.a, so the link flag must be -ltatsu. -pc libtatsu-1.0 1.0.4 "-ltatsu -lcurl -lssl -lcrypto -lz" "libplist-2.0" -pc libimobiledevice-1.0 2.0.0 "-limobiledevice-1.0" "libplist-2.0 libusbmuxd-2.0 libimobiledevice-glue-1.0 libtatsu-1.0 libcurl" - -# The NDK's prebuilt static libraries (libc.a & co.) carry zstd-compressed -# debug sections, but the swift.org toolchain's lld is built without zstd -# support and errors out reading them ("is compressed with ELFCOMPRESS_ZSTD, -# but lld is not built with zstd support"). Strip debug sections from every -# static archive in the sysroot so lld can consume them. Note -L: the SDK's -# setup-android-sdk.sh symlinks ndk-sysroot/usr/lib/ into the NDK by -# default (SWIFT_ANDROID_NDK_LINK=1), and find's default -P won't traverse it. -# Skip failures: some NDK "archives" (e.g. libc++.a in the per-API dirs) are -# GNU ld scripts, not objects, and llvm-objcopy can't parse them. -find -L "$SDK/ndk-sysroot" -name '*.a' -print0 \ - | xargs -0 -n1 -P1 -I {} bash -c 'llvm-objcopy --strip-debug "$0" 2>/dev/null || true' {} +pc libtatsu-1.0 1.0.4 "-ltatsu" "libplist-2.0" +pc libimobiledevice-1.0 2.0.0 "-limobiledevice-1.0" "libplist-2.0 libusbmuxd-2.0 libimobiledevice-glue-1.0 libtatsu-1.0" echo "==> done: native libs installed into $DEST" diff --git a/Dockerfile b/Dockerfile index cd7b77a9..ea51d6ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -118,7 +118,7 @@ RUN curl -fSL --retry 3 -o /tmp/swift-android-sdk.tar.gz "https://download.swift && "$ANDROID_SWIFT_SDK/swift-android/scripts/setup-android-sdk.sh" COPY Android/build-native-libs.sh /tmp/build-native-libs.sh -RUN /tmp/build-native-libs.sh "$ANDROID_SWIFT_SDK/swift-android" "$ANDROID_NATIVE_PREFIX" \ +RUN /tmp/build-native-libs.sh "$ANDROID_NATIVE_PREFIX" \ && rm /tmp/build-native-libs.sh # Keep SwiftPM's systemLibrary targets from finding host libraries. From fa12cdeea715ae2755e8a58f7497564e3849dcfa Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 17:58:50 -0400 Subject: [PATCH 54/58] More dynamic libs --- Android/build-native-libs.sh | 67 +++++++++--------------------------- 1 file changed, 16 insertions(+), 51 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index c20e6a9a..13c0705c 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -10,19 +10,19 @@ # ANDROID_NDK_HOME must point at an unpacked NDK (>= r27). # Native clang, clang++, ld.lld, and LLVM archive tools must be on PATH. # -# The library set mirrors the Linux Docker image (see Dockerfile): OpenSSL -# plus libplist/libimobiledevice-glue/libusbmuxd/libtatsu/libimobiledevice -# from the libimobiledevice project, built as shared libraries. OpenSSL, -# curl, zlib, and xz remain static. libxadi is not -# needed: XADIProvider is os(Linux)-only (on macOS/Android anisette uses -# Omnisette), so the XADI system library never enters the link. +# The library set mirrors the Linux Docker image (see Dockerfile) +# libxadi is not needed: XADIProvider is os(Linux)-only (on macOS/Android anisette +# uses Omnisette), so we don't need the xadi system library. + set -euo pipefail +shopt -s extglob API=28 TRIPLE=aarch64-linux-android -DEST=${1:?usage: build-native-libs.sh } -mkdir -p "$DEST" -DEST=$(cd "$DEST" && pwd) +PREFIX=${1:?usage: build-native-libs.sh } +rm -rf "$PREFIX" +mkdir -p "$PREFIX" +PREFIX=$(cd "$PREFIX" && pwd) : "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" # Only use the NDK's target headers and libraries, not its host executables. @@ -36,8 +36,6 @@ export STRIP="llvm-objcopy --strip-all" WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT -PREFIX=$WORK/prefix -mkdir -p "$PREFIX" # 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 @@ -58,7 +56,7 @@ fetch \ tar -C "$WORK" -xzf "$WORK/openssl.tar.gz" ( cd "$WORK/openssl-3.3.2" - ./Configure linux-aarch64 no-shared no-tests --prefix="$PREFIX" + ./Configure linux-aarch64 no-tests --prefix="$PREFIX" make -j"$(nproc)" build_libs make install_dev ) @@ -98,12 +96,12 @@ tar -C "$WORK" -xzf "$WORK/zlib.tar.gz" ( cd "$WORK/zlib-1.3.1" # position-independent, like everything else we build - CHOST="$TRIPLE" CFLAGS="-fPIC" ./configure --prefix="$PREFIX" --static + CHOST="$TRIPLE" CFLAGS="-fPIC" ./configure --prefix="$PREFIX" make -j"$(nproc)" install ) build_autotools \ https://github.com/curl/curl/releases/download/curl-8_16_0/curl-8.16.0.tar.bz2 \ - curl-8.16.0 --disable-shared --enable-static --with-openssl --without-libpsl \ + 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 build_autotools \ @@ -130,44 +128,11 @@ fetch https://github.com/tukaani-project/xz/releases/download/v5.6.4/xz-5.6.4.ta tar -C "$WORK" -xf "$WORK/xz.tar" ( cd "$WORK/xz-5.6.4" - ./configure --host="$TRIPLE" --prefix="$PREFIX" --disable-shared --enable-static + ./configure --host="$TRIPLE" --prefix="$PREFIX" --enable-shared --disable-static make -j"$(nproc)" install ) -echo "==> installing into $DEST" -INC_DST=$DEST/include -LIB_DST=$DEST/lib -mkdir -p "$INC_DST" "$LIB_DST" -cp -R "$PREFIX/include/." "$INC_DST/" -cp -a "$PREFIX/lib/"*.a "$LIB_DST/" -cp -a "$PREFIX/lib/"*.so* "$LIB_DST/" - -# Generate pkg-config files pointing at the installation prefix. SwiftPM's -# systemLibrary targets query pkg-config for cflags/libs; on this host -# pkg-config would otherwise resolve to host libraries. -echo "==> generating pkg-config files" -PC_DST=$DEST/lib/pkgconfig -mkdir -p "$PC_DST" -pc() { # [requires] - cat > "$PC_DST/$1.pc" < cleaning up $PREFIX" +rm -rf "$PREFIX"/!(include|lib) "$PREFIX"/lib/!(*.so*|pkgconfig) -echo "==> done: native libs installed into $DEST" +echo "==> done: native libs installed into $PREFIX" From 2339d0507bc76fcb2955241737335f51b3bc7e90 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 18:04:09 -0400 Subject: [PATCH 55/58] Semantic platform guards --- Package.resolved | 2 +- Package.swift | 2 +- Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift | 2 +- Sources/XKit/GrandSlam/Anisette/XADIProvider.swift | 2 +- Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift | 2 +- Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Package.resolved b/Package.resolved index be4a160c..06ed8ded 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "4255bd8e7916b8091f5da45b983dde8bab3f0dc0a4a10f9c7a42d7be32b74d45", + "originHash" : "785b055a62128030add1c38cd3f4f5357d2682da41d792ae6417602522fe32a2", "pins" : [ { "identity" : "aexml", diff --git a/Package.swift b/Package.swift index 1cb03d2f..96ed70e6 100644 --- a/Package.swift +++ b/Package.swift @@ -111,7 +111,7 @@ let package = Package( "DeveloperAPI", "CXKit", "XUtils", - .byName(name: "XADI", condition: .when(platforms: [.linux, .android])), + .byName(name: "XADI", condition: .when(platforms: [.linux])), .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "SwiftyMobileDevice", package: "SwiftyMobileDevice"), diff --git a/Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift b/Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift index 4cee00a9..085730c4 100644 --- a/Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift +++ b/Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift @@ -49,7 +49,7 @@ private struct UnimplementedRawADIProvider: RawADIProvider { private enum RawADIProviderDependencyKey: DependencyKey { static let testValue: RawADIProvider = UnimplementedRawADIProvider() static let liveValue: RawADIProvider = { - #if os(Linux) + #if canImport(XADI) return XADIProvider() #else return OmnisetteADIProvider() diff --git a/Sources/XKit/GrandSlam/Anisette/XADIProvider.swift b/Sources/XKit/GrandSlam/Anisette/XADIProvider.swift index 3183101c..b1bd2420 100644 --- a/Sources/XKit/GrandSlam/Anisette/XADIProvider.swift +++ b/Sources/XKit/GrandSlam/Anisette/XADIProvider.swift @@ -1,4 +1,4 @@ -#if os(Linux) +#if canImport(XADI) import Foundation import XADI diff --git a/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift b/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift index bb717312..2a46cada 100644 --- a/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift +++ b/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift @@ -5,7 +5,7 @@ // Created by Kabir Oberai on 05/05/21. // -#if os(Linux) || os(Android) +#if canImport(AsyncHTTPClient) import Foundation import AsyncHTTPClient import NIO diff --git a/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift b/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift index b0c3eda7..bc9e2cae 100644 --- a/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift +++ b/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift @@ -5,7 +5,7 @@ // Created by Kabir Oberai on 05/05/21. // -#if !os(Linux) && !os(Android) +#if canImport(Darwin) import Foundation import ConcurrencyExtras import OpenAPIRuntime From 2c4e1be56aed6f2b564ac84607d5f96550102731 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 18:39:00 -0400 Subject: [PATCH 56/58] Cleanup --- Android/build-native-libs.sh | 69 +++++++++++++-------------- Sources/XKit/Utilities/CHelpers.swift | 1 - 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/Android/build-native-libs.sh b/Android/build-native-libs.sh index 13c0705c..2c343ebd 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-native-libs.sh @@ -1,18 +1,16 @@ #!/usr/bin/env bash -# Cross-builds the native libraries xtool links against (OpenSSL and the -# libimobiledevice stack) for aarch64 Android, installing them into a -# separate prefix. Point PKG_CONFIG_PATH and PKG_CONFIG_LIBDIR at its -# lib/pkgconfig directory so that -# swift build --swift-sdk aarch64-unknown-linux-android28 -# can compile and link against them. -# + # Usage: Android/build-native-libs.sh -# ANDROID_NDK_HOME must point at an unpacked NDK (>= r27). -# Native clang, clang++, ld.lld, and LLVM archive tools must be on PATH. +# +# 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. # -# The library set mirrors the Linux Docker image (see Dockerfile) -# libxadi is not needed: XADIProvider is os(Linux)-only (on macOS/Android anisette -# uses Omnisette), so we don't need the xadi system library. +# After running this, point PKG_CONFIG_PATH and PKG_CONFIG_LIBDIR at +# `/lib/pkgconfig` so that SwiftPM can find them +# during +# swift build --swift-sdk aarch64-unknown-linux-android28 set -euo pipefail shopt -s extglob @@ -20,10 +18,13 @@ shopt -s extglob API=28 TRIPLE=aarch64-linux-android PREFIX=${1:?usage: build-native-libs.sh } +: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" + rm -rf "$PREFIX" mkdir -p "$PREFIX" PREFIX=$(cd "$PREFIX" && pwd) -: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" + +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. @@ -40,8 +41,7 @@ trap 'rm -rf "$WORK"' EXIT # 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: -# AC_CHECK_LIB link tests need -L, header checks need -I. +# 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" @@ -57,7 +57,7 @@ tar -C "$WORK" -xzf "$WORK/openssl.tar.gz" ( cd "$WORK/openssl-3.3.2" ./Configure linux-aarch64 no-tests --prefix="$PREFIX" - make -j"$(nproc)" build_libs + make -j"$PROCS" build_libs make install_dev ) @@ -69,7 +69,7 @@ build_autotools() { # [configure args...] ( cd "$WORK/$dir" ./configure --host="$TRIPLE" --prefix="$PREFIX" --enable-shared --disable-static "$@" - make -j"$(nproc)" install + make -j"$PROCS" install ) } @@ -77,33 +77,30 @@ build_autotools() { # [configure args...] # provide an empty static lib so -lpthread probes and links resolve. "$AR" cr "$PREFIX/lib/libpthread.a" -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 -# libtatsu and libimobiledevice need libcurl (they talk to Apple's TSS -# and activation servers), so build it before them; curl needs zlib, -# which the NDK does not ship pkg-config files for. -echo "==> zlib" +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" - # position-independent, like everything else we build CHOST="$TRIPLE" CFLAGS="-fPIC" ./configure --prefix="$PREFIX" - make -j"$(nproc)" install + 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 @@ -119,17 +116,17 @@ tar -C "$WORK" -xzf "$WORK/libimobiledevice.tar.gz" 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"$(nproc)" install + make -j"$PROCS" install ) -# unxip links liblzma; build it too. +# 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"$(nproc)" install + make -j"$PROCS" install ) echo "==> cleaning up $PREFIX" diff --git a/Sources/XKit/Utilities/CHelpers.swift b/Sources/XKit/Utilities/CHelpers.swift index 3b89cbde..f2877a19 100644 --- a/Sources/XKit/Utilities/CHelpers.swift +++ b/Sources/XKit/Utilities/CHelpers.swift @@ -13,7 +13,6 @@ import Android #endif #if os(Android) -// bionic's FILE is an incomplete type, imported as OpaquePointer package var stdoutSafe: OpaquePointer { get_stdout() } From 7205fd79abe827b434fe2c1348a38cacabc10939 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 21:36:58 -0400 Subject: [PATCH 57/58] Improvements --- .dockerignore | 1 + .github/workflows/build.yml | 4 +- .gitignore | 1 + Android/README.md | 15 ++-- .../{build-native-libs.sh => build-deps.sh} | 21 +++-- Android/build.sh | 85 ++++++++++++++++++ Android/defs.sh | 3 + Android/smoke-test.sh | 90 +++++++++++++++++++ Dockerfile | 8 +- Makefile | 9 ++ 10 files changed, 215 insertions(+), 22 deletions(-) rename Android/{build-native-libs.sh => build-deps.sh} (89%) create mode 100755 Android/build.sh create mode 100755 Android/defs.sh create mode 100755 Android/smoke-test.sh diff --git a/.dockerignore b/.dockerignore index 94d5fc43..bec26435 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,5 +5,6 @@ !/Tests !/Linux !/Android +/Android/output /Linux/packages /Linux/staging diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4508b0f..1c401d43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,6 +56,4 @@ jobs: - name: Checkout uses: actions/checkout@v6 - name: Cross-compile for Android - run: | - docker compose run --build --rm xtool-android \ - swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 + run: Android/build.sh --debug diff --git a/.gitignore b/.gitignore index 0fefab93..77ee58c1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.ipa .DS_Store /.build +/Android/output/ /Packages xcuserdata/ DerivedData/ diff --git a/Android/README.md b/Android/README.md index 37a130ab..b3a9a048 100644 --- a/Android/README.md +++ b/Android/README.md @@ -1,15 +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 -docker compose run --build --rm xtool-android \ - swift build --product xtool --swift-sdk aarch64-unknown-linux-android28 +make android [RELEASE=1] ``` -Run `docker compose run --build --rm xtool-android` to open a development -shell. +Run the Android runtime smoke checks with an existing ARM64 AVD: -When deploying the executable to Android, include the `.so` files from -`/opt/android-native/lib` in the container, alongside the Swift/Android -runtime libraries required by the app. +```sh +Android/smoke-test.sh Pixel_9a +``` diff --git a/Android/build-native-libs.sh b/Android/build-deps.sh similarity index 89% rename from Android/build-native-libs.sh rename to Android/build-deps.sh index 2c343ebd..3a3a2b42 100755 --- a/Android/build-native-libs.sh +++ b/Android/build-deps.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Usage: Android/build-native-libs.sh +# Usage: Android/build-deps.sh # # Cross-compiles necessary native libraries for aarch64 Android. # @@ -9,29 +9,32 @@ # # After running this, point PKG_CONFIG_PATH and PKG_CONFIG_LIBDIR at # `/lib/pkgconfig` so that SwiftPM can find them -# during -# swift build --swift-sdk aarch64-unknown-linux-android28 +# during build.sh. set -euo pipefail shopt -s extglob -API=28 -TRIPLE=aarch64-linux-android -PREFIX=${1:?usage: build-native-libs.sh } +: "${ANDROID_ARCH:?ANDROID_ARCH must be set}" + +TRIPLE=$ANDROID_ARCH-linux-android +PREFIX=${1:?usage: build-deps.sh } : "${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$API --sysroot=$NDK/sysroot -resource-dir=${RESOURCE_DIR[0]}" -export CXX="clang++ --target=$TRIPLE$API --sysroot=$NDK/sysroot -resource-dir=${RESOURCE_DIR[0]}" +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" @@ -56,7 +59,7 @@ fetch \ tar -C "$WORK" -xzf "$WORK/openssl.tar.gz" ( cd "$WORK/openssl-3.3.2" - ./Configure linux-aarch64 no-tests --prefix="$PREFIX" + ./Configure linux-$ANDROID_ARCH no-tests --prefix="$PREFIX" make -j"$PROCS" build_libs make install_dev ) diff --git a/Android/build.sh b/Android/build.sh new file mode 100755 index 00000000..a16af080 --- /dev/null +++ b/Android/build.sh @@ -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 -it 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" diff --git a/Android/defs.sh b/Android/defs.sh new file mode 100755 index 00000000..36dd03d1 --- /dev/null +++ b/Android/defs.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +export ANDROID_API_LEVEL=28 diff --git a/Android/smoke-test.sh b/Android/smoke-test.sh new file mode 100755 index 00000000..bf8e1258 --- /dev/null +++ b/Android/smoke-test.sh @@ -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 " >&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" diff --git a/Dockerfile b/Dockerfile index ea51d6ea..7359d682 100644 --- a/Dockerfile +++ b/Dockerfile @@ -104,6 +104,7 @@ FROM build-base AS dev-android ARG SWIFT_VERSION ARG NDK_VERSION=27c +ENV ANDROID_ARCH=aarch64 ENV ANDROID_NDK_HOME=/opt/android-ndk-r${NDK_VERSION} ENV ANDROID_SWIFT_SDK=/root/.swiftpm/swift-sdks/swift-${SWIFT_VERSION}-RELEASE_android.artifactbundle ENV ANDROID_NATIVE_PREFIX=/opt/android-native @@ -117,13 +118,14 @@ RUN curl -fSL --retry 3 -o /tmp/swift-android-sdk.tar.gz "https://download.swift && rm /tmp/swift-android-sdk.tar.gz \ && "$ANDROID_SWIFT_SDK/swift-android/scripts/setup-android-sdk.sh" -COPY Android/build-native-libs.sh /tmp/build-native-libs.sh -RUN /tmp/build-native-libs.sh "$ANDROID_NATIVE_PREFIX" \ - && rm /tmp/build-native-libs.sh +COPY Android/defs.sh Android/build-deps.sh /tmp/build-deps/ +RUN /tmp/build-deps/build-deps.sh "$ANDROID_NATIVE_PREFIX" \ + && rm -rf /tmp/build-deps # Keep SwiftPM's systemLibrary targets from finding host libraries. ENV PKG_CONFIG_PATH=${ANDROID_NATIVE_PREFIX}/lib/pkgconfig ENV PKG_CONFIG_LIBDIR=${ANDROID_NATIVE_PREFIX}/lib/pkgconfig +ENV XTL_ANDROID_ENV=1 WORKDIR /xtool CMD [ "/bin/bash" ] diff --git a/Makefile b/Makefile index 342f0a4a..64b4dd91 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ endif ifeq ($(RELEASE),1) INTERNAL_SWIFTFLAGS = -c release INTERNAL_XCFLAGS = -configuration Release +IS_RELEASE = 1 else INTERNAL_SWIFTFLAGS = -c debug INTERNAL_XCFLAGS = -configuration Debug @@ -86,6 +87,14 @@ mac-dist: @echo "bundle exec fastlane package" @cd macOS && bundle exec fastlane package +.PHONY: android +android: + Android/build.sh $(if $(IS_RELEASE),,--debug) + +.PHONY: android-dev +android-dev: + docker compose run --build --rm xtool-android + .PHONY: reload # update Xcode project and restart Xcode reload: From 7f58b4ccf9a0b1083b127c1504a6cc78fc8a602e Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 5 Sep 2026 21:39:48 -0400 Subject: [PATCH 58/58] Maybe fix --- Android/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/build.sh b/Android/build.sh index a16af080..9883a1fb 100755 --- a/Android/build.sh +++ b/Android/build.sh @@ -14,7 +14,7 @@ cd "$(dirname "${BASH_SOURCE[0]}")/.." if [[ "${XTL_ANDROID_ENV:-}" != 1 ]]; then # re-exec inside the Android build environment - exec docker compose run --build --rm -it xtool-android env XTL_ANDROID_NESTED=1 Android/build.sh "$@" + exec docker compose run --build --rm xtool-android env XTL_ANDROID_NESTED=1 Android/build.sh "$@" fi source Android/defs.sh