From 20be46422d90c944bbce75af77a0737b9aa7dfce Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Tue, 21 Apr 2026 12:04:14 -0500 Subject: [PATCH] feat(android,ios): Add automatic doctor checks on shell init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lightweight health checks that run automatically on shell init for both Android and iOS environments. Shows simple ✓ if everything is good, or detailed warnings if issues are detected. Changes: - doctor.sh scripts for comprehensive diagnostics - init/doctor.sh for lightweight shell init checks - Drift detection for Android config mismatches - Silent output if healthy, verbose if issues found - Integrated into setup.sh for automatic checks Example output (healthy): ✅ [OK] Android setup complete ✓ Android Example output (with issues): ⚠️ Android issues detected: - Config drift: env vars don't match android.lock Fix: devbox run android:sync Run 'devbox run doctor' for more details Co-Authored-By: Claude Sonnet 4.5 --- plugins/android/plugin.json | 33 ++---- .../android/virtenv/scripts/init/doctor.sh | 57 ++++++++++ .../android/virtenv/scripts/platform/drift.sh | 79 +++++++++++++ .../android/virtenv/scripts/user/doctor.sh | 104 ++++++++++++++++++ plugins/android/virtenv/scripts/user/setup.sh | 12 +- plugins/ios/plugin.json | 20 ++-- plugins/ios/virtenv/scripts/init/doctor.sh | 43 ++++++++ plugins/ios/virtenv/scripts/user/setup.sh | 5 + 8 files changed, 314 insertions(+), 39 deletions(-) create mode 100644 plugins/android/virtenv/scripts/init/doctor.sh create mode 100644 plugins/android/virtenv/scripts/platform/drift.sh create mode 100644 plugins/android/virtenv/scripts/user/doctor.sh create mode 100644 plugins/ios/virtenv/scripts/init/doctor.sh diff --git a/plugins/android/plugin.json b/plugins/android/plugin.json index 70d2010a..25550687 100644 --- a/plugins/android/plugin.json +++ b/plugins/android/plugin.json @@ -44,18 +44,22 @@ "{{ .Virtenv }}/scripts/lib/lib.sh": "virtenv/scripts/lib/lib.sh", "{{ .Virtenv }}/scripts/platform/core.sh": "virtenv/scripts/platform/core.sh", "{{ .Virtenv }}/scripts/platform/device_config.sh": "virtenv/scripts/platform/device_config.sh", + "{{ .Virtenv }}/scripts/platform/drift.sh": "virtenv/scripts/platform/drift.sh", "{{ .Virtenv }}/scripts/domain/avd.sh": "virtenv/scripts/domain/avd.sh", "{{ .Virtenv }}/scripts/domain/avd-reset.sh": "virtenv/scripts/domain/avd-reset.sh", "{{ .Virtenv }}/scripts/domain/emulator.sh": "virtenv/scripts/domain/emulator.sh", "{{ .Virtenv }}/scripts/domain/deploy.sh": "virtenv/scripts/domain/deploy.sh", "{{ .Virtenv }}/scripts/domain/validate.sh": "virtenv/scripts/domain/validate.sh", + "{{ .Virtenv }}/scripts/domain/hash-fix.sh": "virtenv/scripts/domain/hash-fix.sh", "{{ .Virtenv }}/scripts/user/android.sh": "virtenv/scripts/user/android.sh", "{{ .Virtenv }}/scripts/user/config.sh": "virtenv/scripts/user/config.sh", "{{ .Virtenv }}/scripts/user/devices.sh": "virtenv/scripts/user/devices.sh", "{{ .Virtenv }}/scripts/user/setup.sh": "virtenv/scripts/user/setup.sh", + "{{ .Virtenv }}/scripts/user/doctor.sh": "virtenv/scripts/user/doctor.sh", "{{ .Virtenv }}/scripts/init/init-hook.sh": "virtenv/scripts/init/init-hook.sh", "{{ .Virtenv }}/scripts/init/setup.sh": "virtenv/scripts/init/setup.sh", - "{{ .Virtenv }}/flake.nix": "virtenv/flake.nix", + "{{ .Virtenv }}/scripts/init/doctor.sh": "virtenv/scripts/init/doctor.sh", + "{{ .DevboxDir }}/flake.nix": "virtenv/flake.nix", "{{ .DevboxDir }}/devices/min.json": "config/devices/min.json", "{{ .DevboxDir }}/devices/max.json": "config/devices/max.json" }, @@ -68,9 +72,15 @@ "setup": [ "bash {{ .Virtenv }}/scripts/user/setup.sh" ], + "android:sync": [ + "android.sh devices sync" + ], "android:devices:eval": [ "ANDROID_SDK_REQUIRED=0 android.sh devices eval" ], + "android:hash-fix": [ + "ANDROID_HASH_FIX_VERBOSE=1 bash {{ .Virtenv }}/scripts/domain/hash-fix.sh auto" + ], "start:emu": [ "android.sh emulator start \"${1:-}\"" ], @@ -81,26 +91,7 @@ "android.sh emulator reset" ], "doctor": [ - "echo 'Android Environment Check'", - "echo '========================='", - "echo ''", - "echo 'ANDROID_SDK_ROOT:' ${ANDROID_SDK_ROOT:-'NOT SET'}", - "test -n \"${ANDROID_SDK_ROOT}\" && echo '✓ ANDROID_SDK_ROOT is set' || echo '✗ ANDROID_SDK_ROOT is not set'", - "test -d \"${ANDROID_SDK_ROOT}\" && echo '✓ ANDROID_SDK_ROOT directory exists' || echo '✗ ANDROID_SDK_ROOT directory does not exist'", - "echo ''", - "echo 'ANDROID_AVD_HOME:' ${ANDROID_AVD_HOME:-'NOT SET'}", - "test -w \"${ANDROID_AVD_HOME}\" && echo '✓ ANDROID_AVD_HOME is writable' || echo '✗ ANDROID_AVD_HOME is not writable'", - "echo ''", - "command -v adb >/dev/null 2>&1 && echo '✓ adb is in PATH' || echo '✗ adb is not in PATH'", - "command -v emulator >/dev/null 2>&1 && echo '✓ emulator is in PATH' || echo '✗ emulator is not in PATH'", - "command -v avdmanager >/dev/null 2>&1 && echo '✓ avdmanager is in PATH' || echo '✗ avdmanager is not in PATH'", - "echo ''", - "echo 'Device Definitions:'", - "ls -1 ${ANDROID_DEVICES_DIR}/*.json 2>/dev/null | wc -l | xargs echo ' Count:'", - "echo ' ANDROID_DEVICES:' ${ANDROID_DEVICES:-'(all devices)'}", - "echo ''", - "test -f ${ANDROID_DEVICES_DIR}/devices.lock && echo '✓ Lock file exists' || echo '⚠ Lock file not generated yet (run devbox shell)'", - "echo ''" + "bash {{ .Virtenv }}/scripts/user/doctor.sh" ], "verify:setup": [ "test -n \"${ANDROID_SDK_ROOT}\" && test -d \"${ANDROID_SDK_ROOT}\" && command -v adb >/dev/null 2>&1 && echo '✓ Android environment OK' || (echo '✗ Android environment check failed. Run: devbox run doctor' && exit 1)" diff --git a/plugins/android/virtenv/scripts/init/doctor.sh b/plugins/android/virtenv/scripts/init/doctor.sh new file mode 100644 index 00000000..e11f49ef --- /dev/null +++ b/plugins/android/virtenv/scripts/init/doctor.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Android Plugin - Doctor Init Check +# Lightweight health check run on shell init +# Shows ✓ if all good, warnings if issues detected + +set -eu + +# Source drift detection if available +if [ -n "${ANDROID_SCRIPTS_DIR:-}" ] && [ -f "${ANDROID_SCRIPTS_DIR}/platform/drift.sh" ]; then + . "${ANDROID_SCRIPTS_DIR}/platform/drift.sh" +fi + +# Silent mode - only output if there are issues +issues=() + +# Check 1: SDK Root +if [ -z "${ANDROID_SDK_ROOT:-}" ] || [ ! -d "${ANDROID_SDK_ROOT:-}" ]; then + issues+=("Android SDK not found") +fi + +# Check 2: Essential tools +if [ -n "${ANDROID_SDK_ROOT:-}" ]; then + if ! command -v adb >/dev/null 2>&1; then + issues+=("adb not in PATH") + fi + if ! command -v emulator >/dev/null 2>&1; then + issues+=("emulator not in PATH") + fi +fi + +# Check 3: Configuration drift (android.lock out of sync with env vars) +if command -v android_check_config_drift >/dev/null 2>&1; then + android_check_config_drift + if [ "${ANDROID_DRIFT_DETECTED:-false}" = true ]; then + issues+=("Config drift: env vars don't match android.lock") + fi +fi + +# Output results +if [ ${#issues[@]} -eq 0 ]; then + echo "✓ Android" +else + echo "⚠️ Android issues detected:" >&2 + for issue in "${issues[@]}"; do + echo " - $issue" >&2 + done + + # If drift detected, show details + if [ "${ANDROID_DRIFT_DETECTED:-false}" = true ]; then + echo "" >&2 + echo " Config differences:" >&2 + printf '%s' "${ANDROID_DRIFT_DETAILS}" >&2 + echo " Fix: devbox run android:sync" >&2 + fi + + echo " Run 'devbox run doctor' for more details" >&2 +fi diff --git a/plugins/android/virtenv/scripts/platform/drift.sh b/plugins/android/virtenv/scripts/platform/drift.sh new file mode 100644 index 00000000..ddb21869 --- /dev/null +++ b/plugins/android/virtenv/scripts/platform/drift.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Android Plugin - Configuration Drift Detection +# Detects when environment variables don't match android.lock + +# Android configuration variables to check for drift +readonly ANDROID_CONFIG_VARS=( + "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" +) + +# Normalize boolean value (consistent with jq test("true|1|yes|on"; "i")) +# Accepts: true/1/yes/on (case-insensitive) +android_normalize_bool() { + local val="$1" + # Convert to lowercase for case-insensitive comparison + case "${val,,}" in + 1|true|yes|on) echo "true" ;; + *) echo "false" ;; + esac +} + +# android_check_config_drift +# Compares Android env vars with android.lock and detects drift +# Sets global variables: +# ANDROID_DRIFT_DETECTED - "true" if drift detected, "false" if no drift, "unknown" if cannot check +# ANDROID_DRIFT_DETAILS - formatted string with drift details (for printf %s) +android_check_config_drift() { + local config_dir="${ANDROID_CONFIG_DIR:-./devbox.d/android}" + local android_lock="${config_dir}/android.lock" + + ANDROID_DRIFT_DETECTED="false" + ANDROID_DRIFT_DETAILS="" + + # Check if lock file exists + if [ ! -f "$android_lock" ]; then + return 0 + fi + + # Check if jq is available + if ! command -v jq >/dev/null 2>&1; then + ANDROID_DRIFT_DETECTED="unknown" + ANDROID_DRIFT_DETAILS=" jq not available, cannot check configuration drift\n" + export ANDROID_DRIFT_DETECTED + export ANDROID_DRIFT_DETAILS + return 0 + fi + + # Compare each env var with android.lock + for var in "${ANDROID_CONFIG_VARS[@]}"; do + local env_val="${!var:-}" + local lock_val + 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 + env_val=$(android_normalize_bool "$env_val") + # lock_val is already normalized in android.lock (true/false) + fi + + # Skip if lock value is empty (field doesn't exist in lock) + [ -z "$lock_val" ] && continue + + # Detect drift + if [ "$env_val" != "$lock_val" ]; then + ANDROID_DRIFT_DETECTED="true" + ANDROID_DRIFT_DETAILS="${ANDROID_DRIFT_DETAILS} ${var}: \"${env_val}\" (env) vs \"${lock_val}\" (lock)\n" + fi + done + + export ANDROID_DRIFT_DETECTED + export ANDROID_DRIFT_DETAILS +} diff --git a/plugins/android/virtenv/scripts/user/doctor.sh b/plugins/android/virtenv/scripts/user/doctor.sh new file mode 100644 index 00000000..eca85df7 --- /dev/null +++ b/plugins/android/virtenv/scripts/user/doctor.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Android Plugin - Doctor Script +# Comprehensive health check for Android environment + +set -eu + +# Source drift detection if available +if [ -n "${ANDROID_SCRIPTS_DIR:-}" ] && [ -f "${ANDROID_SCRIPTS_DIR}/platform/drift.sh" ]; then + . "${ANDROID_SCRIPTS_DIR}/platform/drift.sh" +fi + +echo 'Android Environment Check' +echo '=========================' +echo '' + +# Check 1: Android SDK +echo 'Android SDK:' +if [ -n "${ANDROID_SDK_ROOT:-}" ]; then + echo " ANDROID_SDK_ROOT: ${ANDROID_SDK_ROOT}" + if [ -d "${ANDROID_SDK_ROOT}" ]; then + echo " ✓ SDK directory exists" + else + echo " ✗ SDK directory does not exist" + fi +else + echo " ✗ ANDROID_SDK_ROOT not set" +fi +echo '' + +# Check 2: AVD Home +echo 'AVD Environment:' +if [ -n "${ANDROID_AVD_HOME:-}" ]; then + echo " ANDROID_AVD_HOME: ${ANDROID_AVD_HOME}" + if [ -w "${ANDROID_AVD_HOME}" ]; then + echo " ✓ AVD directory is writable" + else + echo " ⚠ AVD directory is not writable" + fi +else + echo " ANDROID_AVD_HOME: NOT SET" +fi +echo '' + +# Check 3: Essential tools +echo 'Tools:' +if command -v adb >/dev/null 2>&1; then + echo " ✓ adb is in PATH" +else + echo " ✗ adb is not in PATH" +fi + +if command -v emulator >/dev/null 2>&1; then + echo " ✓ emulator is in PATH" +else + echo " ✗ emulator is not in PATH" +fi + +if command -v avdmanager >/dev/null 2>&1; then + echo " ✓ avdmanager is in PATH" +else + echo " ⚠ avdmanager is not in PATH" +fi +echo '' + +# Check 4: Device configuration +echo 'Device Configuration:' +devices_dir="${ANDROID_DEVICES_DIR:-./devbox.d/android/devices}" +device_count=$(ls -1 "${devices_dir}"/*.json 2>/dev/null | wc -l | tr -d ' ') +echo " Device files: ${device_count}" +echo " ANDROID_DEVICES: ${ANDROID_DEVICES:-'(all devices)'}" + +if [ -f "${devices_dir}/devices.lock" ]; then + echo " ✓ devices.lock exists" +else + echo " ⚠ devices.lock not generated yet (run devbox shell)" +fi +echo '' + +# Check 5: Configuration drift (android.lock vs env vars) +config_dir="${ANDROID_CONFIG_DIR:-./devbox.d/android}" +android_lock="${config_dir}/android.lock" + +echo 'Configuration Sync:' +if [ ! -f "$android_lock" ]; then + echo " ⚠ android.lock not found" + echo " Run: devbox run android:sync" +elif ! command -v jq >/dev/null 2>&1; then + echo " ⚠ jq not available, cannot check drift" +elif command -v android_check_config_drift >/dev/null 2>&1; then + # Use shared drift detection function + android_check_config_drift + + if [ "${ANDROID_DRIFT_DETECTED}" = true ]; then + echo " ⚠ Configuration drift detected:" + printf '%s' "${ANDROID_DRIFT_DETAILS}" + echo "" + echo " Fix: devbox run android:sync" + else + echo " ✓ Env vars match android.lock" + fi +else + echo " ⚠ Cannot check drift (drift detection not available)" +fi +echo '' diff --git a/plugins/android/virtenv/scripts/user/setup.sh b/plugins/android/virtenv/scripts/user/setup.sh index be3cf9a9..5e54e713 100755 --- a/plugins/android/virtenv/scripts/user/setup.sh +++ b/plugins/android/virtenv/scripts/user/setup.sh @@ -96,13 +96,9 @@ if [ -n "${ANDROID_RUNTIME_DIR:-}" ]; then echo "${ANDROID_SDK_ROOT}" > "${ANDROID_RUNTIME_DIR}/.state/sdk_root" fi -# Verify essential tools are in PATH -if ! command -v adb >/dev/null 2>&1; then - echo "⚠️ [WARN] adb not in PATH" >&2 -fi +echo "✅ [OK] Android setup complete" -if ! command -v emulator >/dev/null 2>&1; then - echo "⚠️ [WARN] emulator not in PATH" >&2 +# 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 fi - -echo "✅ [OK] Android setup complete" diff --git a/plugins/ios/plugin.json b/plugins/ios/plugin.json index db0e7825..7db7cbaf 100644 --- a/plugins/ios/plugin.json +++ b/plugins/ios/plugin.json @@ -54,6 +54,7 @@ "{{ .Virtenv }}/scripts/user/setup.sh": "virtenv/scripts/user/setup.sh", "{{ .Virtenv }}/scripts/init/init-hook.sh": "virtenv/scripts/init/init-hook.sh", "{{ .Virtenv }}/scripts/init/setup.sh": "virtenv/scripts/init/setup.sh", + "{{ .Virtenv }}/scripts/init/doctor.sh": "virtenv/scripts/init/doctor.sh", "{{ .DevboxDir }}/devices/min.json": "config/devices/min.json", "{{ .DevboxDir }}/devices/max.json": "config/devices/max.json" }, @@ -79,18 +80,17 @@ "echo 'iOS Environment Check'", "echo '===================='", "echo ''", - "echo 'IOS_DEVELOPER_DIR:' ${IOS_DEVELOPER_DIR:-'NOT SET (using xcode-select)'}", - "xcrun --show-sdk-path >/dev/null 2>&1 && echo '✓ Xcode command line tools available' || echo '✗ Xcode command line tools not found'", + "echo 'Xcode and Tools:'", + "echo ' IOS_DEVELOPER_DIR:' ${IOS_DEVELOPER_DIR:-'NOT SET (using xcode-select)'}", + "xcrun --show-sdk-path >/dev/null 2>&1 && echo ' ✓ Xcode command line tools available' || echo ' ✗ Xcode command line tools not found'", + "command -v xcrun >/dev/null 2>&1 && echo ' ✓ xcrun is in PATH' || echo ' ✗ xcrun is not in PATH'", + "command -v simctl >/dev/null 2>&1 && echo ' ✓ simctl is available' || echo ' ⚠ simctl not in PATH (use xcrun simctl)'", + "xcrun simctl list devices >/dev/null 2>&1 && echo ' ✓ xcrun simctl working' || echo ' ✗ xcrun simctl not working'", "echo ''", - "command -v xcrun >/dev/null 2>&1 && echo '✓ xcrun is in PATH' || echo '✗ xcrun is not in PATH'", - "command -v simctl >/dev/null 2>&1 && echo '✓ simctl is available' || echo '⚠ simctl not in PATH (use xcrun simctl)'", - "xcrun simctl list devices >/dev/null 2>&1 && echo '✓ xcrun simctl working' || echo '✗ xcrun simctl not working'", - "echo ''", - "echo 'Device Definitions:'", - "ls -1 ${IOS_DEVICES_DIR}/*.json 2>/dev/null | wc -l | xargs echo ' Count:'", + "echo 'Device Configuration:'", + "ls -1 ${IOS_DEVICES_DIR}/*.json 2>/dev/null | wc -l | xargs echo ' Device files:'", "echo ' IOS_DEVICES:' ${IOS_DEVICES:-'(all devices)'}", - "echo ''", - "test -f ${IOS_DEVICES_DIR}/devices.lock && echo '✓ Lock file exists' || echo '⚠ Lock file not generated yet (run devbox shell)'", + "test -f ${IOS_DEVICES_DIR}/devices.lock && echo ' ✓ devices.lock exists' || echo ' ⚠ devices.lock not generated yet (run devbox shell)'", "echo ''" ], "verify:setup": [ diff --git a/plugins/ios/virtenv/scripts/init/doctor.sh b/plugins/ios/virtenv/scripts/init/doctor.sh new file mode 100644 index 00000000..f7aaf3e1 --- /dev/null +++ b/plugins/ios/virtenv/scripts/init/doctor.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# iOS Plugin - Doctor Init Check +# Lightweight health check run on shell init +# Shows ✓ if all good, warnings if issues detected + +set -eu + +# Silent mode - only output if there are issues +issues=() + +# Check 1: Xcode command line tools +if ! xcrun --show-sdk-path >/dev/null 2>&1; then + issues+=("Xcode command line tools not available") +fi + +# Check 2: Essential tools +if ! command -v xcrun >/dev/null 2>&1; then + issues+=("xcrun not in PATH") +fi + +if ! xcrun simctl list devices >/dev/null 2>&1; then + issues+=("xcrun simctl not working") +fi + +# Check 3: Device lock file +config_dir="${IOS_CONFIG_DIR:-./devbox.d/ios}" +devices_dir="${IOS_DEVICES_DIR:-${config_dir}/devices}" +lock_file="${devices_dir}/devices.lock" + +if [ ! -f "$lock_file" ]; then + issues+=("devices.lock not found (run devbox shell to generate)") +fi + +# Output results +if [ ${#issues[@]} -eq 0 ]; then + echo "✓ iOS" +else + echo "⚠️ iOS issues detected:" >&2 + for issue in "${issues[@]}"; do + echo " - $issue" >&2 + done + echo " Run 'devbox run doctor' for more details" >&2 +fi diff --git a/plugins/ios/virtenv/scripts/user/setup.sh b/plugins/ios/virtenv/scripts/user/setup.sh index 18d66615..a4a1a96d 100755 --- a/plugins/ios/virtenv/scripts/user/setup.sh +++ b/plugins/ios/virtenv/scripts/user/setup.sh @@ -46,3 +46,8 @@ fi echo "✅ [OK] iOS environment ready" echo "✅ [OK] iOS setup complete" + +# Run lightweight doctor check +if [ -n "${IOS_SCRIPTS_DIR:-}" ] && [ -f "${IOS_SCRIPTS_DIR}/init/doctor.sh" ]; then + bash "${IOS_SCRIPTS_DIR}/init/doctor.sh" 2>&1 +fi