Support building natively on Android (bionic) hosts #658
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-linux: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| host: | |
| - runner: ubuntu-24.04 | |
| arch: x86_64 | |
| - runner: ubuntu-24.04-arm | |
| arch: aarch64 | |
| runs-on: ${{ matrix.host.runner }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Build and test | |
| run: | | |
| docker compose run --build xtool bash -c \ | |
| "swift build --product xtool && .build/debug/xtool --help && swift test" | |
| build-macos: | |
| runs-on: macos-26 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - uses: ./.github/actions/configure-xcode | |
| - name: Build | |
| run: | | |
| swift build --product xtool && .build/debug/xtool --help | |
| - name: Run tests | |
| run: | | |
| swift test | |
| build-ios: | |
| runs-on: macos-26 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - uses: ./.github/actions/configure-xcode | |
| - name: Build | |
| run: | | |
| set -o pipefail \ | |
| && xcodebuild build \ | |
| -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 -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" | |
| 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 | |
| # some SwiftPM versions and the SDK then isn't found by | |
| # `swift build --swift-sdk <triple>`; 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; } | |
| 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: 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): | |
| # `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 | |
| # 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" | |
| 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 | |
| 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 |