diff --git a/.dockerignore b/.dockerignore index df31b0fe..bec26435 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,5 +4,7 @@ !/Sources !/Tests !/Linux +!/Android +/Android/output /Linux/packages /Linux/staging diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c6bfb77..1c401d43 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 @@ -49,3 +50,10 @@ jobs: -skipMacroValidation -skipPackagePluginValidation \ -scheme XKit -destination generic/platform=iOS \ | xcbeautify + build-android: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Cross-compile for Android + run: Android/build.sh --debug 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 new file mode 100644 index 00000000..b3a9a048 --- /dev/null +++ b/Android/README.md @@ -0,0 +1,16 @@ +# Android development + +Run `make android-dev` to open a development +shell with the Android Swift SDK + dependencies configured. + +Build xtool for aarch64 Android (API 28) from the repository root: + +```sh +make android [RELEASE=1] +``` + +Run the Android runtime smoke checks with an existing ARM64 AVD: + +```sh +Android/smoke-test.sh Pixel_9a +``` diff --git a/Android/build-deps.sh b/Android/build-deps.sh new file mode 100755 index 00000000..3a3a2b42 --- /dev/null +++ b/Android/build-deps.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash + +# Usage: Android/build-deps.sh +# +# Cross-compiles necessary native libraries for aarch64 Android. +# +# ANDROID_NDK_HOME must point at an unpacked NDK (>= r27). +# Native clang, clang++, ld.lld, and LLVM archive tools must be on PATH. +# +# After running this, point PKG_CONFIG_PATH and PKG_CONFIG_LIBDIR at +# `/lib/pkgconfig` so that SwiftPM can find them +# during build.sh. + +set -euo pipefail +shopt -s extglob + +: "${ANDROID_ARCH:?ANDROID_ARCH must be set}" + +TRIPLE=$ANDROID_ARCH-linux-android +PREFIX=${1:?usage: build-deps.sh } +: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}" + +rm -rf "$PREFIX" +mkdir -p "$PREFIX" +PREFIX=$(cd "$PREFIX" && pwd) + +cd "$(dirname "${BASH_SOURCE[0]}")" +source defs.sh + +PROCS=$(nproc) + +# Only use the NDK's target headers and libraries, not its host executables. +# The Linux archive labels these directories linux-x86_64 even on ARM hosts. +NDK=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64 +RESOURCE_DIR=("$NDK"/lib/clang/*) +export CC="clang --target=$TRIPLE$ANDROID_API_LEVEL --sysroot=$NDK/sysroot -resource-dir=${RESOURCE_DIR[0]}" +export CXX="clang++ --target=$TRIPLE$ANDROID_API_LEVEL --sysroot=$NDK/sysroot -resource-dir=${RESOURCE_DIR[0]}" +export AR=llvm-ar RANLIB=llvm-ranlib NM=llvm-nm +export STRIP="llvm-objcopy --strip-all" + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +# Point pkg-config exclusively at the cross prefix so the autotools builds +# find each other instead of the host's libraries. +export PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig +export PKG_CONFIG_LIBDIR=$PREFIX/lib/pkgconfig +# Make all configure probes (not just pkg-config ones) find the prefix +export CPPFLAGS="-I$PREFIX/include" +export LDFLAGS="-fuse-ld=lld -L$PREFIX/lib" + +fetch() { + curl -sfL --retry 3 -o "$WORK/$2" "$1" +} + +echo "==> OpenSSL" +fetch \ + https://github.com/openssl/openssl/releases/download/openssl-3.3.2/openssl-3.3.2.tar.gz \ + openssl.tar.gz +tar -C "$WORK" -xzf "$WORK/openssl.tar.gz" +( + cd "$WORK/openssl-3.3.2" + ./Configure linux-$ANDROID_ARCH no-tests --prefix="$PREFIX" + make -j"$PROCS" build_libs + make install_dev +) + +build_autotools() { # [configure args...] + local url=$1 dir=$2 + shift 2 + fetch "$url" "$dir.tar" + tar -C "$WORK" -xf "$WORK/$dir.tar" + ( + cd "$WORK/$dir" + ./configure --host="$TRIPLE" --prefix="$PREFIX" --enable-shared --disable-static "$@" + make -j"$PROCS" install + ) +} + +# bionic's pthreads are in libc and modern NDKs ship no libpthread; +# provide an empty static lib so -lpthread probes and links resolve. +"$AR" cr "$PREFIX/lib/libpthread.a" + +echo "==> curl" +fetch https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz zlib.tar.gz +tar -C "$WORK" -xzf "$WORK/zlib.tar.gz" +( + cd "$WORK/zlib-1.3.1" + CHOST="$TRIPLE" CFLAGS="-fPIC" ./configure --prefix="$PREFIX" + make -j"$PROCS" install +) +build_autotools \ + https://github.com/curl/curl/releases/download/curl-8_16_0/curl-8.16.0.tar.bz2 \ + curl-8.16.0 --enable-shared --disable-static --with-openssl --without-libpsl \ + --without-libidn2 --without-brotli --without-zstd --without-nghttp2 \ + --disable-ldap --disable-ldaps --with-ca-bundle=/system/etc/security/cacerts + +echo "==> libimobiledevice stack" +build_autotools \ + https://github.com/libimobiledevice/libplist/releases/download/2.6.0/libplist-2.6.0.tar.bz2 \ + libplist-2.6.0 --without-cython +build_autotools \ + https://github.com/libimobiledevice/libimobiledevice-glue/releases/download/1.3.1/libimobiledevice-glue-1.3.1.tar.bz2 \ + libimobiledevice-glue-1.3.1 +build_autotools \ + https://github.com/libimobiledevice/libusbmuxd/releases/download/2.1.0/libusbmuxd-2.1.0.tar.bz2 \ + libusbmuxd-2.1.0 --without-udev +build_autotools \ + https://github.com/libimobiledevice/libtatsu/releases/download/1.0.4/libtatsu-1.0.4.tar.bz2 \ + libtatsu-1.0.4 +# libimobiledevice has no release tarball with the API SwiftyMobileDevice +# needs; use master like the Linux Docker image does. +fetch \ + https://codeload.github.com/libimobiledevice/libimobiledevice/tar.gz/refs/heads/master \ + libimobiledevice.tar.gz +tar -C "$WORK" -xzf "$WORK/libimobiledevice.tar.gz" +( + cd "$WORK/libimobiledevice-master" + # git-archive tarballs have no version info; provide one for bootstrap + git init -q . && git add -A && git -c user.email=ci@localhost -c user.name=ci commit -qm "libimobiledevice master snapshot" + echo "2.0.1-git" > .tarball-version + ./autogen.sh --host="$TRIPLE" --prefix="$PREFIX" --without-cython --enable-shared --disable-static + make -j"$PROCS" install +) + +# unxip links liblzma +echo "==> xz" +fetch https://github.com/tukaani-project/xz/releases/download/v5.6.4/xz-5.6.4.tar.gz xz.tar +tar -C "$WORK" -xf "$WORK/xz.tar" +( + cd "$WORK/xz-5.6.4" + ./configure --host="$TRIPLE" --prefix="$PREFIX" --enable-shared --disable-static + make -j"$PROCS" install +) + +echo "==> cleaning up $PREFIX" +rm -rf "$PREFIX"/!(include|lib) "$PREFIX"/lib/!(*.so*|pkgconfig) + +echo "==> done: native libs installed into $PREFIX" diff --git a/Android/build.sh b/Android/build.sh new file mode 100755 index 00000000..9883a1fb --- /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 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 6e9c71dc..7359d682 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,37 @@ ENV USBMUXD_SOCKET_ADDRESS=host.docker.internal:27015 CMD [ "/bin/bash" ] +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 + +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 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/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" ] + FROM build-xtool-base AS build-xtool ADD Package.swift Package.resolved /xtool/ 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: diff --git a/Package.resolved b/Package.resolved index 0bcfc149..06ed8ded 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "80db0cddb0e78cf6a4c79f94537cdfebd8004679161532fbae8a57ee9d1d8cd1", + "originHash" : "785b055a62128030add1c38cd3f4f5357d2682da41d792ae6417602522fe32a2", "pins" : [ { "identity" : "aexml", @@ -400,10 +400,9 @@ { "identity" : "unxip", "kind" : "remoteSourceControl", - "location" : "https://github.com/saagarjha/unxip", + "location" : "https://github.com/kabiroberai/unxip", "state" : { - "revision" : "6c3990517fcc4c1db6952fccf4c562fb14097601", - "version" : "3.3.0" + "revision" : "5dfd415917b16218363f0f2ee7872a522d6abb0a" } }, { @@ -456,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 8ea98c7f..96ed70e6 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/kabiroberai/unxip", revision: "5dfd415917b16218363f0f2ee7872a522d6abb0a"), // TODO: just depend on tuist/XcodeProj instead .package(url: "https://github.com/yonaskolb/XcodeGen", from: "2.45.4"), @@ -100,7 +101,7 @@ let package = Package( .product( name: "Subprocess", package: "swift-subprocess", - condition: .when(platforms: [.linux, .macOS]) + condition: .when(platforms: [.linux, .macOS, .android]) ), ] ), @@ -128,17 +129,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/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 eaa8f607..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) +#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 74527bfc..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) +#if canImport(Darwin) import Foundation import ConcurrencyExtras import OpenAPIRuntime diff --git a/Sources/XKit/Utilities/CHelpers.swift b/Sources/XKit/Utilities/CHelpers.swift index afd1e8ea..f2877a19 100644 --- a/Sources/XKit/Utilities/CHelpers.swift +++ b/Sources/XKit/Utilities/CHelpers.swift @@ -8,10 +8,19 @@ import Foundation import CXKit +#if os(Android) +import Android +#endif +#if os(Android) +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/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 { 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 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