diff --git a/.cargo/config.toml b/.cargo/config.toml index 8213778c8..f67a49591 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -16,5 +16,8 @@ runner = "docker/nix-cargo-runner.sh" [target.x86_64-unknown-linux-gnu] rustflags = ["-Clinker-features=-lld"] # dont use lld, it has issues with fenix rn +[target.aarch64-linux-android] +rustflags = ["-C", "debuginfo=line-tables-only", "-C", "strip=debuginfo"] + [alias] x = "run -q --package x --" diff --git a/.github/workflows/rust-ci.yaml b/.github/workflows/rust-ci.yaml index 95ec6fd5e..754eb0043 100644 --- a/.github/workflows/rust-ci.yaml +++ b/.github/workflows/rust-ci.yaml @@ -227,6 +227,53 @@ jobs: if-no-files-found: error retention-days: 14 + android-build: + name: Android Build + runs-on: public-ubuntu-24.04-8core + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3 + with: + token: ${{ secrets.ORB_GIT_HUB_TOKEN }} + - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # pin@v31.8.4 + with: + github_access_token: ${{ secrets.ORB_GIT_HUB_TOKEN }} + - uses: cachix/cachix-action@ad2ddac53f961de1989924296a1f236fcfbaa4fc # pin@v15 + continue-on-error: true + with: + name: worldcoin + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + - name: Authorize private git repos + run: git config --global url."https://${{ secrets.ORB_GIT_HUB_TOKEN }}@github.com".insteadOf https://github.com + - name: Cache cargo dependencies + uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # pin@v2.8.0 + with: + key: custom-${{ hashFiles('**/*.nix', 'flake.lock') }} + - name: Print environment + run: | + uname -a + nix develop -c env + + - name: Allow unprivileged user namespaces for bubblewrap + # apexer's bubblewrap FHS sandbox (nix/packages/android-apex.nix) + # always creates a user namespace for CAP_SYS_ADMIN, which Ubuntu + # 24.04's AppArmor blocks for unprofiled binaries like nix store + # paths ("bwrap: setting up uid map: Permission denied"). Safe to + # relax repo-wide here since this runner is ephemeral (ARC); a + # persistent host should ship a scoped AppArmor exception instead. + run: sudo sysctl --write kernel.apparmor_restrict_unprivileged_userns=0 + - name: Build and package Android APEXes + run: | + nix develop -c \ + cargo x android-apex + + - name: Upload APEX artifacts + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # pin@v4.3.3 + with: + name: android-apex + path: target/android-apex/*.apex + if-no-files-found: error + retention-days: 14 + cargo-deny: name: Cargo Deny runs-on: ubuntu-24.04 diff --git a/Cargo.lock b/Cargo.lock index 197c1a8fc..9e58c6019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7167,7 +7167,7 @@ dependencies = [ [[package]] name = "orb-backend-status" -version = "0.0.0" +version = "0.0.1" dependencies = [ "async-tempfile", "bon", @@ -7515,7 +7515,7 @@ version = "0.0.0" [[package]] name = "orb-jobs-agent" -version = "0.0.0" +version = "0.0.1" dependencies = [ "async-tempfile", "async-trait", @@ -7570,7 +7570,7 @@ dependencies = [ [[package]] name = "orb-jwk-util" -version = "0.0.0" +version = "0.0.1" dependencies = [ "base64 0.22.1", "clap", @@ -7644,7 +7644,7 @@ dependencies = [ [[package]] name = "orb-ota-backend" -version = "0.0.0" +version = "0.0.1" dependencies = [ "axum 0.8.6", "clap", @@ -7774,7 +7774,7 @@ dependencies = [ [[package]] name = "orb-se050" -version = "0.0.0" +version = "0.0.1" dependencies = [ "bon", "clap", @@ -7796,7 +7796,7 @@ dependencies = [ [[package]] name = "orb-se050-reprovision" -version = "0.0.0" +version = "0.0.1" dependencies = [ "base64 0.22.1", "bon", @@ -7928,7 +7928,7 @@ dependencies = [ [[package]] name = "orb-speed-test" -version = "0.0.0" +version = "0.0.1" dependencies = [ "base64 0.22.1", "clap", @@ -8281,7 +8281,7 @@ dependencies = [ [[package]] name = "orb-zbus-proxies-cli" -version = "0.0.0" +version = "0.0.1" dependencies = [ "clap", "color-eyre", @@ -13877,6 +13877,7 @@ dependencies = [ "orb-x-optee", "serde", "serde_json", + "tempfile", "tracing", "tracing-subscriber", ] diff --git a/agentwire/Cargo.toml b/agentwire/Cargo.toml index dff59c15e..696520ac8 100644 --- a/agentwire/Cargo.toml +++ b/agentwire/Cargo.toml @@ -53,4 +53,8 @@ tracing = "0.1" tokio = { version = "1", features = ["macros"] } [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] diff --git a/bidiff-cli/Cargo.toml b/bidiff-cli/Cargo.toml index 904360543..37430eade 100644 --- a/bidiff-cli/Cargo.toml +++ b/bidiff-cli/Cargo.toml @@ -43,3 +43,6 @@ test-log.workspace = true [build-dependencies] orb-build-info = { workspace = true, features = ["build-script"] } + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/bidiff-squashfs/lib/Cargo.toml b/bidiff-squashfs/lib/Cargo.toml index b187dc02f..9becb9267 100644 --- a/bidiff-squashfs/lib/Cargo.toml +++ b/bidiff-squashfs/lib/Cargo.toml @@ -21,3 +21,6 @@ cmd_lib.workspace = true color-eyre.workspace = true orb-telemetry = { workspace = true, default-features = false } tempfile.workspace = true + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/bidiff-squashfs/shim/Cargo.toml b/bidiff-squashfs/shim/Cargo.toml index 95b6a1a7d..363e77947 100644 --- a/bidiff-squashfs/shim/Cargo.toml +++ b/bidiff-squashfs/shim/Cargo.toml @@ -13,3 +13,6 @@ cc.workspace = true license-stub-glib.workspace = true license-stub-libsquashfs1.workspace = true pkg-config.workspace = true + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/can/Cargo.toml b/can/Cargo.toml index d4efd48a5..6ceaedc16 100644 --- a/can/Cargo.toml +++ b/can/Cargo.toml @@ -25,4 +25,8 @@ thiserror = { workspace = true, features = ["std"] } isotp = [] [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] diff --git a/efivar/Cargo.toml b/efivar/Cargo.toml index 3ec2b722e..cc3671c0f 100644 --- a/efivar/Cargo.toml +++ b/efivar/Cargo.toml @@ -14,3 +14,6 @@ rust-version.workspace = true [dependencies] color-eyre.workspace = true libc.workspace = true + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/gst-jpeg/Cargo.toml b/gst-jpeg/Cargo.toml index 66526f9eb..1ce35abb1 100644 --- a/gst-jpeg/Cargo.toml +++ b/gst-jpeg/Cargo.toml @@ -15,3 +15,6 @@ gstreamer-app.workspace = true gstreamer-video.workspace = true license-stub-gstreamer.workspace = true tracing.workspace = true + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/hil/Cargo.toml b/hil/Cargo.toml index 26c45d9eb..3bc7dc9b8 100644 --- a/hil/Cargo.toml +++ b/hil/Cargo.toml @@ -51,3 +51,6 @@ zbus.workspace = true [build-dependencies] orb-build-info = { path = "../build-info", features = ["build-script"] } + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/jwk-util/Cargo.toml b/jwk-util/Cargo.toml index da74036a7..2aa08a85e 100644 --- a/jwk-util/Cargo.toml +++ b/jwk-util/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orb-jwk-util" -version = "0.0.0" +version = "0.0.1" authors = ["Ryan Butler "] description = "CLI utility for manipulating Json Web Keys" publish = false diff --git a/mcu-interface/Cargo.toml b/mcu-interface/Cargo.toml index 9cae4adf6..08c0c41b9 100644 --- a/mcu-interface/Cargo.toml +++ b/mcu-interface/Cargo.toml @@ -24,7 +24,11 @@ tokio.workspace = true tracing.workspace = true [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] [dev-dependencies] futures.workspace = true diff --git a/mcu-util/Cargo.toml b/mcu-util/Cargo.toml index 48d124b31..394d0abbd 100644 --- a/mcu-util/Cargo.toml +++ b/mcu-util/Cargo.toml @@ -28,7 +28,11 @@ tracing.workspace = true orb-build-info = { path = "../build-info", features = ["build-script"] } [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] [package.metadata.deb] assets = [ diff --git a/nix/packages/android-apex.nix b/nix/packages/android-apex.nix new file mode 100644 index 000000000..f24dd78bd --- /dev/null +++ b/nix/packages/android-apex.nix @@ -0,0 +1,221 @@ +# Packages per-crate payloads staged by `cargo x android-apex` into +# signed `.apex` files. +# +# `apexer` and its apex_manifest/apex_build_info protobuf schemas only +# exist in AOSP source, not as a standalone tool, so we fetch just the +# `system/apex` repo and build it, substituting nixpkgs for the rest of +# its usual AOSP-build-only toolchain: android-tools for avbtool, aapt for +# aapt2. +# +# Payload filesystem is erofs. nixpkgs' erofs-utils can't build a +# canned_fs_config-capable mkfs.erofs (needs AOSP's libcutils, not +# buildable outside a full AOSP tree - see git history for the ext4 +# approach used before this). Instead we fetch Google's own prebuilt +# mkfs.erofs from kernel/prebuilts/build-tools and patch it to run under +# nixpkgs' glibc via autoPatchelfHook. +{ pkgs }: +let + # googlesource's raw-content endpoint only serves files base64-encoded + # (`?format=TEXT`); this fetches and decodes in one step. + fetchGoogleSourceFile = + { url, hash }: + pkgs.runCommand "google-source-file" { } '' + base64 -d ${pkgs.fetchurl { inherit url hash; }} > $out + ''; + + # Pinned for reproducibility - bump deliberately, and re-verify the hash + # below when you do. + aospRev = "android-16.0.0_r4"; + + apexSrc = pkgs.fetchgit { + url = "https://android.googlesource.com/platform/system/apex"; + rev = "refs/tags/${aospRev}"; + hash = "sha256-cKJfpbQR42ozKLaqSICeN5GLJtlDxAbeuCTEz1vXVpg="; + }; + + # apexer.py imports helpers from AOSP's build/soong/scripts/manifest.py - + # not worth cloning that whole (much larger) repo for one file. + manifestPy = fetchGoogleSourceFile { + url = "https://android.googlesource.com/platform/build/soong/+/refs/tags/${aospRev}/scripts/manifest.py?format=TEXT"; + hash = "sha256-MG7PUB2ZeNexpNgnKunBQl0gzFJA+BLfpTdhcvRnnlc="; + }; + + # AOSP's published AVB test key (external/avb/test/data) - test-only, + # matches apexer's SHA256_RSA4096 signing. Never a real key. + testKey = fetchGoogleSourceFile { + url = "https://android.googlesource.com/platform/external/avb/+/refs/tags/${aospRev}/test/data/testkey_rsa4096.pem?format=TEXT"; + hash = "sha256-5qt2JnvmWaLN1QclfuLxm4oKynyxPAjnBp2tiirUaiA="; + }; + + # AOSP's published test cert/key pair (build/target/product/security) - + # test-only, standard AOSP "testkey" used to APK-sign the outer container + # (see buildApex below). Never a real key. + testCertX509 = fetchGoogleSourceFile { + url = "https://android.googlesource.com/platform/build/+/refs/tags/${aospRev}/target/product/security/testkey.x509.pem?format=TEXT"; + hash = "sha256-vjogVTJxUkt/DdXLQcPC+iyqQfxeGPo/OAbUl92CDtM="; + }; + testCertPk8 = fetchGoogleSourceFile { + url = "https://android.googlesource.com/platform/build/+/refs/tags/${aospRev}/target/product/security/testkey.pk8?format=TEXT"; + hash = "sha256-kdco/lAWlHmJdQeAAvj6mqfdI7RZEBVAG2O30GrFKOk="; + }; + + # Google's own prebuilt mkfs.erofs (nixpkgs' erofs-utils lacks the + # -DWITH_ANDROID build canned_fs_config needs). Pinned separately from + # aospRev: this repo's tags use a different numbering scheme. + erofsPrebuiltRev = "android-16.0.0_r0.4"; + mkfsErofsBin = fetchGoogleSourceFile { + url = "https://android.googlesource.com/kernel/prebuilts/build-tools/+/refs/tags/${erofsPrebuiltRev}/linux-x86/bin/mkfs.erofs?format=TEXT"; + hash = "sha256-RH7THhG6cld3SDL9vaZxl7u1E1x3P8ClmMIveU7lT4c="; + }; + # AOSP's own libc++.so, to avoid an ABI mismatch with nixpkgs' - the + # rest of mkfs.erofs's deps come from nixpkgs' glibc via autoPatchelfHook. + mkfsErofsLibcxx = fetchGoogleSourceFile { + url = "https://android.googlesource.com/kernel/prebuilts/build-tools/+/refs/tags/${erofsPrebuiltRev}/linux-x86/lib64/libc%2B%2B.so?format=TEXT"; + hash = "sha256-msZUtf10GxcJ+uNPN5cV0aynTIXPZy/dYVaocfM8lXY="; + }; + mkfsErofs = pkgs.stdenv.mkDerivation { + pname = "mkfs-erofs-aosp-prebuilt"; + version = erofsPrebuiltRev; + dontUnpack = true; + nativeBuildInputs = [ pkgs.autoPatchelfHook ]; + buildInputs = [ pkgs.stdenv.cc.cc.lib ]; + installPhase = '' + mkdir -p $out/bin $out/lib + install -m755 ${mkfsErofsBin} $out/bin/mkfs.erofs + cp ${mkfsErofsLibcxx} $out/lib/libc++.so + ''; + }; + + # apexer shells out to `aapt2 link -I ` for the outer + # APK-style container, so we need some android.jar - not tied to aospRev, + # just recent enough for whatever AndroidManifest.xml apexer generates. + # build-tools is pulled in too, for `apksigner`, which APK-signs that + # otherwise-unsigned outer container (see buildApex below). + platformVersion = "36"; + buildToolsVersion = "36.0.0"; + androidPlatform = pkgs.androidenv.composeAndroidPackages { + platformVersions = [ platformVersion ]; + includeNDK = false; + includeEmulator = false; + includeSystemImages = false; + includeSources = false; + includeExtras = [ ]; + buildToolsVersions = [ buildToolsVersion ]; + }; + androidJar = "${androidPlatform.androidsdk}/libexec/android-sdk/platforms/android-${platformVersion}/android.jar"; + apksigner = "${androidPlatform.androidsdk}/libexec/android-sdk/build-tools/${buildToolsVersion}/apksigner"; + + pythonWithProtobuf = pkgs.python3.withPackages (ps: [ ps.protobuf ]); + + apexerToolchain = pkgs.runCommand "apexer-toolchain" { } '' + mkdir -p $out/bin + ln -s ${mkfsErofs}/bin/mkfs.erofs $out/bin/ + ln -s ${pkgs.aapt}/bin/aapt2 $out/bin/ + ln -s ${pkgs.android-tools}/bin/avbtool $out/bin/ + ''; + + apexer = pkgs.stdenv.mkDerivation { + pname = "apexer"; + version = aospRev; + src = apexSrc; + nativeBuildInputs = [ pkgs.protobuf ]; + buildPhase = '' + mkdir -p build + protoc --python_out=build proto/apex_build_info.proto proto/apex_manifest.proto + ''; + installPhase = '' + mkdir -p $out/lib/apexer $out/bin + cp apexer/*.py $out/lib/apexer/ + cp build/proto/*.py $out/lib/apexer/ + cp ${manifestPy} $out/lib/apexer/manifest.py + cat < $out/bin/apexer + #!${pkgs.runtimeShell} + export PYTHONPATH="\$PYTHONPATH:$out/lib/apexer" + export APEXER_TOOL_PATH="${apexerToolchain}/bin" + exec ${pythonWithProtobuf}/bin/python3 "$out/lib/apexer/apexer.py" "\$@" + EOF + chmod +x $out/bin/apexer + ''; + }; + + # apexer.py shells out to /usr/bin/fallocate, /bin/cp, /bin/ls - absent + # on a bare NixOS host, so run it inside a synthetic FHS root. + apexerFHS = pkgs.buildFHSEnv { + name = "apexer"; + targetPkgs = pkgs: [ + apexer + pkgs.coreutils + pkgs.util-linux + ]; + runScript = "apexer"; + }; + + # JSON -> compiled apex_manifest.pb, using apexer's own generated + # protobuf module. Avoids needing AOSP's separate conv_apex_manifest host + # tool (and whatever repo that would drag in) for this one conversion. + compileApexManifest = pkgs.runCommand "compile-apex-manifest" { } '' + mkdir -p $out/bin + cat < $out/compile_apex_manifest.py + import sys, json + sys.path.insert(0, "${apexer}/lib/apexer") + from google.protobuf.json_format import Parse + from apex_manifest_pb2 import ApexManifest + data = json.load(sys.stdin) + msg = Parse(json.dumps(data), ApexManifest()) + sys.stdout.buffer.write(msg.SerializeToString()) + PYEOF + cat < $out/bin/compile-apex-manifest + #!${pkgs.runtimeShell} + exec ${pythonWithProtobuf}/bin/python3 "$out/compile_apex_manifest.py" + EOF + chmod +x $out/bin/compile-apex-manifest + ''; + + # Packages one already-staged payload dir (as produced by `cargo x + # android-apex`) into `.apex`, always signed with the AOSP test + # key/cert above - never use this for a real release. + buildApex = pkgs.writeShellApplication { + name = "build-apex"; + runtimeInputs = [ + compileApexManifest + apexerFHS + pkgs.jdk21_headless + ]; + text = '' + set -euo pipefail + if [ $# -ne 2 ]; then + echo "usage: build-apex " >&2 + exit 1 + fi + payload=$1 + out=$2 + work=$(mktemp -d) + trap 'rm -rf "$work"' EXIT + + compile-apex-manifest < "$payload/apex_manifest.json" > "$work/apex_manifest.pb" + + # apexer only AVB-signs the inner payload (--key below); APK-sign the + # outer container here too, so both apexd and PackageManager accept it. + apexer -v \ + --manifest "$work/apex_manifest.pb" \ + --file_contexts "$payload/file_contexts" \ + --canned_fs_config "$payload/canned_fs_config" \ + --key "${testKey}" \ + --payload_type image \ + --payload_fs_type erofs \ + --android_jar_path "${androidJar}" \ + --do_not_check_keyname \ + --force \ + "$payload/content" "$work/unsigned.apex" + + "${apksigner}" sign \ + --cert "${testCertX509}" \ + --key "${testCertPk8}" \ + --in "$work/unsigned.apex" \ + --out "$out" + ''; + }; +in +{ + inherit buildApex; +} diff --git a/nix/packages/android-ndk.nix b/nix/packages/android-ndk.nix new file mode 100644 index 000000000..ac107aa2d --- /dev/null +++ b/nix/packages/android-ndk.nix @@ -0,0 +1,45 @@ +# The Android NDK's standalone Clang-based cross-compilation toolchain, used +# to link Rust crates for Android targets (see the `targets` list in +# ../../rust-toolchain.toml). We only need the NDK's toolchain, not the rest +# of the Android SDK (build-tools/platform-tools/emulator/etc), so we compose +# a minimal package set that skips all of that. +{ pkgs }: +let + # Pinned explicitly (rather than "latest") so bumping nixpkgs doesn't + # silently change the NDK version, and therefore the linker/output of + # Android builds, out from under us. + ndkVersion = "28.2.13676358"; # NDK r28c + + # The lowest Android API level the cross-compiled binaries will support. + # Bump this if a higher floor is ever required; the NDK ships one prebuilt + # Clang wrapper per API level (e.g. `aarch64-linux-android24-clang`), all + # from the same toolchain, so this alone is enough to change it. + apiLevel = 24; + + androidComposition = pkgs.androidenv.composeAndroidPackages { + ndkVersions = [ ndkVersion ]; + includeNDK = true; + includeEmulator = false; + includeSystemImages = false; + includeSources = false; + includeExtras = [ ]; + abiVersions = [ "arm64-v8a" ]; + platformVersions = [ ]; + buildToolsVersions = [ ]; + }; + + ndkRoot = "${androidComposition.androidsdk}/libexec/android-sdk/ndk-bundle"; + hostTag = if pkgs.stdenv.hostPlatform.isDarwin then "darwin-x86_64" else "linux-x86_64"; + llvmBin = "${ndkRoot}/toolchains/llvm/prebuilt/${hostTag}/bin"; +in +{ + inherit ndkRoot apiLevel; + + # Toolchain for aarch64-linux-android (the only Android target this + # workspace cross-compiles to today - see rust-toolchain.toml). If a + # second ABI is ever needed, generalize this into a per-triple attrset + # then. + cc = "${llvmBin}/aarch64-linux-android${toString apiLevel}-clang"; + ar = "${llvmBin}/llvm-ar"; + ranlib = "${llvmBin}/llvm-ranlib"; +} diff --git a/nix/packages/nixpkgs.nix b/nix/packages/nixpkgs.nix index 32193ca2f..53a6821a6 100644 --- a/nix/packages/nixpkgs.nix +++ b/nix/packages/nixpkgs.nix @@ -17,6 +17,11 @@ let # Configuration for nixpkgs. config = { allowUnfree = true; + # Accepts the Android SDK/NDK terms of service, required to build + # `pkgs.androidenv.composeAndroidPackages` (used by + # nix/packages/android-ndk.nix for cross-compiling to Android). + # See https://developer.android.com/studio/terms + android_sdk.accept_license = true; }; flake = abort "this should be specified in nixos modules, its inert here"; } diff --git a/nix/shells/development.nix b/nix/shells/development.nix index b26a8d01a..6ac0e9e30 100644 --- a/nix/shells/development.nix +++ b/nix/shells/development.nix @@ -25,6 +25,10 @@ let }; rustPlatform = p.native.makeRustPlatform { inherit (rustToolchain) cargo rustc; }; + # Only wired up on Linux hosts for now (untested on Darwin), matching the + # other cross-compilation toolchains in this file (e.g. OP-TEE below). + androidNdk = import ../packages/android-ndk.nix { pkgs = p.native; }; + macFrameworks = p.native.apple-sdk_15; # Set PKG_CONFIG_PATH for the cross-compiled libraries @@ -125,6 +129,7 @@ in # venv uv # python venv management + android-tools awscli2 bacon # better cargo-watch black # Python autoformatter @@ -200,6 +205,15 @@ in export OPTEE_CLIENT_EXPORT_x86_64_unknown_linux_gnu="${optee-client-pkg-x86}"; export TEEC_STATIC=1; export TA_DEV_KIT_DIR="${optee-os-devkit-pkg}"; + + # Android NDK toolchain, used by `cargo build --target + # aarch64-linux-android`. Not every crate builds for Android + # (dbus/systemd/gstreamer-dependent crates won't) - expected. + export ANDROID_NDK_HOME="${androidNdk.ndkRoot}"; + export CC_aarch64_linux_android="${androidNdk.cc}"; + export AR_aarch64_linux_android="${androidNdk.ar}"; + export RANLIB_aarch64_linux_android="${androidNdk.ranlib}"; + export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${androidNdk.cc}"; '' else "" diff --git a/nix/shells/flake-outputs.nix b/nix/shells/flake-outputs.nix index 89678d17e..e4de63f83 100644 --- a/nix/shells/flake-outputs.nix +++ b/nix/shells/flake-outputs.nix @@ -8,10 +8,16 @@ let lib = inputs.nixpkgs.lib; tegraBashFHS = import ./tegra-bash.nix { pkgs = instantiatedPkgs.x86_64-linux; }; nfsboot = import ./nfsboot.nix { pkgs = instantiatedPkgs.x86_64-linux; }; + androidApex = import ../packages/android-apex.nix { pkgs = instantiatedPkgs.x86_64-linux; }; a = { # Used like a dev shell, but only for flashing. packages."x86_64-linux"."tegra-bash" = tegraBashFHS; + # Packages a staged Android APEX payload dir into a signed `.apex` - + # see nix/packages/android-apex.nix. The name "build-apex" is matched + # by a hardcoded string in xtask/src/cmd/android.rs's flake_ref - + # nothing checks the two stay in sync, so grep both if renaming. + packages."x86_64-linux"."build-apex" = androidApex.buildApex; devShells.x86_64-linux.nfsboot = nfsboot; }; b = flake-utils.lib.eachDefaultSystem ( diff --git a/optee/secure-storage/ca/Cargo.toml b/optee/secure-storage/ca/Cargo.toml index 57e16bb9d..2a1069c3e 100644 --- a/optee/secure-storage/ca/Cargo.toml +++ b/optee/secure-storage/ca/Cargo.toml @@ -39,7 +39,11 @@ branch = "main" git = "https://github.com/worldcoin/orb-rustzone.git" [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] [package.metadata.deb] assets = [["target/release/orb-secure-storage-ca", "/usr/local/bin/", "755"]] diff --git a/orb-backend-status/Cargo.toml b/orb-backend-status/Cargo.toml index 030fe625c..c4ea70c98 100644 --- a/orb-backend-status/Cargo.toml +++ b/orb-backend-status/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orb-backend-status" -version = "0.0.0" +version = "0.0.1" description = "Systemd service that receives orb status and periodicallyprovides it to the Orb backend" authors = ["Paul Quinn "] publish = false diff --git a/orb-connd/Cargo.toml b/orb-connd/Cargo.toml index e04701520..bc003f43f 100644 --- a/orb-connd/Cargo.toml +++ b/orb-connd/Cargo.toml @@ -87,7 +87,11 @@ prelude = { workspace = true, features = ["future"] } orb-build-info = { workspace = true, features = ["build-script"] } [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] [package.metadata.deb] maintainer-scripts = "debian/" diff --git a/orb-core/agent-iroh/Cargo.toml b/orb-core/agent-iroh/Cargo.toml index 6f9a087de..3be64fc1b 100644 --- a/orb-core/agent-iroh/Cargo.toml +++ b/orb-core/agent-iroh/Cargo.toml @@ -34,4 +34,8 @@ tokio-util.workspace = true tracing-subscriber.workspace = true [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] diff --git a/orb-jobs-agent/Cargo.toml b/orb-jobs-agent/Cargo.toml index 661a16941..dc7456da1 100644 --- a/orb-jobs-agent/Cargo.toml +++ b/orb-jobs-agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orb-jobs-agent" -version = "0.0.0" +version = "0.0.1" authors = ["Paul Quinn "] description = "Worldcoin Jobs Agent" publish = false diff --git a/ota-backend/Cargo.toml b/ota-backend/Cargo.toml index 913f14fd2..f4e0323bf 100644 --- a/ota-backend/Cargo.toml +++ b/ota-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orb-ota-backend" -version = "0.0.0" +version = "0.0.1" description = "Internal tooling for OTAs" authors = ["Ryan Butler "] publish = false diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 280b3d4b7..561749c97 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,4 +2,8 @@ channel = "1.97.1" # See Cargo.toml components = ["clippy", "llvm-tools-preview", "rustfmt", "rust-src", "rust-analyzer"] profile = "minimal" -targets = ["aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"] +targets = [ + "aarch64-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "aarch64-linux-android", +] diff --git a/se050-reprovision/Cargo.toml b/se050-reprovision/Cargo.toml index 4463f4f4e..f25801123 100644 --- a/se050-reprovision/Cargo.toml +++ b/se050-reprovision/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orb-se050-reprovision" -version = "0.0.0" +version = "0.0.1" description = "Oneshot service that reprovisions the SE050 secure element" authors = ["Ryan Butler "] publish = false diff --git a/se050/Cargo.toml b/se050/Cargo.toml index 917733f6a..5ed69c64d 100644 --- a/se050/Cargo.toml +++ b/se050/Cargo.toml @@ -2,7 +2,7 @@ name = "orb-se050" description = "se050 utilities" authors = ["Ryan Butler "] -version = "0.0.0" +version = "0.0.1" publish = false edition.workspace = true diff --git a/seek-camera/sys/Cargo.toml b/seek-camera/sys/Cargo.toml index 31d749ae9..6d8e4be0e 100644 --- a/seek-camera/sys/Cargo.toml +++ b/seek-camera/sys/Cargo.toml @@ -18,4 +18,8 @@ color-eyre.workspace = true convert_case = "0.6" [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] diff --git a/seek-camera/wrapper/Cargo.toml b/seek-camera/wrapper/Cargo.toml index 904363903..cea894fc0 100644 --- a/seek-camera/wrapper/Cargo.toml +++ b/seek-camera/wrapper/Cargo.toml @@ -22,4 +22,8 @@ rusty-fork = "0.3" tempfile = "3.9" [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] diff --git a/slot-ctrl/Cargo.toml b/slot-ctrl/Cargo.toml index c54cac77a..2ab1fb673 100644 --- a/slot-ctrl/Cargo.toml +++ b/slot-ctrl/Cargo.toml @@ -52,4 +52,8 @@ assets = [ test = [] [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] diff --git a/speed-test/Cargo.toml b/speed-test/Cargo.toml index c9b019d77..a30beae9a 100644 --- a/speed-test/Cargo.toml +++ b/speed-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orb-speed-test" -version = "0.0.0" +version = "0.0.1" authors = ["Popov Philipp "] description = "Network speed test utility for Orb" publish = false diff --git a/thermal-cam-ctrl/Cargo.toml b/thermal-cam-ctrl/Cargo.toml index 0e28d016b..3338a67a3 100644 --- a/thermal-cam-ctrl/Cargo.toml +++ b/thermal-cam-ctrl/Cargo.toml @@ -32,7 +32,11 @@ zenorb.workspace = true orb-build-info = { workspace = true, features = ["build-script"] } [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] [package.metadata.deb] assets = [ diff --git a/tools/Cargo.toml b/tools/Cargo.toml index b27077eb8..770118113 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -21,3 +21,6 @@ tokio-util.workspace = true [build-dependencies] orb-build-info = { workspace = true, features = ["build-script"] } + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/ui/Cargo.toml b/ui/Cargo.toml index ad9c5ee0d..9411ad46b 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -67,7 +67,11 @@ assets = [ systemd-units = [{ unit-name = "worldcoin-ui" }] [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] [lints.rust.unexpected_cfgs] check-cfg = ['cfg(tokio_unstable)'] diff --git a/ui/cone/Cargo.toml b/ui/cone/Cargo.toml index b6e268106..071d18457 100644 --- a/ui/cone/Cargo.toml +++ b/ui/cone/Cargo.toml @@ -24,3 +24,6 @@ tinybmp = "0.5.0" tokio.workspace = true tracing.workspace = true tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/ui/sound/Cargo.toml b/ui/sound/Cargo.toml index b27fcc8c8..f4fcdb376 100644 --- a/ui/sound/Cargo.toml +++ b/ui/sound/Cargo.toml @@ -22,4 +22,8 @@ color-eyre.workspace = true tokio.workspace = true [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] diff --git a/ui/uart/Cargo.toml b/ui/uart/Cargo.toml index 721bb7fb7..535663ed0 100644 --- a/ui/uart/Cargo.toml +++ b/ui/uart/Cargo.toml @@ -15,3 +15,6 @@ rust-version.workspace = true [dependencies] libc = "0.2" log = "0.4" + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/update-agent-loader/Cargo.toml b/update-agent-loader/Cargo.toml index ca2d484e3..4ead9a39f 100644 --- a/update-agent-loader/Cargo.toml +++ b/update-agent-loader/Cargo.toml @@ -44,4 +44,8 @@ ed25519-dalek = "2.1.1" rand = "0.8.5" [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] diff --git a/update-agent/Cargo.toml b/update-agent/Cargo.toml index caebdfeac..60d2c7f97 100644 --- a/update-agent/Cargo.toml +++ b/update-agent/Cargo.toml @@ -87,7 +87,11 @@ toml = "0.8.10" orb-build-info = { workspace = true, features = ["build-script"] } [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] flavors = [{ name = "no-sig", features = ["skip-manifest-signature-verification"] }] [package.metadata.deb] diff --git a/update-agent/core/Cargo.toml b/update-agent/core/Cargo.toml index d6582d509..d826127fa 100644 --- a/update-agent/core/Cargo.toml +++ b/update-agent/core/Cargo.toml @@ -30,3 +30,6 @@ tap = "1.0.1" thiserror = { workspace = true, features = ["std"] } tracing.workspace = true url = { version = "2.2.2", features = ["serde"] } + +[package.metadata.orb] +unsupported_targets = ["aarch64-linux-android"] diff --git a/update-verifier/Cargo.toml b/update-verifier/Cargo.toml index 5085ce6b7..855ba2503 100644 --- a/update-verifier/Cargo.toml +++ b/update-verifier/Cargo.toml @@ -58,7 +58,11 @@ tempfile.workspace = true orb-dogd = { workspace = true, features = ["testing"] } [package.metadata.orb] -unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +unsupported_targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", +] [package.metadata.deb] maintainer-scripts = "debian/" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index c700bbee0..eb53c021c 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -20,5 +20,6 @@ derive_more = { workspace = true, features = ["display"] } orb-x-optee.path = "optee" serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +tempfile.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/xtask/src/cmd/android.rs b/xtask/src/cmd/android.rs new file mode 100644 index 000000000..096d5d84c --- /dev/null +++ b/xtask/src/cmd/android.rs @@ -0,0 +1,337 @@ +use crate::cmd::{args, cmd, cmd_captured}; +use cargo_metadata::{Metadata, MetadataCommand}; +use clap::Args as ClapArgs; +use color_eyre::{eyre::eyre, Result}; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +const TARGET: &str = "aarch64-linux-android"; + +/// Names of workspace packages whose `[package.metadata.orb] +/// unsupported_targets` lists `aarch64-linux-android` (same mechanism +/// `ci/rust_ci_helper.py` uses for its Darwin exclusions). +fn unsupported_packages(md: &Metadata) -> Result> { + let mut names: Vec = md + .workspace_packages() + .into_iter() + .filter(|pkg| { + pkg.metadata + .get("orb") + .and_then(|orb| orb.get("unsupported_targets")) + .and_then(|targets| targets.as_array()) + .is_some_and(|targets| { + targets.iter().any(|t| t.as_str() == Some(TARGET)) + }) + }) + .map(|pkg| pkg.name.as_str().to_owned()) + .collect(); + names.sort(); + + Ok(names) +} + +#[derive(ClapArgs, Debug)] +pub struct BuildArgs { + /// Build in release mode. + #[arg(long)] + pub release: bool, +} + +/// Builds the whole workspace for Android, skipping crates marked +/// unsupported via `[package.metadata.orb] unsupported_targets`. Meant for +/// CI: any failure among the non-excluded crates is a hard error. +pub fn run_build(args: BuildArgs) -> Result<()> { + let md = MetadataCommand::new().no_deps().exec()?; + run_build_with(&md, args) +} + +/// Like [`run_build`], but reuses metadata the caller already fetched +/// instead of shelling out to `cargo metadata` again. +fn run_build_with(md: &Metadata, args: BuildArgs) -> Result<()> { + let BuildArgs { release } = args; + let excludes = unsupported_packages(md)?; + + println!( + "skipping (unsupported on {TARGET}): {}", + excludes.join(", ") + ); + + let mut cmd_args = vec!["cargo", "build", "--workspace", "--target", TARGET]; + if release { + cmd_args.push("--release"); + } + for pkg in &excludes { + cmd_args.push("--exclude"); + cmd_args.push(pkg); + } + + cmd(&cmd_args) +} + +/// Stages one APEX payload directory per Android-supported binary crate: +/// `//content/{bin/,etc/init/.rc}` plus +/// sidecar `apex_manifest.json`, `canned_fs_config`, and `file_contexts` - +/// the inputs `nix/packages/android-apex.nix`'s `apexer` needs to produce a +/// signed `.apex`. +/// +/// The manifest name, `etc/init/*.rc` contents, and `file_contexts` are +/// TODO placeholders needing real naming and SELinux details. +fn run_payload(md: &Metadata, out_dir: &Path, release: bool) -> Result> { + // Rebuild first, so the binaries staged below aren't stale. + run_build_with(md, BuildArgs { release })?; + + let excluded = unsupported_packages(md)?; + let profile_dir = if release { "release" } else { "debug" }; + // Absolute, so this works regardless of the invoking cwd. + let target_dir = md.target_directory.as_std_path(); + + fs::create_dir_all(out_dir)?; + + let mut staged = Vec::new(); + + for pkg in md.workspace_packages() { + if excluded.iter().any(|e| e == pkg.name.as_str()) { + continue; + } + let binaries: Vec<&str> = pkg + .targets + .iter() + .filter(|t| t.is_bin()) + .map(|t| t.name.as_str()) + .collect(); + if binaries.is_empty() { + continue; + } + + // content/ must hold exactly the APEX's root filesystem; the + // sidecar manifest/canned_fs_config/file_contexts live outside it, + // or apexer's e2fsdroid mistakes them for payload files. + let pkg_out = out_dir.join(pkg.name.as_str()); + // Recreate from scratch, so a stale binary/init script from a prior + // run isn't bundled into an APEX whose canned_fs_config no longer + // lists it. + if pkg_out.exists() { + fs::remove_dir_all(&pkg_out)?; + } + let content_dir = pkg_out.join("content"); + let bin_out = content_dir.join("bin"); + fs::create_dir_all(&bin_out)?; + + // We author every path ourselves, so canned_fs_config is emitted + // directly instead of walking the tree afterward. uid/gid 1000 + // (`system`) is a placeholder, same status as TODO_USER/ + // TODO_SELINUX_DOMAIN below. + let mut fs_config = vec![ + "/ 1000 1000 0755".to_string(), + "/apex_manifest.pb 1000 1000 0644".to_string(), + "/bin 1000 1000 0755".to_string(), + ]; + + for bin in &binaries { + let src = target_dir.join(TARGET).join(profile_dir).join(bin); + let dst = bin_out.join(bin); + fs::copy(&src, &dst).map_err(|e| { + eyre!("failed to copy {} -> {}: {e}", src.display(), dst.display()) + })?; + fs_config.push(format!("/bin/{bin} 1000 1000 0755")); + } + + // TODO: "com.worldcoin.orb.*" is a placeholder - confirm the real + // naming scheme before shipping. `-` isn't valid in Android package + // names (Java identifiers joined by dots); aapt2 rejects it, so + // it's swapped for `_`. + let pkg_name = pkg + .name + .as_str() + .strip_prefix("orb-") + .unwrap_or(pkg.name.as_str()); + let apex_name = format!("com.worldcoin.orb.{}", pkg_name.replace('-', "_")); + + // apex_manifest's `version` must be a monotonically increasing + // int64, not the semver this crate is actually released under (see + // Cargo.toml), so encode that semver into one instead of a + // placeholder constant - assumes minor/patch stay under 1000, true + // for every crate version in this workspace today. Must be nonzero: + // apexer's own ValidateApexManifest rejects a literal 0 ("version + // field is required"), which is why every crate here carries a real + // Cargo.toml version instead of the 0.0.0 placeholder. The original + // string is kept as `versionName` for humans. + let version = &pkg.version; + let version_code = + version.major * 1_000_000 + version.minor * 1_000 + version.patch; + fs::write( + pkg_out.join("apex_manifest.json"), + format!( + "{{\n \"name\": \"{apex_name}\",\n \"version\": {version_code},\n \"versionName\": \"{version}\"\n}}\n" + ), + )?; + + let init_dir = content_dir.join("etc/init"); + fs::create_dir_all(&init_dir)?; + fs_config.push("/etc 1000 1000 0755".to_string()); + fs_config.push("/etc/init 1000 1000 0755".to_string()); + for bin in &binaries { + fs::write( + init_dir.join(format!("{bin}.rc")), + format!( + "# TODO: placeholder, not a working init script. Needs a real \ + SELinux domain (see external/sepolicy) plus a real decision on \ + class/user/group/oneshot before this can boot the daemon.\n\ + service {bin} /apex/{apex_name}/bin/{bin}\n \ + class TODO_CLASS\n user TODO_USER\n group TODO_GROUP\n \ + seclabel u:r:TODO_SELINUX_DOMAIN:s0\n", + bin = bin, + ), + )?; + fs_config.push(format!("/etc/init/{bin}.rc 1000 1000 0644")); + } + + fs_config.sort(); + fs::write( + pkg_out.join("canned_fs_config"), + fs_config.join("\n") + "\n", + )?; + + // TODO: placeholder SELinux context, matches no real sepolicy type + // yet. A single catch-all regex entry is valid file_contexts + // syntax, unlike the per-path listing in canned_fs_config. + fs::write( + pkg_out.join("file_contexts"), + "(/.*)? u:object_r:TODO_SELINUX_CONTEXT:s0\n", + )?; + + staged.push(pkg.name.as_str().to_owned()); + } + + Ok(staged) +} + +#[derive(ClapArgs, Debug)] +pub struct ApexArgs { + /// Directory to write the resulting `.apex` files into. + #[arg(long, default_value = "target/android-apex")] + pub out_dir: PathBuf, + /// Stage/build binaries in release mode. + #[arg(long)] + pub release: bool, +} + +/// Stages Android payloads and packages each into a signed +/// `/.apex`, using the `build-apex` tool from +/// `nix/packages/android-apex.nix` (exposed as the `build-apex` flake +/// package). Requires `nix` on PATH. +pub fn run_apex(args: ApexArgs) -> Result<()> { + if !cfg!(all(target_os = "linux", target_arch = "x86_64")) { + return Err(eyre!( + "android-apex packaging requires an x86_64-linux host: the \ + `build-apex` flake package (see nix/packages/android-apex.nix) \ + only ships a Linux/x86_64 prebuilt mkfs.erofs and is only \ + exposed for that system in nix/shells/flake-outputs.nix" + )); + } + + let ApexArgs { out_dir, release } = args; + + // Fetched once and threaded through run_build_with/run_payload/ + // unsupported_packages below, instead of each shelling out to `cargo + // metadata` again for data that can't change mid-run. + let md = MetadataCommand::new().no_deps().exec()?; + + // Absolute, so this works regardless of the invoking cwd. "build-apex" + // must match the flake output name in nix/shells/flake-outputs.nix + // (packages."x86_64-linux"."build-apex") - nothing checks the two + // stay in sync, so grep for "build-apex" in both places if renaming. + let flake_ref = format!("{}#build-apex", md.workspace_root); + + // A unique dir per invocation: `run_payload` tears down and recreates + // each package's subdirectory on every call, so a fixed shared path + // would race across concurrent `android-apex` invocations. + let payload_out_dir_handle = tempfile::tempdir()?; + let payload_out_dir = payload_out_dir_handle.path().to_path_buf(); + let packages = run_payload(&md, &payload_out_dir, release)?; + + // Recreate from scratch, so a stale `.apex` from a crate that's now + // unsupported/removed doesn't linger for CI to pick up. + if out_dir.exists() { + fs::remove_dir_all(&out_dir)?; + } + fs::create_dir_all(&out_dir)?; + + // A unique dir per invocation: `nix build --out-link` replaces + // whatever's at that path, so a fixed path would race across runs. + let build_apex_link_dir = tempfile::tempdir()?; + let build_apex_link = build_apex_link_dir.path().join("build-apex"); + cmd(&args![ + "nix", + "build", + "--extra-experimental-features", + "nix-command flakes", + &flake_ref, + "--out-link", + &build_apex_link, + ])?; + let build_apex_bin = build_apex_link.join("bin/build-apex"); + + // Keep going on failure, so one bad crate doesn't block every other + // APEX that's otherwise ready. One thread per package, uncapped: each + // build-apex invocation spends most of its time waiting on apexer/ + // aapt2/avbtool/mkfs.erofs subprocesses rather than burning CPU itself, + // so it's not purely core-bound - a few dozen threads is affordable. + let payload_out_dir = &payload_out_dir; + let out_dir = &out_dir; + let build_apex_bin = &build_apex_bin; + let outcomes = std::thread::scope(|scope| { + let handles: Vec<_> = packages + .iter() + .map(|pkg| { + scope.spawn(move || -> Result { + let payload_dir = payload_out_dir.join(pkg); + let apex_out = out_dir.join(format!("{pkg}.apex")); + let result = + cmd_captured(&args![build_apex_bin, &payload_dir, &apex_out]); + + match result { + Ok(output) => { + let mut out = std::io::stdout().lock(); + let _ = out.write_all(&output); + let _ = writeln!( + out, + "packaged `{pkg}` -> {}", + apex_out.display() + ); + Ok(pkg.clone()) + } + Err(e) => { + let mut err = std::io::stderr().lock(); + let _ = writeln!(err, "failed to package `{pkg}`: {e}"); + Err(pkg.clone()) + } + } + }) + }) + .collect(); + + handles + .into_iter() + .map(|handle| handle.join().unwrap()) + .collect::>() + }); + + let mut succeeded: Vec = + outcomes.iter().cloned().filter_map(Result::ok).collect(); + let mut failed: Vec = + outcomes.into_iter().filter_map(Result::err).collect(); + succeeded.sort(); + failed.sort(); + + println!("\n=== apex packaging summary ==="); + println!("succeeded ({}): {}", succeeded.len(), succeeded.join(", ")); + + if !failed.is_empty() { + println!("failed ({}): {}", failed.len(), failed.join(", ")); + return Err(eyre!("failed to package APEX for: {}", failed.join(", "))); + } + + Ok(()) +} diff --git a/xtask/src/cmd/build.rs b/xtask/src/cmd/build.rs index 1a0596d35..f75d58b45 100644 --- a/xtask/src/cmd/build.rs +++ b/xtask/src/cmd/build.rs @@ -1,4 +1,4 @@ -use crate::cmd::cmd; +use crate::cmd::{args, cmd}; use clap::Args as ClapArgs; use color_eyre::Result; @@ -12,14 +12,14 @@ pub struct Args { pub fn run(args: Args) -> Result<()> { let Args { pkg, target } = args; - cmd(&[ + cmd(&args![ "cargo", "zigbuild", "--target", - target.as_str(), + &target, "--release", "-p", - pkg.as_str(), + &pkg, ])?; Ok(()) diff --git a/xtask/src/cmd/deb.rs b/xtask/src/cmd/deb.rs index a514259a0..6c1b59e7f 100644 --- a/xtask/src/cmd/deb.rs +++ b/xtask/src/cmd/deb.rs @@ -1,5 +1,5 @@ use super::build; -use crate::cmd::cmd; +use crate::cmd::{args, cmd}; use clap::Args as ClapArgs; use color_eyre::Result; @@ -19,17 +19,17 @@ pub fn run(args: Args) -> Result<()> { })?; let path = format!("./target/deb/{pkg}.deb"); - cmd(&[ + cmd(&args![ "cargo", "deb", "--no-build", "--no-strip", "-p", - pkg.as_str(), + &pkg, "--target", - target.as_str(), + &target, "-o", - path.as_str(), + &path, ])?; println!("\n{pkg} successfully packaged at {path}"); diff --git a/xtask/src/cmd/deploy.rs b/xtask/src/cmd/deploy.rs index b72047a02..eef6662b7 100644 --- a/xtask/src/cmd/deploy.rs +++ b/xtask/src/cmd/deploy.rs @@ -1,6 +1,5 @@ use super::build; -use crate::cmd::cmd; -use crate::cmd::deb; +use crate::cmd::{args, cmd, deb}; use cargo_metadata::MetadataCommand; use clap::Args as ClapArgs; use color_eyre::{eyre::eyre, Result}; @@ -53,35 +52,35 @@ fn deploy_deb(pkg: String) -> Result<()> { let remote_deb = format!("./{pkg}.deb"); println!("\ncopying .deb file to orb"); - cmd(&[ + cmd(&args![ "sshpass", "-p", - worldcoin_pw.as_str(), + &worldcoin_pw, "scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", - deb_path.as_str(), - scp_target.as_str(), + &deb_path, + &scp_target, ])?; println!("installing .deb pkg on orb\n"); - cmd(&[ + cmd(&args![ "sshpass", "-p", - worldcoin_pw.as_str(), + &worldcoin_pw, "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", - host.as_str(), + &host, "sudo", "apt", "install", "--reinstall", - remote_deb.as_str(), + &remote_deb, "-y", ])?; @@ -103,14 +102,14 @@ fn deploy_bin(pkg: String) -> Result<()> { let bin = get_crate_binary_name(&pkg)?; - cmd(&[ + cmd(&args![ "cargo", "zigbuild", "--target", target, "--release", "-p", - pkg.as_str(), + &pkg, ])?; let bin_path = format!("./target/{target}/release/{bin}"); @@ -120,36 +119,36 @@ fn deploy_bin(pkg: String) -> Result<()> { let install_target = format!("/usr/local/bin/{bin}"); println!("\ncopying binary to orb"); - cmd(&[ + cmd(&args![ "sshpass", "-p", - worldcoin_pw.as_str(), + &worldcoin_pw, "scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", - bin_path.as_str(), - scp_target.as_str(), + &bin_path, + &scp_target, ])?; println!("installing binary to /usr/local/bin on orb\n"); - cmd(&[ + cmd(&args![ "sshpass", "-p", - worldcoin_pw.as_str(), + &worldcoin_pw, "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", - host.as_str(), + &host, "sudo", "install", "-m", "0755", - remote_bin.as_str(), - install_target.as_str(), + &remote_bin, + &install_target, ])?; restart_services(&worldcoin_pw, &host, services)?; @@ -164,7 +163,7 @@ fn restart_services( ) -> Result<()> { for service in services { println!("\nrestarting service {service} on orb\n"); - cmd(&[ + cmd(&args![ "sshpass", "-p", worldcoin_pw, @@ -177,7 +176,7 @@ fn restart_services( "sudo", "systemctl", "restart", - service.as_str(), + &service, ])?; } diff --git a/xtask/src/cmd/mod.rs b/xtask/src/cmd/mod.rs index 486702227..f9bd3a1fc 100644 --- a/xtask/src/cmd/mod.rs +++ b/xtask/src/cmd/mod.rs @@ -1,3 +1,4 @@ +pub mod android; pub mod build; pub mod deb; pub mod deploy; @@ -5,14 +6,31 @@ pub mod pre_commit; pub mod test; pub mod test_watch; +use std::ffi::OsStr; use std::process::{Command, Stdio}; use color_eyre::{eyre::eyre, Result}; -pub(crate) fn cmd(args: &[&str]) -> Result<()> { +/// Builds a `[&OsStr; N]` from a mix of `&str`/`&String`/`&Path`/`&PathBuf` +/// arguments - so a `cmd(&args![...])` call site can freely mix string +/// literals and paths in one argument list. +macro_rules! args { + ($($arg:expr),+ $(,)?) => { + [$(::std::convert::AsRef::<::std::ffi::OsStr>::as_ref($arg)),+] + }; +} +pub(crate) use args; + +fn new_command>(args: &[S]) -> Result<(&S, Command)> { let (program, rest) = args.split_first().ok_or_else(|| eyre!("empty cmd"))?; let mut command = Command::new(program); command.args(rest); + + Ok((program, command)) +} + +pub(crate) fn cmd>(args: &[S]) -> Result<()> { + let (program, mut command) = new_command(args)?; command .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) @@ -20,8 +38,28 @@ pub(crate) fn cmd(args: &[&str]) -> Result<()> { let status = command.status()?; if !status.success() { + let program = program.as_ref().to_string_lossy(); return Err(eyre!("{program} exited with {status}")); } Ok(()) } + +/// Like [`cmd`], but captures stdout/stderr instead of streaming them live - +/// for callers running several of these concurrently, where interleaved +/// output from independent processes would otherwise be unreadable. On +/// success, returns the captured output, stdout followed by stderr, as raw +/// bytes - the child's output isn't guaranteed to be valid UTF-8. +pub(crate) fn cmd_captured>(args: &[S]) -> Result> { + let (program, mut command) = new_command(args)?; + let mut output = command.output()?; + + if !output.status.success() { + let program = program.as_ref().to_string_lossy(); + return Err(eyre!("{program} exited with {}", output.status)); + } + + output.stdout.extend(output.stderr); + + Ok(output.stdout) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index f8839e0f7..0939ed460 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,6 +1,6 @@ use clap::{Parser, Subcommand}; use color_eyre::Result; -use x::cmd::{build, deb, deploy, pre_commit, test, test_watch}; +use x::cmd::{android, build, deb, deploy, pre_commit, test, test_watch}; #[derive(Parser, Debug)] pub struct Cli { @@ -13,6 +13,13 @@ enum Cmd { /// Build the select crate using `cargo zigbuild --release`. alias: 'b' #[command(alias = "b")] Build(build::Args), + /// Builds the whole workspace for `aarch64-linux-android`, skipping + /// crates marked unsupported via `[package.metadata.orb] + /// unsupported_targets`. + AndroidBuild(android::BuildArgs), + /// Build Android payloads and package each into a signed + /// `.apex` + AndroidApex(android::ApexArgs), /// Build the select crate using `cargo zigbuild --release`, then package it into a `.deb` using /// `cargo deb` Deb(deb::Args), @@ -40,6 +47,8 @@ fn main() -> Result<()> { match cmd { Cmd::Build(args) => build::run(args), + Cmd::AndroidBuild(args) => android::run_build(args), + Cmd::AndroidApex(args) => android::run_apex(args), Cmd::Deb(args) => deb::run(args), Cmd::PreCommit => pre_commit::run(), Cmd::Deploy(args) => deploy::run(args), diff --git a/zbus-proxies/cli/Cargo.toml b/zbus-proxies/cli/Cargo.toml index 9b9a0da8b..c56ebf5a0 100644 --- a/zbus-proxies/cli/Cargo.toml +++ b/zbus-proxies/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orb-zbus-proxies-cli" -version = "0.0.0" +version = "0.0.1" description = "CLI tool to generate zbus proxies for third party services" publish = false