diff --git a/.gitignore b/.gitignore index 3746265c..137fb13d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ examples/*/reports/ examples/*/test-results/ plugins/*/tests/reports/ plugins/*/tests/test-results/ +notes/ diff --git a/examples/android/tests/test-suite.yaml b/examples/android/tests/test-suite.yaml index 7b19a3a0..ea27eefe 100644 --- a/examples/android/tests/test-suite.yaml +++ b/examples/android/tests/test-suite.yaml @@ -138,26 +138,25 @@ processes: echo "Verifying emulator is ready..." - # Early failure detection: Check if emulator process exists - echo "Checking if emulator process started..." + # Early failure detection: Check if emulator appears in adb devices + echo "Checking if emulator is visible to adb..." initial_wait=30 elapsed=0 - emulator_process_found=false + emulator_detected=false while [ $elapsed -lt $initial_wait ]; do - if pgrep -f "emulator.*-avd" >/dev/null 2>&1; then - emulator_process_found=true - echo "✓ Emulator process detected" + if adb devices 2>/dev/null | grep -q "emulator-"; then + emulator_detected=true + echo "✓ Emulator detected by adb" break fi sleep 2 elapsed=$((elapsed + 2)) - echo " Waiting for emulator process... ${elapsed}s/${initial_wait}s" done - if [ "$emulator_process_found" = false ]; then + if [ "$emulator_detected" = false ]; then echo "" - echo "ERROR: Emulator process not found after ${initial_wait}s" >&2 + echo "ERROR: Emulator not visible to adb after ${initial_wait}s" >&2 echo "" echo "This usually means:" >&2 echo " 1. Device filtering removed all devices (check sync-avds logs)" >&2 @@ -165,7 +164,7 @@ processes: echo " 3. System images not available for selected device" >&2 echo "" echo "Check process-compose logs above for error details" >&2 - printf 'fail\nEmulator process never started - check filtering and device availability\n' > "$_step_dir/$_step.status" + printf 'fail\nEmulator not detected - check filtering and device availability\n' > "$_step_dir/$_step.status" exit 1 fi @@ -175,19 +174,18 @@ processes: max_wait=300 elapsed=0 while ! android.sh emulator ready 2>/dev/null; do - # Recheck that emulator process is still running - if ! pgrep -f "emulator.*-avd" >/dev/null 2>&1; then + # Recheck that emulator is still visible to adb (crash detection) + if ! adb devices 2>/dev/null | grep -q "emulator-"; then echo "" - echo "ERROR: Emulator process terminated unexpectedly" >&2 + echo "ERROR: Emulator no longer visible to adb (crashed or terminated)" >&2 printf 'fail\nEmulator process crashed during boot\n' > "$_step_dir/$_step.status" exit 1 fi sleep 3 elapsed=$((elapsed + 3)) - echo " Waiting for emulator... ${elapsed}s/${max_wait}s" if [ $elapsed -ge $max_wait ]; then - printf 'fail\nTimed out after %ds waiting for emulator to boot\n' "$max_wait" > "$_step_dir/$_step.status" + printf 'fail\nTimed out after 300s waiting for emulator to boot\n' > "$_step_dir/$_step.status" exit 1 fi done diff --git a/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/devices.lock b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/devices.lock new file mode 100644 index 00000000..f2c920fc --- /dev/null +++ b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/devices.lock @@ -0,0 +1,17 @@ +{ + "devices": [ + { + "name": "medium_phone_api36", + "api": 36, + "device": "medium_phone", + "tag": "google_apis" + }, + { + "name": "pixel_api21", + "api": 21, + "device": "pixel", + "tag": "google_apis" + } + ], + "checksum": "8df4d3393b61fbbb08e45cf8762f95c521316938e514527916e4fce88a849d57" +} diff --git a/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/max.json b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/max.json new file mode 100644 index 00000000..7ed1bd7d --- /dev/null +++ b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/max.json @@ -0,0 +1,6 @@ +{ + "name": "medium_phone_api36", + "api": 36, + "device": "medium_phone", + "tag": "google_apis" +} diff --git a/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/min.json b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/min.json new file mode 100644 index 00000000..8c3394cd --- /dev/null +++ b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.android/devices/min.json @@ -0,0 +1,6 @@ +{ + "name": "pixel_api21", + "api": 21, + "device": "pixel", + "tag": "google_apis" +} diff --git a/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/devices.lock b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/devices.lock new file mode 100644 index 00000000..ed4e6c1f --- /dev/null +++ b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/devices.lock @@ -0,0 +1,13 @@ +{ + "devices": [ + { + "name": "iPhone 17", + "runtime": "26.2" + }, + { + "name": "iPhone 13", + "runtime": "15.4" + } + ], + "checksum": "4d5276f203d7ad62860bfc067f76194df53be449d4aa8a3b2d069855ec1f3232" +} diff --git a/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/max.json b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/max.json new file mode 100644 index 00000000..0e76d698 --- /dev/null +++ b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/max.json @@ -0,0 +1,4 @@ +{ + "name": "iPhone 17", + "runtime": "26.2" +} diff --git a/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/min.json b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/min.json new file mode 100644 index 00000000..fba99bb5 --- /dev/null +++ b/examples/react-native/devbox.d/segment-integrations.mobile-devtools.ios/devices/min.json @@ -0,0 +1,4 @@ +{ + "name": "iPhone 13", + "runtime": "15.4" +} diff --git a/plugins/android/config/README.md b/plugins/android/config/README.md index bea29fc7..93a6b5bb 100644 --- a/plugins/android/config/README.md +++ b/plugins/android/config/README.md @@ -21,32 +21,44 @@ Example: } ``` -### `hash-overrides.json` (Optional) -Temporary workarounds for Android SDK hash mismatches caused by Google updating files on their servers. +### `android.lock` +Locked Android SDK configuration including optional hash overrides for temporary workarounds. -**Location:** `devbox.d/plugin-name/hash-overrides.json` -**Committed:** ✅ Yes (for reproducibility) -**Purpose:** Fix hash mismatches until nixpkgs is updated -**Auto-generated:** By `devbox run android:hash-fix` +**Location:** `devbox.d/plugin-name/android.lock` +**Committed:** ✅ Yes +**Purpose:** Lock SDK versions and fix hash mismatches +**Generated by:** `android.sh devices sync` Example: ```json { - "dl.google.com-android-repository-platform-tools_r37.0.0-darwin.zip": "8c4c926d0ca192376b2a04b0318484724319e67c" + "ANDROID_BUILD_TOOLS_VERSION": "36.1.0", + "ANDROID_CMDLINE_TOOLS_VERSION": "19.0", + "ANDROID_COMPILE_SDK": 36, + "ANDROID_TARGET_SDK": 36, + "ANDROID_SYSTEM_IMAGE_TAG": "google_apis", + "ANDROID_INCLUDE_NDK": false, + "ANDROID_NDK_VERSION": "27.0.12077973", + "ANDROID_INCLUDE_CMAKE": false, + "ANDROID_CMAKE_VERSION": "3.22.1", + "hash_overrides": { + "https://dl.google.com/android/repository/platform-tools_r37.0.0-darwin.zip": "8c4c926d0ca192376b2a04b0318484724319e67c" + } } ``` -**When to commit:** -- ✅ **Always commit** when auto-generated - ensures everyone on the team gets the fix -- 🗑️ **Remove when obsolete** - once nixpkgs is updated, the override is no longer needed -- ✓ **Safe to keep** - having stale overrides is harmless (they're just not used if nixpkgs already has the correct hash) - -**Why commit it:** -- **Reproducibility**: Everyone on the team uses the same fixed hash -- **CI/CD**: Automated builds get the fix automatically -- **Onboarding**: New team members don't hit the same error +**Hash overrides field (optional):** +- By default, this field is **not present** - uses upstream hashes from nixpkgs +- Only set via `android.sh hash update` when Google updates files without version changes +- Uses **SHA1 hex format** (40 characters) matching nixpkgs Android repo.json +- Temporary workaround until nixpkgs catches up +- Safe to remove once nixpkgs is updated -This prevents the scenario where one developer fixes a hash mismatch but others keep hitting the same error. +**When to commit:** +- ✅ **Always commit** - ensures reproducible builds for the team +- ✅ **After sync** - when SDK versions change +- ✅ **After hash fix** - when adding hash overrides +- 🗑️ **Can clear overrides** - via `android.sh hash clear` when no longer needed ## Hash Mismatch Issue @@ -59,15 +71,37 @@ error: hash mismatch in fixed-output derivation got: sha1-YYYYYYY ``` -**Automatic fix:** -The plugin automatically detects and fixes this during `devbox shell`. Just run `devbox shell` twice: -1. First run: Detects error + auto-fixes + saves to hash-overrides.json -2. Second run: Uses fixed hash + builds successfully +**Manual fix:** +When `devbox shell` fails with a hash mismatch: +1. Extract the URL from the error message +2. Download the file and compute its SHA1 hash +3. Add the override: `android.sh hash update ` +4. Commit the fix: `git add devbox.d/*/android.lock && git commit -m "fix(android): add hash override"` +5. Retry: `devbox shell` -**Then commit the file:** +**Example:** ```bash -git add devbox.d/*/hash-overrides.json -git commit -m "fix(android): add SDK hash override" +# Download file and compute SHA1 hash +curl -O https://dl.google.com/android/repository/platform-tools_r37.0.0-darwin.zip +shasum platform-tools_r37.0.0-darwin.zip # or sha1sum on Linux + +# Add override with SHA1 hex (40 characters) +android.sh hash update https://dl.google.com/android/repository/platform-tools_r37.0.0-darwin.zip 8c4c926d0ca192376b2a04b0318484724319e67c + +# Commit and retry +git add devbox.d/*/android.lock +git commit -m "fix(android): add hash override for platform-tools" +devbox shell ``` -See: [HASH_MISMATCH_ISSUE.md](../../../notes/HASH_MISMATCH_ISSUE.md) for full details. +**View current overrides:** +```bash +android.sh hash show +``` + +**Remove overrides (when nixpkgs is updated):** +```bash +android.sh hash clear +git add devbox.d/*/android.lock +git commit -m "fix(android): remove hash overrides (nixpkgs updated)" +``` diff --git a/plugins/android/config/hash-overrides.json b/plugins/android/config/hash-overrides.json new file mode 100644 index 00000000..2f029a14 --- /dev/null +++ b/plugins/android/config/hash-overrides.json @@ -0,0 +1,3 @@ +{ + "https://dl.google.com/android/repository/platform-tools_r37.0.0-darwin.zip": "094a1395683c509fd4d48667da0d8b5ef4d42b2abfcd29f2e8149e2f989357c7" +} diff --git a/plugins/android/config/hash-overrides.json.example b/plugins/android/config/hash-overrides.json.example new file mode 100644 index 00000000..93c1dbd7 --- /dev/null +++ b/plugins/android/config/hash-overrides.json.example @@ -0,0 +1,3 @@ +{ + "dl.google.com-android-repository-platform-tools_r37.0.0-darwin.zip": "8c4c926d0ca192376b2a04b0318484724319e67c" +} diff --git a/plugins/android/virtenv/flake.nix b/plugins/android/virtenv/flake.nix index 42244dc3..110b24cf 100644 --- a/plugins/android/virtenv/flake.nix +++ b/plugins/android/virtenv/flake.nix @@ -13,13 +13,13 @@ "aarch64-darwin" ]; - # Read generated android.json (created from env vars by android-init.sh) - # On first initialization, android.json may not exist yet, so provide defaults - configFileExists = builtins.pathExists ./android.json; - versionData = if configFileExists - then builtins.fromJSON (builtins.readFile ./android.json) + # Read android.lock (generated by android.sh devices sync) + # On first initialization, android.lock may not exist yet, so provide defaults + androidLockExists = builtins.pathExists ./android.lock; + androidLockData = if androidLockExists + then builtins.fromJSON (builtins.readFile ./android.lock) else { - # Default values for initial flake evaluation before android-init.sh runs + # Default values for initial flake evaluation before sync runs ANDROID_BUILD_TOOLS_VERSION = "36.1.0"; ANDROID_CMDLINE_TOOLS_VERSION = "19.0"; ANDROID_SYSTEM_IMAGE_TAG = "google_apis"; @@ -28,7 +28,7 @@ ANDROID_INCLUDE_CMAKE = false; ANDROID_CMAKE_VERSION = "3.22.1"; }; - defaultsData = if builtins.hasAttr "defaults" versionData then versionData.defaults else versionData; + defaultsData = androidLockData; getVar = name: if builtins.hasAttr name defaultsData then toString (builtins.getAttr name defaultsData) @@ -71,6 +71,13 @@ cmakeVersion = getVar "ANDROID_CMAKE_VERSION"; }; + # Hash overrides for when Google updates files on their servers + # These can be set in android.lock to work around nixpkgs hash mismatches + # By default this field is not set - only set via `android.sh hash update` when upstream is broken + hashOverrides = if builtins.hasAttr "hash_overrides" androidLockData + then androidLockData.hash_overrides + else {}; + forAllSystems = f: builtins.listToAttrs ( @@ -94,9 +101,28 @@ abiVersions = if builtins.match "aarch64-.*" system != null then [ "arm64-v8a" ] else [ "x86_64" ]; + # Apply hash overrides to nixpkgs if any are specified + # Android packages use SHA1 hashes, not SHA256 + # We need to re-import nixpkgs with overlays when overrides are present + pkgsWithOverrides = if (builtins.length (builtins.attrNames hashOverrides)) > 0 + then import nixpkgs { + inherit system; + config = { + allowUnfree = true; + android_sdk.accept_license = true; + }; + overlays = [(final: prev: { + fetchurl = args: + if builtins.isAttrs args && builtins.hasAttr "url" args && builtins.hasAttr args.url hashOverrides + then prev.fetchurl (args // { sha1 = hashOverrides.${args.url}; }) + else prev.fetchurl args; + })]; + } + else pkgs; + androidPkgs = config: - pkgs.androidenv.composeAndroidPackages { + pkgsWithOverrides.androidenv.composeAndroidPackages { platformVersions = config.platformVersions; buildToolsVersions = [ config.buildToolsVersion ]; cmdLineToolsVersion = config.cmdLineToolsVersion; diff --git a/plugins/android/virtenv/scripts/domain/avd.sh b/plugins/android/virtenv/scripts/domain/avd.sh index 1d29b2ce..ccc75458 100644 --- a/plugins/android/virtenv/scripts/domain/avd.sh +++ b/plugins/android/virtenv/scripts/domain/avd.sh @@ -391,7 +391,8 @@ android_setup_avds() { fi done - devices_json="$filtered_json" + # Strip trailing newline to prevent empty iterations + devices_json="${filtered_json%$'\n'}" if [ -z "$devices_json" ]; then echo "ERROR: No devices match ANDROID_DEVICES filter: ${ANDROID_DEVICES}" >&2 @@ -422,10 +423,33 @@ android_setup_avds() { # Get default system image tag default_image_tag="${ANDROID_SYSTEM_IMAGE_TAG:-google_apis}" + # Count devices to process + device_count=$(echo "$devices_json" | grep -c '{' || echo "0") + echo "Processing $device_count device(s) from lock file" + + # Debug: show devices_json content + if android_debug_enabled; then + echo "DEBUG: devices_json content:" >&2 + echo "$devices_json" | cat -A >&2 + fi + echo "$devices_json" | while IFS= read -r device_json; do + # Skip empty lines (defensive guard) + if [ -z "$device_json" ]; then + if android_debug_enabled; then + echo "DEBUG: Skipping empty device line" >&2 + fi + continue + fi + echo "" echo "Processing device from lock file..." + # Debug: show raw JSON being parsed + if android_debug_enabled; then + echo "DEBUG: Raw device JSON: $device_json" >&2 + fi + # Parse device definition from lock file device_name="$(echo "$device_json" | jq -r '.name // empty')" api_level="$(echo "$device_json" | jq -r '.api // empty')" diff --git a/plugins/android/virtenv/scripts/platform/core.sh b/plugins/android/virtenv/scripts/platform/core.sh index c9b61db5..558ec884 100644 --- a/plugins/android/virtenv/scripts/platform/core.sh +++ b/plugins/android/virtenv/scripts/platform/core.sh @@ -69,10 +69,13 @@ resolve_flake_sdk_root() { root="${ANDROID_SDK_FLAKE_PATH:-}" if [ -z "$root" ]; then - if [ -n "${ANDROID_RUNTIME_DIR:-}" ] && [ -d "${ANDROID_RUNTIME_DIR}" ]; then + # Flake is in the config directory (devbox.d/) where device configs live + if [ -n "${ANDROID_CONFIG_DIR:-}" ] && [ -d "${ANDROID_CONFIG_DIR}" ]; then + root="${ANDROID_CONFIG_DIR}" + elif [ -n "${ANDROID_RUNTIME_DIR:-}" ] && [ -d "${ANDROID_RUNTIME_DIR}" ]; then root="${ANDROID_RUNTIME_DIR}" elif [ -n "${ANDROID_SCRIPTS_DIR:-}" ] && [ -d "${ANDROID_SCRIPTS_DIR}" ]; then - # Flake is in same directory as scripts (virtenv) + # Fallback: flake in same directory as scripts (virtenv) - deprecated root="$(dirname "${ANDROID_SCRIPTS_DIR}")" elif [ -n "${DEVBOX_PROJECT_ROOT:-}" ] && [ -d "${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/android" ]; then root="${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/android" diff --git a/plugins/android/virtenv/scripts/user/android.sh b/plugins/android/virtenv/scripts/user/android.sh index fad5441a..30324f08 100755 --- a/plugins/android/virtenv/scripts/user/android.sh +++ b/plugins/android/virtenv/scripts/user/android.sh @@ -31,6 +31,7 @@ Usage: android.sh [args] Commands: deploy [apk_path] Install and launch app on running emulator devices [args] Manage device definitions + hash [args] Manage Nix hash overrides (SHA1) info Display resolved SDK information doctor Diagnose environment and check for SDK version mismatches config Manage configuration @@ -47,6 +48,10 @@ Examples: android.sh deploy path/to/app.apk android.sh devices list android.sh devices create pixel_api28 --api 28 --device pixel + android.sh devices sync + android.sh hash show + android.sh hash update https://dl.google.com/android/repository/platform-tools_r37.0.0-darwin.zip 8c4c926d0ca192376b2a04b0318484724319e67c + android.sh hash clear android.sh info android.sh doctor android.sh config show @@ -204,6 +209,18 @@ case "$command_name" in exec "$devices_script" "$@" ;; + # -------------------------------------------------------------------------- + # hash - Manage Nix hash overrides (proxy to devices.sh) + # -------------------------------------------------------------------------- + hash) + devices_script="${scripts_dir%/}/user/devices.sh" + if [ ! -x "$devices_script" ]; then + echo "ERROR: devices.sh not found or not executable: $devices_script" >&2 + exit 1 + fi + exec "$devices_script" hash "$@" + ;; + # -------------------------------------------------------------------------- # info - Display SDK information # -------------------------------------------------------------------------- diff --git a/plugins/android/virtenv/scripts/user/devices.sh b/plugins/android/virtenv/scripts/user/devices.sh index 9d406a64..e54976f2 100755 --- a/plugins/android/virtenv/scripts/user/devices.sh +++ b/plugins/android/virtenv/scripts/user/devices.sh @@ -38,7 +38,13 @@ Commands: update [options] Update existing device delete Remove device definition eval Generate devices.lock from ANDROID_DEVICES - sync Ensure AVDs match device definitions + sync Generate locks and sync AVDs + hash [args] Manage Nix hash overrides + +Hash Subcommands: + hash show Show current hash overrides in android.lock + hash update Add/update a hash override (SHA1 hex, 40 chars) + hash clear Remove all hash overrides from android.lock Device Creation Options: --api Android API level (required, e.g., 28, 34) @@ -53,11 +59,21 @@ Device Selection: Set ANDROID_DEVICES env var in devbox.json (comma-separated, empty = all): {"ANDROID_DEVICES": "min,max"} +Hash Overrides: + When Google updates files on their servers but nixpkgs hasn't caught up, + you may see hash mismatch errors. Use 'hash update' to add an override. + By default, hash overrides are not set - only use as a temporary fix. + + Hash format: SHA1 hex string (40 characters), e.g., 8c4c926d0ca192376b2a04b0318484724319e67c + Examples: devices.sh list devices.sh create pixel_api28 --api 28 --device pixel --tag google_apis devices.sh eval devices.sh sync + devices.sh hash show + devices.sh hash update https://dl.google.com/android/repository/platform-tools_r37.0.0-darwin.zip 8c4c926d0ca192376b2a04b0318484724319e67c + devices.sh hash clear USAGE exit 1 } @@ -230,33 +246,69 @@ fi # Generate android.lock from environment variables # Creates/updates android.lock with current Android SDK configuration from env vars +# Preserves hash_overrides field if it exists android_generate_android_lock() { local android_lock_file="${config_dir}/android.lock" local android_lock_tmp="${android_lock_file}.tmp" + # Preserve existing hash_overrides if present + local hash_overrides_json="{}" + if [ -f "$android_lock_file" ]; then + hash_overrides_json="$(jq -c '.hash_overrides // {}' "$android_lock_file")" + fi + # Extract relevant Android env vars and create lock file # Convert boolean env vars (accepts: true/1/yes/on, case-insensitive) - jq -n \ - --arg build_tools "${ANDROID_BUILD_TOOLS_VERSION:-36.1.0}" \ - --arg cmdline_tools "${ANDROID_CMDLINE_TOOLS_VERSION:-19.0}" \ - --arg compile_sdk "${ANDROID_COMPILE_SDK:-36}" \ - --arg target_sdk "${ANDROID_TARGET_SDK:-36}" \ - --arg system_image_tag "${ANDROID_SYSTEM_IMAGE_TAG:-google_apis}" \ - --arg include_ndk "${ANDROID_INCLUDE_NDK:-false}" \ - --arg ndk_version "${ANDROID_NDK_VERSION:-27.0.12077973}" \ - --arg include_cmake "${ANDROID_INCLUDE_CMAKE:-false}" \ - --arg cmake_version "${ANDROID_CMAKE_VERSION:-3.22.1}" \ - '{ - ANDROID_BUILD_TOOLS_VERSION: $build_tools, - ANDROID_CMDLINE_TOOLS_VERSION: $cmdline_tools, - ANDROID_COMPILE_SDK: ($compile_sdk | tonumber), - ANDROID_TARGET_SDK: ($target_sdk | tonumber), - ANDROID_SYSTEM_IMAGE_TAG: $system_image_tag, - ANDROID_INCLUDE_NDK: ($include_ndk | test("true|1|yes|on"; "i")), - ANDROID_NDK_VERSION: $ndk_version, - ANDROID_INCLUDE_CMAKE: ($include_cmake | test("true|1|yes|on"; "i")), - ANDROID_CMAKE_VERSION: $cmake_version - }' > "$android_lock_tmp" + # Only include hash_overrides field if it has content + if [ "$hash_overrides_json" = "{}" ]; then + # No hash overrides - standard lock file + jq -n \ + --arg build_tools "${ANDROID_BUILD_TOOLS_VERSION:-36.1.0}" \ + --arg cmdline_tools "${ANDROID_CMDLINE_TOOLS_VERSION:-19.0}" \ + --arg compile_sdk "${ANDROID_COMPILE_SDK:-36}" \ + --arg target_sdk "${ANDROID_TARGET_SDK:-36}" \ + --arg system_image_tag "${ANDROID_SYSTEM_IMAGE_TAG:-google_apis}" \ + --arg include_ndk "${ANDROID_INCLUDE_NDK:-false}" \ + --arg ndk_version "${ANDROID_NDK_VERSION:-27.0.12077973}" \ + --arg include_cmake "${ANDROID_INCLUDE_CMAKE:-false}" \ + --arg cmake_version "${ANDROID_CMAKE_VERSION:-3.22.1}" \ + '{ + ANDROID_BUILD_TOOLS_VERSION: $build_tools, + ANDROID_CMDLINE_TOOLS_VERSION: $cmdline_tools, + ANDROID_COMPILE_SDK: ($compile_sdk | tonumber), + ANDROID_TARGET_SDK: ($target_sdk | tonumber), + ANDROID_SYSTEM_IMAGE_TAG: $system_image_tag, + ANDROID_INCLUDE_NDK: ($include_ndk | test("true|1|yes|on"; "i")), + ANDROID_NDK_VERSION: $ndk_version, + ANDROID_INCLUDE_CMAKE: ($include_cmake | test("true|1|yes|on"; "i")), + ANDROID_CMAKE_VERSION: $cmake_version + }' > "$android_lock_tmp" + else + # Has hash overrides - include them + jq -n \ + --arg build_tools "${ANDROID_BUILD_TOOLS_VERSION:-36.1.0}" \ + --arg cmdline_tools "${ANDROID_CMDLINE_TOOLS_VERSION:-19.0}" \ + --arg compile_sdk "${ANDROID_COMPILE_SDK:-36}" \ + --arg target_sdk "${ANDROID_TARGET_SDK:-36}" \ + --arg system_image_tag "${ANDROID_SYSTEM_IMAGE_TAG:-google_apis}" \ + --arg include_ndk "${ANDROID_INCLUDE_NDK:-false}" \ + --arg ndk_version "${ANDROID_NDK_VERSION:-27.0.12077973}" \ + --arg include_cmake "${ANDROID_INCLUDE_CMAKE:-false}" \ + --arg cmake_version "${ANDROID_CMAKE_VERSION:-3.22.1}" \ + --argjson hash_overrides "$hash_overrides_json" \ + '{ + ANDROID_BUILD_TOOLS_VERSION: $build_tools, + ANDROID_CMDLINE_TOOLS_VERSION: $cmdline_tools, + ANDROID_COMPILE_SDK: ($compile_sdk | tonumber), + ANDROID_TARGET_SDK: ($target_sdk | tonumber), + ANDROID_SYSTEM_IMAGE_TAG: $system_image_tag, + ANDROID_INCLUDE_NDK: ($include_ndk | test("true|1|yes|on"; "i")), + ANDROID_NDK_VERSION: $ndk_version, + ANDROID_INCLUDE_CMAKE: ($include_cmake | test("true|1|yes|on"; "i")), + ANDROID_CMAKE_VERSION: $cmake_version, + hash_overrides: $hash_overrides + }' > "$android_lock_tmp" + fi mv "$android_lock_tmp" "$android_lock_file" echo "✓ Generated android.lock" @@ -735,6 +787,104 @@ case "$command_name" in android_sync_avds || exit 1 ;; + # -------------------------------------------------------------------------- + # hash - Manage Nix hash overrides in android.lock + # -------------------------------------------------------------------------- + hash) + subcommand="${1-}" + [ -n "$subcommand" ] || usage + shift || true + + android_lock_file="${config_dir}/android.lock" + + case "$subcommand" in + show) + # Display current hash overrides + if [ ! -f "$android_lock_file" ]; then + echo "No android.lock file found" + exit 0 + fi + + if ! jq -e '.hash_overrides' "$android_lock_file" >/dev/null 2>&1; then + echo "No hash overrides set" + exit 0 + fi + + override_count=$(jq '.hash_overrides | length' "$android_lock_file") + if [ "$override_count" -eq 0 ]; then + echo "No hash overrides set" + exit 0 + fi + + echo "Hash overrides in android.lock:" + jq -r '.hash_overrides | to_entries[] | " \(.key): \(.value)"' "$android_lock_file" + ;; + + update) + # Add or update a hash override + url="${1-}" + new_hash="${2-}" + + if [ -z "$url" ] || [ -z "$new_hash" ]; then + echo "ERROR: Both URL and hash are required" >&2 + echo "Usage: devices.sh hash update " >&2 + exit 1 + fi + + # Ensure android.lock exists + if [ ! -f "$android_lock_file" ]; then + echo "ERROR: android.lock not found. Run 'devices.sh sync' first." >&2 + exit 1 + fi + + # Update hash override + temp_lock="${android_lock_file}.tmp" + jq --arg url "$url" --arg hash "$new_hash" \ + '.hash_overrides = (.hash_overrides // {}) | .hash_overrides[$url] = $hash' \ + "$android_lock_file" > "$temp_lock" + + mv "$temp_lock" "$android_lock_file" + echo "✓ Added hash override for: $url" + echo " Hash: $new_hash" + echo "" + echo "IMPORTANT: Commit android.lock to preserve this fix:" + echo " git add devbox.d/*/android.lock" + echo " git commit -m 'fix(android): add hash override for $(basename "$url")'" + ;; + + clear) + # Remove all hash overrides + if [ ! -f "$android_lock_file" ]; then + echo "No android.lock file found" + exit 0 + fi + + if ! jq -e '.hash_overrides' "$android_lock_file" >/dev/null 2>&1; then + echo "No hash overrides to clear" + exit 0 + fi + + override_count=$(jq '.hash_overrides | length' "$android_lock_file") + if [ "$override_count" -eq 0 ]; then + echo "No hash overrides to clear" + exit 0 + fi + + # Remove hash_overrides field + temp_lock="${android_lock_file}.tmp" + jq 'del(.hash_overrides)' "$android_lock_file" > "$temp_lock" + mv "$temp_lock" "$android_lock_file" + + echo "✓ Cleared $override_count hash override(s) from android.lock" + ;; + + *) + echo "ERROR: Unknown hash subcommand: $subcommand" >&2 + usage + ;; + esac + ;; + # -------------------------------------------------------------------------- # Unknown command # -------------------------------------------------------------------------- diff --git a/plugins/android/virtenv/scripts/user/doctor.sh b/plugins/android/virtenv/scripts/user/doctor.sh index eca85df7..1b31b524 100644 --- a/plugins/android/virtenv/scripts/user/doctor.sh +++ b/plugins/android/virtenv/scripts/user/doctor.sh @@ -102,3 +102,64 @@ else echo " ⚠ Cannot check drift (drift detection not available)" fi echo '' + +# Check 6: Hash overrides validation +echo 'Hash Overrides:' +if [ -f "$android_lock" ] && command -v jq >/dev/null 2>&1; then + if jq -e '.hash_overrides' "$android_lock" >/dev/null 2>&1; then + override_count=$(jq '.hash_overrides | length' "$android_lock") + if [ "$override_count" -gt 0 ]; then + echo " ⚠ $override_count hash override(s) active" + jq -r '.hash_overrides | to_entries[] | " - \(.key | split("/") | last)"' "$android_lock" + + # Test if overrides are still needed + echo " Testing override validity..." + + # Create temporary android.lock without overrides + temp_lock=$(mktemp) + jq 'del(.hash_overrides)' "$android_lock" > "$temp_lock" + + # Try building SDK without overrides (quick check, no full build) + if [ -n "${ANDROID_SCRIPTS_DIR:-}" ] && [ -f "${ANDROID_SCRIPTS_DIR}/platform/core.sh" ]; then + # Source core to get SDK resolution function + . "${ANDROID_SCRIPTS_DIR}/platform/core.sh" 2>/dev/null || true + + # Temporarily swap lock file + mv "$android_lock" "${android_lock}.backup" + mv "$temp_lock" "$android_lock" + + # Try resolving SDK (this will fail fast if hash mismatch) + test_output=$(android_resolve_sdk_root 2>&1 || true) + + # Restore original lock file + mv "$android_lock" "$temp_lock" + mv "${android_lock}.backup" "$android_lock" + + # Check result + if echo "$test_output" | grep -q "hash mismatch"; then + echo " ✓ Overrides are still needed (upstream not fixed)" + elif echo "$test_output" | grep -qE "^/nix/store/"; then + echo " ⚠ Overrides may no longer be needed!" + echo " Test: android.sh hash clear && devbox shell" + echo " If successful, commit the fix" + else + echo " ? Cannot validate overrides (SDK resolution failed for other reasons)" + fi + + rm -f "$temp_lock" + else + echo " ? Cannot validate (core.sh not available)" + fi + + echo " View: android.sh hash show" + echo " Clear: android.sh hash clear" + else + echo " ✓ No hash overrides (using upstream hashes)" + fi + else + echo " ✓ No hash overrides (using upstream hashes)" + fi +else + echo " ⚠ Cannot check (android.lock not found or jq unavailable)" +fi +echo '' diff --git a/plugins/android/virtenv/scripts/user/setup.sh b/plugins/android/virtenv/scripts/user/setup.sh index 5e54e713..4115d8bc 100755 --- a/plugins/android/virtenv/scripts/user/setup.sh +++ b/plugins/android/virtenv/scripts/user/setup.sh @@ -96,9 +96,59 @@ if [ -n "${ANDROID_RUNTIME_DIR:-}" ]; then echo "${ANDROID_SDK_ROOT}" > "${ANDROID_RUNTIME_DIR}/.state/sdk_root" fi -echo "✅ [OK] Android setup complete" +# Verify essential tools are in PATH +if ! command -v adb >/dev/null 2>&1; then + echo "⚠️ [WARN] adb not in PATH" >&2 +fi -# Run lightweight doctor check -if [ -n "${ANDROID_SCRIPTS_DIR:-}" ] && [ -f "${ANDROID_SCRIPTS_DIR}/init/doctor.sh" ]; then - bash "${ANDROID_SCRIPTS_DIR}/init/doctor.sh" 2>&1 +if ! command -v emulator >/dev/null 2>&1; then + echo "⚠️ [WARN] emulator not in PATH" >&2 fi + +# Check for configuration drift (android.lock out of sync with env vars) +config_dir="${ANDROID_CONFIG_DIR:-./devbox.d/android}" +android_lock="${config_dir}/android.lock" + +if [ -f "$android_lock" ] && command -v jq >/dev/null 2>&1; then + drift_detected=false + drift_messages="" + + # Compare each env var with android.lock + for var in ANDROID_BUILD_TOOLS_VERSION ANDROID_CMDLINE_TOOLS_VERSION ANDROID_COMPILE_SDK ANDROID_TARGET_SDK ANDROID_SYSTEM_IMAGE_TAG ANDROID_INCLUDE_NDK ANDROID_NDK_VERSION ANDROID_INCLUDE_CMAKE ANDROID_CMAKE_VERSION; do + env_val="${!var:-}" + lock_val="$(jq -r ".${var} // empty" "$android_lock" 2>/dev/null || echo "")" + + # Normalize boolean values for comparison + if [ "$var" = "ANDROID_INCLUDE_NDK" ] || [ "$var" = "ANDROID_INCLUDE_CMAKE" ]; then + case "$env_val" in + 1|true|TRUE|yes|YES|on|ON) env_val="true" ;; + *) env_val="false" ;; + esac + fi + + # Skip if lock value is empty (field doesn't exist in lock) + [ -z "$lock_val" ] && continue + + if [ "$env_val" != "$lock_val" ]; then + drift_detected=true + drift_messages="${drift_messages} ${var}: \"${env_val}\" (env) vs \"${lock_val}\" (lock)\n" + fi + done + + if [ "$drift_detected" = true ]; then + echo "" >&2 + echo "⚠️ WARNING: Android configuration has changed but lock file is outdated." >&2 + echo "" >&2 + echo "Environment variables don't match android.lock:" >&2 + printf "$drift_messages" >&2 + echo "" >&2 + echo "To apply changes:" >&2 + echo " devbox run android:sync" >&2 + echo "" >&2 + echo "To revert changes:" >&2 + echo " Edit devbox.json to match the lock file" >&2 + echo "" >&2 + fi +fi + +echo "✅ [OK] Android setup complete" diff --git a/plugins/tests/android/test-device-filtering.sh b/plugins/tests/android/test-device-filtering.sh new file mode 100755 index 00000000..5d30a5bc --- /dev/null +++ b/plugins/tests/android/test-device-filtering.sh @@ -0,0 +1,420 @@ +#!/usr/bin/env bash +# Android Plugin - Device Filtering Tests +# Tests device filtering logic and fixes for trailing newline bug + +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging + +echo "========================================" +echo "Android Device Filtering Tests" +echo "========================================" +echo "" + +# Setup test environment +test_root="$(make_temp_dir "android-device-filtering")" +mkdir -p "$test_root/devices" +mkdir -p "$test_root/scripts/lib" +mkdir -p "$test_root/scripts/platform" +mkdir -p "$test_root/scripts/domain" +mkdir -p "$test_root/scripts/user" +mkdir -p "$test_root/avd" + +# Copy required scripts +cp "$script_dir/../../android/virtenv/scripts/lib/lib.sh" "$test_root/scripts/lib/" +cp "$script_dir/../../android/virtenv/scripts/platform/core.sh" "$test_root/scripts/platform/" +cp "$script_dir/../../android/virtenv/scripts/platform/device_config.sh" "$test_root/scripts/platform/" +cp "$script_dir/../../android/virtenv/scripts/domain/avd.sh" "$test_root/scripts/domain/" +cp "$script_dir/../../android/virtenv/scripts/user/devices.sh" "$test_root/scripts/user/" + +# Set environment variables +export ANDROID_CONFIG_DIR="$test_root" +export ANDROID_DEVICES_DIR="$test_root/devices" +export ANDROID_SCRIPTS_DIR="$test_root/scripts" +export ANDROID_AVD_HOME="$test_root/avd" +export ANDROID_DEFAULT_DEVICE="" +export ANDROID_SYSTEM_IMAGE_TAG="google_apis" + +devices_script="$test_root/scripts/user/devices.sh" +avd_script="$test_root/scripts/domain/avd.sh" + +# ============================================================================ +# Test Setup: Create Test Devices +# ============================================================================ + +echo "SETUP: Creating test devices" +"$devices_script" create min --api 24 --device pixel --tag google_apis >/dev/null 2>&1 +"$devices_script" create max --api 36 --device medium_phone --tag google_apis >/dev/null 2>&1 +"$devices_script" create mid --api 30 --device pixel_5 --tag google_apis >/dev/null 2>&1 + +assert_success "[ -f '$test_root/devices/min.json' ]" "min.json created" +assert_success "[ -f '$test_root/devices/max.json' ]" "max.json created" +assert_success "[ -f '$test_root/devices/mid.json' ]" "mid.json created" + +# Generate lock file with all devices +export ANDROID_DEVICES="" +"$devices_script" eval >/dev/null 2>&1 +assert_success "[ -f '$test_root/devices/devices.lock' ]" "devices.lock created" + +# ============================================================================ +# Test 1: Single Device Filter (Bug Case) +# ============================================================================ + +start_test "Single device filter - max only" + +export ANDROID_DEVICES="max" +lock_file="$test_root/devices/devices.lock" + +# Source avd.sh functions to test filtering logic +( + set -e + . "$test_root/scripts/lib/lib.sh" + . "$test_root/scripts/platform/core.sh" + + # Extract the filtering logic + devices_json="$(jq -c '.devices[]' "$lock_file" 2>/dev/null || echo "")" + + if [ -n "${ANDROID_DEVICES:-}" ]; then + IFS=',' read -ra selected_devices <<< "${ANDROID_DEVICES}" + + filtered_json="" + for device_json in $devices_json; do + device_filename="$(echo "$device_json" | jq -r '.filename // empty')" + + should_include=false + for selected in "${selected_devices[@]}"; do + if [ "$device_filename" = "$selected" ]; then + should_include=true + break + fi + done + + if [ "$should_include" = true ]; then + filtered_json="${filtered_json}${device_json}"$'\n' + fi + done + + # Strip trailing newline (THE FIX) + devices_json="${filtered_json%$'\n'}" + fi + + # Count lines (should be 1, not 2) + line_count=$(echo "$devices_json" | wc -l | tr -d ' ') + + if [ "$line_count" -ne 1 ]; then + echo "ERROR: Expected 1 line, got $line_count" >&2 + echo "Content:" >&2 + echo "$devices_json" | cat -A >&2 + exit 1 + fi + + # Count non-empty lines + non_empty_count=$(echo "$devices_json" | grep -c '{' || echo "0") + + if [ "$non_empty_count" -ne 1 ]; then + echo "ERROR: Expected 1 device JSON, got $non_empty_count" >&2 + exit 1 + fi + + # Verify no trailing newline + if [[ "$devices_json" == *$'\n' ]]; then + echo "ERROR: devices_json has trailing newline" >&2 + exit 1 + fi + + # Process each device (simulate the while loop) + device_processed=0 + empty_lines=0 + + echo "$devices_json" | while IFS= read -r device_json; do + # Skip empty lines (defensive guard) + if [ -z "$device_json" ]; then + empty_lines=$((empty_lines + 1)) + continue + fi + + device_processed=$((device_processed + 1)) + + # Parse fields + api_level="$(echo "$device_json" | jq -r '.api // empty')" + device_hardware="$(echo "$device_json" | jq -r '.device // empty')" + + if [ -z "$api_level" ] || [ -z "$device_hardware" ]; then + echo "ERROR: Device definition missing required fields" >&2 + exit 1 + fi + done + + echo "Processed 1 device successfully" +) + +if [ $? -eq 0 ]; then + echo " ✓ PASS: Single device filter works without empty lines" + test_passed=$((test_passed + 1)) +else + echo " ✗ FAIL: Single device filter produced empty lines or invalid data" + test_failed=$((test_failed + 1)) +fi + +# ============================================================================ +# Test 2: Multiple Device Filter +# ============================================================================ + +start_test "Multiple device filter - min,max" + +export ANDROID_DEVICES="min,max" + +( + set -e + . "$test_root/scripts/lib/lib.sh" + . "$test_root/scripts/platform/core.sh" + + devices_json="$(jq -c '.devices[]' "$lock_file" 2>/dev/null || echo "")" + + IFS=',' read -ra selected_devices <<< "${ANDROID_DEVICES}" + + filtered_json="" + for device_json in $devices_json; do + device_filename="$(echo "$device_json" | jq -r '.filename // empty')" + + should_include=false + for selected in "${selected_devices[@]}"; do + if [ "$device_filename" = "$selected" ]; then + should_include=true + break + fi + done + + if [ "$should_include" = true ]; then + filtered_json="${filtered_json}${device_json}"$'\n' + fi + done + + # Strip trailing newline + devices_json="${filtered_json%$'\n'}" + + # Count devices + device_count=$(echo "$devices_json" | grep -c '{') + + if [ "$device_count" -ne 2 ]; then + echo "ERROR: Expected 2 devices, got $device_count" >&2 + exit 1 + fi + + # Verify no trailing newline + if [[ "$devices_json" == *$'\n' ]]; then + echo "ERROR: devices_json has trailing newline" >&2 + exit 1 + fi + + echo "Filtered 2 devices successfully" +) + +if [ $? -eq 0 ]; then + echo " ✓ PASS: Multiple device filter works correctly" + test_passed=$((test_passed + 1)) +else + echo " ✗ FAIL: Multiple device filter failed" + test_failed=$((test_failed + 1)) +fi + +# ============================================================================ +# Test 3: Empty Filter (All Devices) +# ============================================================================ + +start_test "Empty filter - all devices" + +export ANDROID_DEVICES="" + +( + set -e + . "$test_root/scripts/lib/lib.sh" + . "$test_root/scripts/platform/core.sh" + + devices_json="$(jq -c '.devices[]' "$lock_file" 2>/dev/null || echo "")" + + # No filtering when ANDROID_DEVICES is empty + + device_count=$(echo "$devices_json" | grep -c '{') + + if [ "$device_count" -ne 3 ]; then + echo "ERROR: Expected 3 devices (all), got $device_count" >&2 + exit 1 + fi + + echo "All 3 devices available" +) + +if [ $? -eq 0 ]; then + echo " ✓ PASS: Empty filter returns all devices" + test_passed=$((test_passed + 1)) +else + echo " ✗ FAIL: Empty filter failed" + test_failed=$((test_failed + 1)) +fi + +# ============================================================================ +# Test 4: Invalid Filter (No Matches) +# ============================================================================ + +start_test "Invalid filter - nonexistent device" + +export ANDROID_DEVICES="nonexistent" + +( + set -e + . "$test_root/scripts/lib/lib.sh" + . "$test_root/scripts/platform/core.sh" + + devices_json="$(jq -c '.devices[]' "$lock_file" 2>/dev/null || echo "")" + + IFS=',' read -ra selected_devices <<< "${ANDROID_DEVICES}" + + filtered_json="" + for device_json in $devices_json; do + device_filename="$(echo "$device_json" | jq -r '.filename // empty')" + + should_include=false + for selected in "${selected_devices[@]}"; do + if [ "$device_filename" = "$selected" ]; then + should_include=true + break + fi + done + + if [ "$should_include" = true ]; then + filtered_json="${filtered_json}${device_json}"$'\n' + fi + done + + # Strip trailing newline + devices_json="${filtered_json%$'\n'}" + + # Should be empty + if [ -n "$devices_json" ]; then + echo "ERROR: Expected empty result, got: $devices_json" >&2 + exit 1 + fi + + echo "No matches (as expected)" +) + +if [ $? -eq 0 ]; then + echo " ✓ PASS: Invalid filter returns empty result" + test_passed=$((test_passed + 1)) +else + echo " ✗ FAIL: Invalid filter handling failed" + test_failed=$((test_failed + 1)) +fi + +# ============================================================================ +# Test 5: Device Count Logging +# ============================================================================ + +start_test "Device count logging" + +export ANDROID_DEVICES="max" + +output=$( + . "$test_root/scripts/lib/lib.sh" + . "$test_root/scripts/platform/core.sh" + + devices_json="$(jq -c '.devices[]' "$lock_file" 2>/dev/null || echo "")" + + IFS=',' read -ra selected_devices <<< "${ANDROID_DEVICES}" + + filtered_json="" + for device_json in $devices_json; do + device_filename="$(echo "$device_json" | jq -r '.filename // empty')" + + should_include=false + for selected in "${selected_devices[@]}"; do + if [ "$device_filename" = "$selected" ]; then + should_include=true + break + fi + done + + if [ "$should_include" = true ]; then + filtered_json="${filtered_json}${device_json}"$'\n' + fi + done + + devices_json="${filtered_json%$'\n'}" + + # Simulate device count logging (from fix #4) + device_count=$(echo "$devices_json" | grep -c '{' || echo "0") + echo "Processing $device_count device(s) from lock file" +) + +if echo "$output" | grep -q "Processing 1 device(s) from lock file"; then + echo " ✓ PASS: Device count logging works" + test_passed=$((test_passed + 1)) +else + echo " ✗ FAIL: Device count logging incorrect" + echo " Output: $output" + test_failed=$((test_failed + 1)) +fi + +# ============================================================================ +# Test 6: Empty Line Guard and Debug Logging +# ============================================================================ + +start_test "Empty line guard prevents processing empty lines" + +# Test the empty line guard by checking that a string with trailing newline +# doesn't cause errors when processed +result=$( + devices_json='{"filename":"max","name":"test","api":36,"device":"pixel"}'$'\n' + + error_count=0 + processed_count=0 + + # Simulate the processing loop with empty line guard + while IFS= read -r device_json; do + # Skip empty lines (THE FIX) + if [ -z "$device_json" ]; then + continue + fi + + # Try to parse - this would fail on empty string + api_level="$(echo "$device_json" | jq -r '.api // empty' 2>/dev/null)" + device_hardware="$(echo "$device_json" | jq -r '.device // empty' 2>/dev/null)" + + if [ -z "$api_level" ] || [ -z "$device_hardware" ]; then + error_count=$((error_count + 1)) + else + processed_count=$((processed_count + 1)) + fi + done <<< "$devices_json" + + # Should process 1 device with 0 errors + if [ "$processed_count" -eq 1 ] && [ "$error_count" -eq 0 ]; then + echo "SUCCESS" + else + echo "FAIL: processed=$processed_count errors=$error_count" + fi +) + +if [ "$result" = "SUCCESS" ]; then + echo " ✓ PASS: Empty line guard prevents processing empty lines" + test_passed=$((test_passed + 1)) +else + echo " ✗ FAIL: Empty line guard not working" + echo " Result: $result" + test_failed=$((test_failed + 1)) +fi + +# ============================================================================ +# Cleanup +# ============================================================================ + +rm -rf "$test_root" + +# ============================================================================ +# Test Summary +# ============================================================================ + +test_summary "android-device-filtering"