{ description = "NilsMG4 — Android Automotive build environment"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; outputs = { self, nixpkgs }: let systems = [ "x86_64-linux" "aarch64-linux" ]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (import nixpkgs { inherit system; config = { allowUnfree = true; android_sdk.accept_license = true; }; })); # Must match app/build.gradle (compileSdk 36.1) and EVHardware/lib (compileSdk 36). buildToolsVersion = "36.0.0"; in { devShells = forAllSystems (pkgs: let android = pkgs.androidenv.composeAndroidPackages { platformVersions = [ "36" "36.1" ]; buildToolsVersions = [ buildToolsVersion ]; includeEmulator = false; includeNDK = false; includeSources = false; includeSystemImages = false; }; sdk = "${android.androidsdk}/libexec/android-sdk"; # gradle/gradle-daemon-jvm.properties pins the daemon to JDK 21. jdk = pkgs.jdk21; buildTools = "${sdk}/build-tools/${buildToolsVersion}"; # AGP otherwise downloads an aapt2 binary from Maven, which is dynamically linked # against a generic FHS and does not run on NixOS. Use the SDK's patched one. # Gradle is also pointed at the Nix JDK so the daemon toolchain criteria are met # locally instead of downloading a (non-NixOS) JDK through foojay. gradleOpts = builtins.concatStringsSep " " [ "-Dorg.gradle.project.android.aapt2FromMavenOverride=${buildTools}/aapt2" "-Dorg.gradle.java.installations.paths=${jdk.home}" "-Dorg.gradle.java.installations.auto-download=false" ]; # The MG4 head unit is signed with the public AOSP test platform key, so that is # the key a system-UID (unstable) build must carry. See tools/README.md and OTA.md. aospKeys = "https://raw.githubusercontent.com/aosp-mirror/platform_build/android-9.0.0_r1/target/product/security"; platformCertSha256 = "c8a2e9bccf597c2fb6dc66bee293fc13f2fc47ec77bc6b2b0d52c11f51192ab8"; buildCar = pkgs.writeShellApplication { name = "build-car"; runtimeInputs = [ jdk pkgs.curl pkgs.openssl pkgs.coreutils pkgs.gnugrep pkgs.gawk ]; text = '' root=$(git rev-parse --show-toplevel 2>/dev/null || pwd) cd "$root" if [ ! -f app/build.gradle ]; then echo "build-car: run this from the NilsMG4 checkout" >&2 exit 1 fi export ANDROID_HOME=${sdk} ANDROID_SDK_ROOT=${sdk} JAVA_HOME=${jdk.home} export GRADLE_OPTS="${gradleOpts}" echo "sdk.dir=$ANDROID_HOME" > local.properties # Keys live in tools/, which .gitignore keeps out of the repository. for f in platform.pk8 platform.x509.pem; do if [ ! -f "tools/$f" ]; then echo "Fetching AOSP test key tools/$f" curl -sSfL -o "tools/$f" "${aospKeys}/$f" fi done cert=$(openssl x509 -in tools/platform.x509.pem -outform der | sha256sum | cut -d' ' -f1) if [ "$cert" != "${platformCertSha256}" ]; then echo "build-car: tools/platform.x509.pem has SHA-256 $cert, expected ${platformCertSha256}" >&2 exit 1 fi ./gradlew assembleUnstableDebug "$@" version=$(./gradlew -q printUnstableVersion "$@" | tail -1) in=app/build/outputs/apk/unstable/debug/NilsMG4-unstable-debug.apk mkdir -p build/car aligned=build/car/aligned.apk out="build/car/NilsMG4-unstable-$version.apk" "${buildTools}/zipalign" -f -p 4 "$in" "$aligned" # Options come before the input APK; apksigner rejects the other order. "${buildTools}/apksigner" sign \ --key tools/platform.pk8 --cert tools/platform.x509.pem \ --out "$out" "$aligned" rm -f "$aligned" "$out.idsig" signer=$("${buildTools}/apksigner" verify --print-certs "$out" \ | awk -F': ' '/Signer #1 certificate SHA-256 digest/ { print $2 }') if [ "$signer" != "${platformCertSha256}" ]; then echo "build-car: $out is signed with $signer, expected ${platformCertSha256}" >&2 rm -f "$out" exit 1 fi sha256sum "$out" | cut -d' ' -f1 > "$out.sha256" echo echo "Signed with the AOSP platform key: $out" echo "Install on the head unit:" echo " adb push $out /data/local/tmp/update.apk" echo " adb shell \"pm install -r -f -d -t /data/local/tmp/update.apk\"" ''; }; # Publishes the current car build as a pre-release on the forge, where the car # server relays it to the in-app updater (server/releases.go, OTA.md). Needs a forge # token with write access to the repository's releases, from $FORGE_TOKEN or # ~/.config/nilsmg4/forge-token; it is sent in a header and never printed. publishCar = pkgs.writeShellApplication { name = "publish-car"; runtimeInputs = [ pkgs.curl pkgs.jq pkgs.coreutils pkgs.git ]; text = '' forge=''${FORGE_URL:-https://git.stinnesbeck.com} repo=''${FORGE_REPO:-nils/mg4} token=''${FORGE_TOKEN:-} if [ -z "$token" ] && [ -f "$HOME/.config/nilsmg4/forge-token" ]; then token=$(tr -d '[:space:]' < "$HOME/.config/nilsmg4/forge-token") fi if [ -z "$token" ]; then echo "publish-car: no token. Put one in ~/.config/nilsmg4/forge-token (or \$FORGE_TOKEN)." >&2 exit 1 fi root=$(git rev-parse --show-toplevel) cd "$root" version=$(grep -m1 'versionName "' app/build.gradle | sed 's/.*versionName "\(.*\)".*/\1/') apk="build/car/NilsMG4-unstable-$version.apk" if [ ! -f "$apk" ] || [ ! -f "$apk.sha256" ]; then echo "publish-car: $apk (and .sha256) not found; run build-car first" >&2 exit 1 fi if [ "$(sha256sum "$apk" | cut -d' ' -f1)" != "$(cat "$apk.sha256")" ]; then echo "publish-car: $apk does not match its .sha256" >&2 exit 1 fi tag="car-$version" commit=$(git rev-parse HEAD) api="$forge/api/v1/repos/$repo" auth=(-H "Authorization: token $token") if curl -sSf "''${auth[@]}" "$api/releases/tags/$tag" >/dev/null 2>&1; then echo "publish-car: $tag already exists; bump the version before publishing again" >&2 exit 1 fi body=$(jq -n --arg tag "$tag" --arg commit "$commit" --arg name "Car build $version" \ '{tag_name: $tag, target_commitish: $commit, name: $name, prerelease: true, body: "Platform-signed car build, installed by the in-app updater."}') id=$(curl -sSf "''${auth[@]}" -H "Content-Type: application/json" -d "$body" \ "$api/releases" | jq -r .id) for f in "$apk" "$apk.sha256"; do curl -sSf "''${auth[@]}" -F "attachment=@$f" \ "$api/releases/$id/assets?name=$(basename "$f")" >/dev/null done echo "Published $tag ($(basename "$apk") and .sha256) at $forge/$repo/releases/tag/$tag" ''; }; in { default = pkgs.mkShell { packages = [ jdk android.androidsdk buildCar publishCar pkgs.go pkgs.gopls pkgs.govulncheck pkgs.influxdb2-server pkgs.influxdb2-cli ]; ANDROID_HOME = sdk; ANDROID_SDK_ROOT = sdk; JAVA_HOME = jdk.home; GRADLE_OPTS = gradleOpts; shellHook = '' echo "sdk.dir=$ANDROID_HOME" > local.properties echo "NilsMG4 dev shell — JDK $(java -version 2>&1 | head -1 | cut -d'"' -f2), SDK at $ANDROID_HOME" echo " ./gradlew assembleStableDebug # debug APK" echo " ./gradlew assembleUnstableDebug # build that can update itself (OTA)" echo " build-car # unstable build signed for the car" echo " publish-car # publish it for the in-app updater" ''; }; }); # An Android 9 emulator shaped like the head unit, for testing the car build: the AOSP # "default" image is signed with the same public platform key, so the car APK installs # there as the system user too. 1920x720 at 160 dpi; `adb shell wm size 1778x640` gives # the app window the car leaves. No vehicle data: every value shows "—". Not in the dev # shell, because the emulator and its image are a large download. # nix run .#car-emulator (from the checkout; state in build/emulator/) packages.x86_64-linux.car-emulator = let pkgs = import nixpkgs { system = "x86_64-linux"; config = { allowUnfree = true; android_sdk.accept_license = true; }; }; emu = (pkgs.androidenv.composeAndroidPackages { platformVersions = [ "28" ]; includeEmulator = true; includeSystemImages = true; systemImageTypes = [ "default" ]; abiVersions = [ "x86_64" ]; includeNDK = false; }).androidsdk; in pkgs.writeShellApplication { name = "car-emulator"; runtimeInputs = [ pkgs.coreutils pkgs.gnugrep ]; text = '' root=$(git rev-parse --show-toplevel 2>/dev/null || pwd) state="$root/build/emulator" mkdir -p "$state/avd" "$state/home" export ANDROID_HOME=${emu}/libexec/android-sdk ANDROID_SDK_ROOT=${emu}/libexec/android-sdk export ANDROID_AVD_HOME="$state/avd" ANDROID_EMULATOR_HOME="$state/home" ANDROID_USER_HOME="$state/home" avd="$state/avd/mg4test.avd" if [ ! -d "$avd" ]; then echo no | ${emu}/bin/avdmanager create avd -n mg4test \ -k "system-images;android-28;default;x86_64" --force -p "$avd" { echo "hw.lcd.width=1920" echo "hw.lcd.height=720" echo "hw.lcd.density=160" echo "hw.ramSize=2G" echo "disk.dataPartition.size=10G" } >> "$avd/config.ini" fi echo "Starting the MG4-shaped emulator on port 5560 (adb: emulator-5560); Ctrl-C stops it." echo "adb: ${emu}/bin/adb -s emulator-5560" exec "$ANDROID_HOME/emulator/emulator" -avd mg4test -no-audio -no-boot-anim \ -gpu swiftshader_indirect -no-snapshot -port 5560 "$@" ''; }; apps.x86_64-linux.car-emulator = { type = "app"; program = "${self.packages.x86_64-linux.car-emulator}/bin/car-emulator"; }; }; }