diff --git a/examples/cross_compilation/android_app/BUILD.bazel b/examples/cross_compilation/android_app/BUILD.bazel index 7077dfe35..8b46e53c8 100644 --- a/examples/cross_compilation/android_app/BUILD.bazel +++ b/examples/cross_compilation/android_app/BUILD.bazel @@ -11,11 +11,35 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_android//rules:rules.bzl", "android_binary") load("@rules_cc//cc:defs.bzl", "cc_library") load("@rules_kotlin//kotlin:android.bzl", "kt_android_library") +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("//swift:swift.bzl", "swift_binary", "swift_library") load("//swift/toolchains:android_runtime_lib.bzl", "select_android_runtime_lib") package(default_visibility = ["//visibility:public"]) +# `bazel run //examples/cross_compilation/android_app:run` installs the APK on a +# connected device/emulator and launches it, booting a hermetic emulator first +# if nothing is connected (downloaded emulator + system image; adb from +# @androidsdk). macOS/arm64 only — see emulator.bzl. +sh_binary( + name = "run", + srcs = ["run.sh"], + args = [ + "$(rlocationpath :app.apk)", + "$(rlocationpath @androidsdk//:adb)", + "$(rlocationpath @android_emulator//:emulator/emulator)", + "$(rlocationpath @android_system_image//:system.img)", + ], + data = [ + ":app.apk", + "@android_emulator//:all", + "@android_emulator//:emulator/emulator", + "@android_system_image//:all", + "@android_system_image//:system.img", + "@androidsdk//:adb", + ], +) + android_binary( name = "app", manifest = "AndroidManifest.xml", diff --git a/examples/cross_compilation/android_app/README.md b/examples/cross_compilation/android_app/README.md index 60b69e6e9..ea1034223 100644 --- a/examples/cross_compilation/android_app/README.md +++ b/examples/cross_compilation/android_app/README.md @@ -22,6 +22,17 @@ adb install -r bazel-bin/examples/cross_compilation/android_app/app.apk adb shell am start -n com.example.swiftjni/.MainActivity ``` +Or, with nothing preinstalled (no Android Studio, no device), let Bazel boot a +hermetic emulator, install, and launch — macOS/arm64: + +```sh +bazel run //examples/cross_compilation/android_app:run +``` + +This downloads the emulator + an AOSP system image (see `emulator.bzl`) and uses +adb from `@androidsdk`; if a device/emulator is already connected it just installs +and launches on that. + The screen shows **“Hello from Swift, Android!”** — computed by the `Greeter` `swift_library`, returned through the `@_cdecl` JNI entry point in `libSwiftJNI.so`, and displayed by Kotlin. (`//.bazelrc` sets diff --git a/examples/cross_compilation/android_app/android.MODULE.bazel b/examples/cross_compilation/android_app/android.MODULE.bazel index a860cb7ea..013bb77ee 100644 --- a/examples/cross_compilation/android_app/android.MODULE.bazel +++ b/examples/cross_compilation/android_app/android.MODULE.bazel @@ -66,3 +66,12 @@ register_toolchains( "@androidsdk//:all", dev_dependency = True, ) + +# A hermetic emulator + system image so `bazel run …:run` can launch the example +# with nothing preinstalled (adb comes from @androidsdk). +emulator = use_extension( + "//examples/cross_compilation/android_app:emulator.bzl", + "emulator", + dev_dependency = True, +) +use_repo(emulator, "android_emulator", "android_system_image") diff --git a/examples/cross_compilation/android_app/emulator.bzl b/examples/cross_compilation/android_app/emulator.bzl new file mode 100644 index 000000000..15cd1ae66 --- /dev/null +++ b/examples/cross_compilation/android_app/emulator.bzl @@ -0,0 +1,45 @@ +"""A dev module extension that downloads a hermetic Android emulator and a +system image, so `bazel run //examples/cross_compilation/android_app:run` can +boot an emulator and launch the example with nothing preinstalled. + +macOS/arm64 only, to keep this illustrative delta small (it's the platform the +example's CI runs on). adb comes from `@androidsdk`; only the emulator and the +system image — which `@androidsdk` does not provide — are downloaded here. +""" + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +_EMULATOR_BUILD = """\ +package(default_visibility = ["//visibility:public"]) + +exports_files(["emulator/emulator"]) + +filegroup(name = "all", srcs = glob(["emulator/**"])) +""" + +_SYSTEM_IMAGE_BUILD = """\ +package(default_visibility = ["//visibility:public"]) + +exports_files(["system.img"]) + +filegroup(name = "all", srcs = glob(["**"])) +""" + +def _emulator_impl(_module_ctx): + http_archive( + name = "android_emulator", + build_file_content = _EMULATOR_BUILD, + sha256 = "25378c67fd5bd03178e3a478b866496da8545e969df3a7a26ce9167772ffc026", + url = "https://dl.google.com/android/repository/emulator-darwin_aarch64-15565763.zip", + ) + + # A regular GPU-enabled AOSP (default) image, not the headless ATD image. + http_archive( + name = "android_system_image", + build_file_content = _SYSTEM_IMAGE_BUILD, + sha256 = "1447958a4c6747c44390ac5f5f4c894be6d1dfce93868a0385a95c5f0ae4c339", + strip_prefix = "arm64-v8a", + url = "https://dl.google.com/android/repository/sys-img/android/arm64-v8a-34_r04.zip", + ) + +emulator = module_extension(implementation = _emulator_impl) diff --git a/examples/cross_compilation/android_app/run.sh b/examples/cross_compilation/android_app/run.sh new file mode 100755 index 000000000..6422ce938 --- /dev/null +++ b/examples/cross_compilation/android_app/run.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# `bazel run //examples/cross_compilation/android_app:run`: installs the example +# APK on a connected device/emulator and launches it. If nothing is connected, +# boots the hermetic emulator (downloaded emulator + AOSP system image) first. +set -euo pipefail + +RUNFILES_ROOT="$PWD/.." +APK="$RUNFILES_ROOT/$1" +ADB="$RUNFILES_ROOT/$2" +EMULATOR_BIN="$RUNFILES_ROOT/$3" +SYSIMG_MARKER="$RUNFILES_ROOT/$4" + +has_device() { + "$ADB" devices | awk 'NR>1 && $2 == "device"' | grep -q . +} + +"$ADB" start-server >/dev/null 2>&1 || true + +if ! has_device; then + echo "No device connected; booting the hermetic emulator..." + EMULATOR_DIR="$(cd "$(dirname "$EMULATOR_BIN")" && pwd)" + SYSIMG_DIR="$(cd "$(dirname "$SYSIMG_MARKER")" && pwd)" + + STATE="${TMPDIR:-/tmp}/rules_swift_android_example_emulator" + SDK="$STATE/sdk" + AVD_HOME="$STATE/avd" + AVD_DIR="$AVD_HOME/example.avd" + mkdir -p "$SDK/system-images/android-34/default" "$AVD_DIR" "$SDK/platform-tools" + + ln -sfn "$EMULATOR_DIR" "$SDK/emulator" + ln -sfn "$SYSIMG_DIR" "$SDK/system-images/android-34/default/arm64-v8a" + ln -sfn "$ADB" "$SDK/platform-tools/adb" + + cat > "$AVD_HOME/example.ini" < "$AVD_DIR/config.ini" <"$EMULATOR_LOG" 2>&1 & + echo " (emulator log: $EMULATOR_LOG)" + + "$ADB" wait-for-device + for _ in $(seq 1 120); do + # Wait for boot *and* the package manager (install races a "Broken pipe" + # otherwise, right after sys.boot_completed flips). + if [[ "$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]] && + "$ADB" shell pm path android >/dev/null 2>&1; then + break + fi + sleep 2 + done + has_device || { echo "error: emulator failed to boot; see $EMULATOR_LOG" >&2; exit 1; } + echo "Emulator booted." +fi + +"$ADB" install -r "$APK" +"$ADB" shell am start -n "com.example.swiftjni/.MainActivity" +echo "Launched com.example.swiftjni/.MainActivity"