Skip to content

Support building natively on Android (bionic) hosts #658

Support building natively on Android (bionic) hosts

Support building natively on Android (bionic) hosts #658

Workflow file for this run

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