diff --git a/README.md b/README.md index ff550fd8b..34a6ccd9d 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,35 @@ app. For simulators, use `pnpm expo:ios:simulator` on iOS. For Android, start an Android emulator first, then run `pnpm expo:android`. +#### Windows: Android development setup + +The npm scripts above use `cross-env`, so they work in Windows PowerShell/cmd +out of the box. The first Android build needs a native toolchain: + +1. Install **JDK 17 or 21** ([Eclipse Temurin](https://adoptium.net)) and set + the `JAVA_HOME` user environment variable to its folder. +2. Install **[Android Studio](https://developer.android.com/studio)** and, in + the **SDK Manager**, install: an Android platform (`Android 15/16`, API + 35/36), **Android SDK Build-Tools**, **Android SDK Platform-Tools**, and an + `x86_64` system image for the emulator. +3. Create an AVD in **Device Manager** (or connect a device with USB debugging). +4. Set the `ANDROID_HOME` user environment variable to your SDK path + (default: `%LOCALAPPDATA%\Android\Sdk`). + +Check that everything is in place: + +```powershell +pnpm expo:doctor:android +``` + +Then start Metro and build the dev client (first build downloads Gradle and +native dependencies, so it takes a while): + +```powershell +pnpm expo:start # terminal 1: Metro dev server +pnpm expo:android # terminal 2: build + install the dev client +``` + Mobile app source lives in [`packages/app-expo`](packages/app-expo). ### AI Configuration diff --git a/README_CN.md b/README_CN.md index 92179df04..05c756dd0 100644 --- a/README_CN.md +++ b/README_CN.md @@ -227,6 +227,34 @@ Expo Go。Expo Go 无法加载 ReadAny 当前依赖的原生模块和应用配 模拟器调试时,iOS 使用 `pnpm expo:ios:simulator`;Android 先启动 Android 模拟器,再运行 `pnpm expo:android`。 +#### Windows:Android 开发环境配置 + +上面的 npm 脚本都用了 `cross-env`,在 Windows 的 PowerShell/cmd 下可直接运行。 +首次 Android 构建需要原生工具链: + +1. 安装 **JDK 17 或 21**([Eclipse Temurin](https://adoptium.net)),并把 + `JAVA_HOME` 用户环境变量指向其目录。 +2. 安装 **[Android Studio](https://developer.android.com/studio)**,在 + **SDK Manager** 中安装:Android 平台(`Android 15/16`,API 35/36)、 + **Android SDK Build-Tools**、**Android SDK Platform-Tools**,以及一个 + `x86_64` 模拟器系统镜像。 +3. 在 **Device Manager** 创建 AVD(或用开启 USB 调试的真机)。 +4. 把 `ANDROID_HOME` 用户环境变量设为 SDK 路径(默认 + `%LOCALAPPDATA%\Android\Sdk`)。 + +检查环境是否就绪: + +```powershell +pnpm expo:doctor:android +``` + +然后启动 Metro 并构建 dev client(首次构建会下载 Gradle 和原生依赖,耗时较长): + +```powershell +pnpm expo:start # 终端 1:Metro dev server +pnpm expo:android # 终端 2:构建并安装 dev client +``` + 移动端源码位于 [`packages/app-expo`](packages/app-expo)。 ### AI 配置 diff --git a/package.json b/package.json index e596e59c8..634d2b3ad 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "expo:ios": "pnpm --filter @readany/app-expo ios", "expo:ios:simulator": "pnpm --filter @readany/app-expo ios:simulator", "expo:android": "pnpm --filter @readany/app-expo android", + "expo:doctor:android": "pnpm --filter @readany/app-expo doctor:android", "expo:prebuild": "pnpm --filter @readany/app-expo prebuild", "eas:device:create": "pnpm --filter @readany/app-expo eas:device:create", "eas:build:android": "pnpm --filter @readany/app-expo eas:build:android", diff --git a/packages/app-expo/package.json b/packages/app-expo/package.json index 8117f1158..2fbfc9ad4 100644 --- a/packages/app-expo/package.json +++ b/packages/app-expo/package.json @@ -16,33 +16,34 @@ } }, "scripts": { - "start": "APP_VARIANT=development pnpm run build:reader && APP_VARIANT=development node scripts/configure-native-variant.js && APP_VARIANT=development expo start --dev-client --scheme readany-dev", - "start:clear": "APP_VARIANT=development pnpm run build:reader && APP_VARIANT=development node scripts/configure-native-variant.js && APP_VARIANT=development expo start --dev-client --scheme readany-dev --clear", + "start": "cross-env APP_VARIANT=development pnpm run build:reader && cross-env APP_VARIANT=development node scripts/configure-native-variant.js && cross-env APP_VARIANT=development expo start --dev-client --scheme readany-dev", + "start:clear": "cross-env APP_VARIANT=development pnpm run build:reader && cross-env APP_VARIANT=development node scripts/configure-native-variant.js && cross-env APP_VARIANT=development expo start --dev-client --scheme readany-dev --clear", "start:dev": "pnpm run start", "start:dev:clear": "pnpm run start:clear", "preandroid": "pnpm run build:reader", "android": "expo run:android", - "android:dev": "APP_VARIANT=development pnpm run build:reader && APP_VARIANT=development expo run:android --device", + "android:dev": "cross-env APP_VARIANT=development pnpm run build:reader && cross-env APP_VARIANT=development expo run:android --device", "preios": "pnpm run build:reader && node scripts/configure-native-variant.js", "ios": "expo run:ios", - "ios:dev": "APP_VARIANT=development pnpm run build:reader && APP_VARIANT=development node scripts/configure-native-variant.js && APP_VARIANT=development expo run:ios --device", - "ios:simulator": "APP_VARIANT=development pnpm run build:reader && APP_VARIANT=development node scripts/configure-native-variant.js && APP_VARIANT=development expo run:ios", + "ios:dev": "cross-env APP_VARIANT=development pnpm run build:reader && cross-env APP_VARIANT=development node scripts/configure-native-variant.js && cross-env APP_VARIANT=development expo run:ios --device", + "ios:simulator": "cross-env APP_VARIANT=development pnpm run build:reader && cross-env APP_VARIANT=development node scripts/configure-native-variant.js && cross-env APP_VARIANT=development expo run:ios", "preprebuild": "pnpm run build:reader", "prebuild": "expo prebuild", "build:reader": "node scripts/build-reader.js", "sync:native-variant": "node scripts/configure-native-variant.js", + "doctor:android": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/check-android-env.ps1", "lint": "biome check .", "test": "vitest run", "eas-build-pre-install": "bash scripts/eas-install-ios-build-tools.sh", "eas-build-post-install": "pnpm run build:reader && node scripts/configure-native-variant.js", "eas:device:create": "pnpm exec eas device:create", - "eas:build:android": "APP_VARIANT=production pnpm exec eas build --profile production --platform android", - "eas:build:android:dev": "APP_VARIANT=development pnpm exec eas build --profile development --platform android", - "eas:build:android:preview": "APP_VARIANT=preview pnpm exec eas build --profile preview --platform android", - "eas:build:ios": "APP_VARIANT=production pnpm exec eas build --profile production --platform ios", - "eas:build:ios:dev": "APP_VARIANT=development pnpm exec eas build --profile development --platform ios", - "eas:build:ios:simulator": "APP_VARIANT=development pnpm exec eas build --profile development-simulator --platform ios", - "eas:build:ios:preview": "APP_VARIANT=preview pnpm exec eas build --profile preview --platform ios" + "eas:build:android": "cross-env APP_VARIANT=production pnpm exec eas build --profile production --platform android", + "eas:build:android:dev": "cross-env APP_VARIANT=development pnpm exec eas build --profile development --platform android", + "eas:build:android:preview": "cross-env APP_VARIANT=preview pnpm exec eas build --profile preview --platform android", + "eas:build:ios": "cross-env APP_VARIANT=production pnpm exec eas build --profile production --platform ios", + "eas:build:ios:dev": "cross-env APP_VARIANT=development pnpm exec eas build --profile development --platform ios", + "eas:build:ios:simulator": "cross-env APP_VARIANT=development pnpm exec eas build --profile development-simulator --platform ios", + "eas:build:ios:preview": "cross-env APP_VARIANT=preview pnpm exec eas build --profile preview --platform ios" }, "dependencies": { "@aws-sdk/client-s3": "^3.712.0", @@ -117,6 +118,7 @@ "@types/react": "^19.1.17", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-transform-import-meta": "^2.3.2", + "cross-env": "^7.0.3", "eas-cli": "^18.11.0", "esbuild": "^0.27.3", "react-native-svg-transformer": "^1.5.3", diff --git a/packages/app-expo/scripts/check-android-env.ps1 b/packages/app-expo/scripts/check-android-env.ps1 new file mode 100644 index 000000000..cdbac2ec2 --- /dev/null +++ b/packages/app-expo/scripts/check-android-env.ps1 @@ -0,0 +1,189 @@ +<# +.SYNOPSIS + ReadAny - Android development environment checker (Windows). + +.DESCRIPTION + Verifies the toolchain needed to build and run the Expo dev client on + Android: Node.js/pnpm, JDK 17-21, the Android SDK (platform-tools, + platforms, build-tools, accepted licenses), and an emulator AVD or + connected device. Prints PASS/WARN/FAIL per check with concrete fix steps + and exits non-zero when any check fails. + +.EXAMPLE + powershell -NoProfile -ExecutionPolicy Bypass -File scripts/check-android-env.ps1 +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = "SilentlyContinue" + +$script:Pass = 0 +$script:Fail = 0 +$script:Warn = 0 + +# -Warn marks a condition that alone should not fail the doctor (the paired +# check being satisfied is enough), so it counts separately from failures. +function Write-Result { + param([string]$Name, [bool]$Ok, [string]$Hint, [switch]$Warn) + if ($Ok) { $script:Pass++ } elseif ($Warn) { $script:Warn++ } else { $script:Fail++ } + $mark = if ($Ok) { "[ OK ]" } elseif ($Warn) { "[WARN]" } else { "[FAIL]" } + $color = if ($Ok) { "Green" } elseif ($Warn) { "Yellow" } else { "Red" } + Write-Host ("{0} {1}" -f $mark, $Name) -ForegroundColor $color + if (-not $Ok -and $Hint) { + Write-Host (" > {0}" -f $Hint) -ForegroundColor Yellow + } +} + +Write-Host "ReadAny - Android dev environment check" -ForegroundColor Cyan +Write-Host "" + +# --- Node.js / pnpm -------------------------------------------------- +# The doctor exists to catch version problems, so verify the documented +# minimum (Node 18+), not just that the command resolves. +$node = Get-Command node -ErrorAction SilentlyContinue +$nodeVer = $null +$nodeOk = $false +if ($node) { + $nodeVer = (& node -v 2>$null | Select-Object -First 1) + # Explicit if keeps the version comparison self-contained and independent + # of $Matches state. (PowerShell's -and short-circuits, so the previous + # chained form was correct too — this is for clarity, not a fix.) + if ($nodeVer -match '^v(\d+)') { $nodeOk = [int]$Matches[1] -ge 18 } +} +Write-Result "Node.js" $nodeOk ( + "Node.js 18+ is required (found: $(if ($nodeVer) { $nodeVer.Trim() } else { 'not installed' })). Install from https://nodejs.org" +) + +$pnpm = Get-Command pnpm -ErrorAction SilentlyContinue +Write-Result "pnpm" ($null -ne $pnpm) "Install pnpm: npm install -g pnpm" + +# --- JDK ------------------------------------------------------------- +$javaHome = $env:JAVA_HOME +$jdkOk = $false +$jdkHint = "Install JDK 17 or 21 from https://adoptium.net, then set the JAVA_HOME user environment variable to its folder." +if ($javaHome) { + $javaBin = Join-Path $javaHome "bin\java.exe" + if (Test-Path $javaBin) { + # `cmd /c ... 2>&1` merges java's stderr at the OS level; under + # $ErrorActionPreference=SilentlyContinue PowerShell would otherwise swallow + # the NativeCommandError records produced by `2>&1`. + $verLine = (& cmd /c "`"$javaBin`" -version 2>&1" 2>$null | Select-Object -First 1) + if ($verLine -match 'version "(\d+)') { + $major = [int]$Matches[1] + # Legacy layouts report `java version "1.8.0_401"` — the leading 1 is + # not the major, the minor after "1." is (that's JDK 8). + if ($major -eq 1 -and $verLine -match 'version "1\.(\d+)') { $major = [int]$Matches[1] } + # Enforce the range Gradle 8.13 actually supports; without the upper + # bound a too-new JDK (22+) silently passes and the build fails later. + $jdkOk = $major -ge 17 -and $major -le 21 + if (-not $jdkOk) { + if ($major -lt 17) { + $jdkHint = "Found JDK $major - Gradle 8.13 needs JDK 17 or 21." + } else { + $jdkHint = "Found JDK $major - Gradle 8.13 needs JDK 17 or 21 (JDK 22+ is too new)." + } + } + # A JRE passes the java -version check but Gradle needs the compiler: + # JRE installs ship bin\java.exe without bin\javac.exe. + if ($jdkOk -and -not (Test-Path (Join-Path $javaHome "bin\javac.exe"))) { + $jdkOk = $false + $jdkHint = "JAVA_HOME points to a JRE (no bin\javac.exe) - Gradle needs a full JDK. Install JDK 17 or 21 from https://adoptium.net (choose JDK, not JRE)." + } + } else { + $jdkHint = "Could not read the JDK version from $javaBin." + } + } else { + $jdkHint = "JAVA_HOME is set to `"$javaHome`" but bin\java.exe was not found there." + } +} else { + $jdkHint = "JAVA_HOME is not set. Install JDK 17/21 and set JAVA_HOME (e.g. C:\Program Files\Eclipse Adoptium\jdk-21.x)." +} +Write-Result "JDK (JAVA_HOME=$javaHome)" $jdkOk $jdkHint + +# --- Android SDK ----------------------------------------------------- +# Gradle resolves the SDK from ANDROID_HOME/ANDROID_SDK_ROOT, then from +# android/local.properties (sdk.dir — written by Android Studio / Gradle +# on first sync). Everything else must NOT pass: a bare default-folder +# fallback used to print a green PASS while the build failed with +# "SDK location not found" — exactly the trap this doctor exists to catch. +$sdk = $env:ANDROID_HOME +if (-not $sdk) { $sdk = $env:ANDROID_SDK_ROOT } +$sdkFromEnv = [bool]$sdk +$sdkDefault = Join-Path $env:LOCALAPPDATA "Android\Sdk" +if (-not $sdk) { $sdk = $sdkDefault } +$sdkOk = Test-Path $sdk + +# Honor a pinned sdk.dir as the alternative source the env vars would be. +$localProps = Join-Path (Split-Path $PSScriptRoot -Parent) "android\local.properties" +$sdkDirPinned = $false +$sdkPinnedPath = $null +if (Test-Path $localProps) { + $pinnedLine = Get-Content $localProps | Where-Object { $_ -match '^sdk\.dir=(.+)$' } | Select-Object -First 1 + if ($pinnedLine) { + # properties-file escapes: backslashes doubled, colon written as \: + $sdkPinnedPath = $pinnedLine.Substring(8).Replace('\\', '\').Replace('\:', ':') + $sdkDirPinned = Test-Path $sdkPinnedPath + } +} + +$sdkPass = ($sdkOk -and $sdkFromEnv) -or $sdkDirPinned +$sdkDisplay = if (-not $sdkFromEnv -and $sdkDirPinned) { $sdkPinnedPath } else { $sdk } +$sdkHint = + if ($sdkOk -and -not $sdkFromEnv -and -not $sdkDirPinned) { + "Found the SDK only at the default location, but Gradle does not look there. Set the ANDROID_HOME user environment variable to $sdkDefault (or let Android Studio write android/local.properties)." + } elseif ($sdkFromEnv -and -not $sdkOk) { + "ANDROID_HOME/ANDROID_SDK_ROOT is set to `"$sdk`" but that folder does not exist. Update the variable (e.g. to $sdkDefault) or reinstall the SDK there." + } else { + "Install Android Studio or the command-line tools, then set the ANDROID_HOME user environment variable to the SDK folder (e.g. $sdkDefault)." + } +Write-Result "Android SDK ($sdkDisplay)" $sdkPass $sdkHint + +if ($sdkOk) { + $adb = Join-Path $sdk "platform-tools\adb.exe" + $adbOk = Test-Path $adb + Write-Result "platform-tools (adb)" $adbOk "SDK Manager -> SDK Tools -> check 'Android SDK Platform-Tools'." + + $platforms = @(Get-ChildItem (Join-Path $sdk "platforms") -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^android-(3[5-9]|[4-9][0-9])$' }) + Write-Result "Android SDK Platform (API 35+)" ($platforms.Count -gt 0) ( + "SDK Manager -> SDK Platforms -> check 'Android 15/16 (API 35/36)'. Gradle auto-downloads the exact platform if licenses are accepted." + ) + + $buildTools = @(Get-ChildItem (Join-Path $sdk "build-tools") -Directory -ErrorAction SilentlyContinue) + Write-Result "Android SDK Build-Tools" ($buildTools.Count -gt 0) "SDK Manager -> SDK Tools -> check 'Android SDK Build-Tools'." + + Write-Result "SDK licenses accepted" (Test-Path (Join-Path $sdk "licenses")) ( + "Accept them via SDK Manager, or run sdkmanager --licenses." + ) + + $deviceCount = 0 + if ($adbOk) { + $devOut = @(& $adb devices 2>&1) + $deviceCount = @($devOut | Where-Object { $_ -match "\tdevice$" }).Count + } + + $emu = Join-Path $sdk "emulator\emulator.exe" + $avdCount = 0 + if (Test-Path $emu) { + $avdOut = @(& $emu -list-avds 2>&1) + $avdCount = @($avdOut | Where-Object { $_ -match '\S' -and $_ -notmatch '^INFO|^WARNING|^ERROR' }).Count + } + + # DESCRIPTION promises "an emulator AVD or connected device" — either one + # satisfies the toolchain. A missing half degrades to WARN when its twin + # is present, so a clean setup (AVD created, emulator simply not running + # yet — `expo run:android` starts it) does not fail the whole doctor. + Write-Result "Android device connected" ($deviceCount -gt 0) ( + "Start an emulator (Device Manager) or connect a device with USB debugging enabled." + ) -Warn:($avdCount -gt 0) + Write-Result "Emulator AVD configured" ($avdCount -gt 0) ( + "Android Studio -> Device Manager -> Create device (x86_64 system image, API 35)." + ) -Warn:($deviceCount -gt 0) +} + +# --- Summary --------------------------------------------------------- +Write-Host "" +$color = if ($script:Fail -gt 0) { "Red" } elseif ($script:Warn -gt 0) { "Yellow" } else { "Green" } +Write-Host ("{0} passed, {1} warned, {2} failed" -f $script:Pass, $script:Warn, $script:Fail) -ForegroundColor $color +if ($script:Fail -gt 0) { + exit 1 +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aed804ec..a1ed6aa7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -470,6 +470,9 @@ importers: babel-plugin-transform-import-meta: specifier: ^2.3.2 version: 2.3.3(@babel/core@7.29.0) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eas-cli: specifier: ^18.11.0 version: 18.11.0(@types/node@25.5.2)(typescript@5.9.3) @@ -5013,6 +5016,11 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -14876,6 +14884,10 @@ snapshots: crelt@1.0.6: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1