{ pkgs, lib, config, inputs, ... }: <@if(eq(context.project.mobile,"expo"))> let avdName = "Pixel_5_API36"; # Fixed console port, so scripts can address this emulator as emulator-5554 even when others run. emulatorSerial = "emulator-5554"; # Apple's xcrun, for tools that need `xcrun simctl`, which the Nix SDK's xcrun lacks. systemXcrun = pkgs.runCommand "system-xcrun" { } "mkdir -p $out/bin && ln -s /usr/bin/xcrun $out/bin/xcrun"; # Sets dev_dir to Xcode's developer dir. devenv points DEVELOPER_DIR at the Nix SDK, # which xcode-select would echo back, so it is unset for the lookup. xcodeDevDir = '' dev_dir="$(/usr/bin/env -u DEVELOPER_DIR /usr/bin/xcode-select -p 2>/dev/null)" [ -x "$dev_dir/usr/bin/simctl" ] || dev_dir=/Applications/Xcode.app/Contents/Developer ''; in <@endif> { packages = [ pkgs.bun pkgs.nodejs_24 pkgs.watchexec pkgs.sqlc pkgs.dbmate pkgs.air pkgs.buf pkgs.protoc-gen-go pkgs.protoc-gen-connect-go pkgs.protoc-gen-es pkgs.cobra-cli ]<@if(eq(context.project.mobile,"expo"))> ++ lib.optionals pkgs.stdenv.isDarwin [ pkgs.cocoapods ]<@endif>; <@if(eq(context.project.mobile,"expo"))> android = { enable = true; emulator = { enable = true; }; buildTools.version = [ "34.0.0" "35.0.0" "36.0.0" ]; reactNative.enable = true; android-studio = { enable = false; }; ndk = { enable = true; version = [ "27.1.12297006" ]; }; }; # The emulator's bundled libs are only needed on Linux; macOS uses DYLD paths. enterShell = lib.optionalString pkgs.stdenv.isLinux '' export LD_LIBRARY_PATH=$ANDROID_HOME/emulator/lib64:$LD_LIBRARY_PATH '' + lib.optionalString pkgs.stdenv.isDarwin '' # Re-check at most daily once the iOS requirements are met. SKIP_IOS_CHECK=1 turns this off. ios_check_ok="$DEVENV_STATE/ios-check-ok" if [ -z "''${SKIP_IOS_CHECK:-}" ] && [ -z "$(find "$ios_check_ok" -mtime -1 2>/dev/null)" ]; then if ios-check; then touch "$ios_check_ok"; else echo "Set SKIP_IOS_CHECK=1 to hide this." >&2; fi fi ''; # Use a system image matching the host CPU (arm64 on Apple Silicon, x86_64 elsewhere). scripts.create-avd.exec = '' if ! avdmanager list avd -c | grep -qx "${avdName}"; then echo no | avdmanager create avd -n "${avdName}" -k "system-images;android-36;google_apis_playstore;${if pkgs.stdenv.hostPlatform.isAarch64 then "arm64-v8a" else "x86_64"}" -d "pixel_5" fi ''; # macOS only: Nix can't provide Xcode, so report what the iOS build still needs. scripts.ios-check = lib.mkIf pkgs.stdenv.isDarwin { exec = '' ${xcodeDevDir} export DEVELOPER_DIR="$dev_dir" problems="" missing() { problems+=" - $1"$'\n'; } if [ ! -x "$dev_dir/usr/bin/simctl" ]; then missing "Xcode is not installed. Install it from the App Store: https://apps.apple.com/app/xcode/id497799835" else if ! /usr/bin/xcodebuild -license check >/dev/null 2>&1 || ! /usr/bin/xcodebuild -checkFirstLaunchStatus >/dev/null 2>&1; then missing "Xcode setup is incomplete (license or first-launch components). Run: sudo xcodebuild -runFirstLaunch" fi if ! /usr/bin/xcrun simctl list runtimes 2>/dev/null | grep -q '^iOS '; then missing "No iOS simulator runtime is installed. Run: xcodebuild -downloadPlatform iOS" elif ! /usr/bin/xcrun simctl list devices available 2>/dev/null | grep -q iPhone; then missing "No iPhone simulator exists. Create one in Xcode > Window > Devices and Simulators" fi fi if [ -n "$problems" ]; then printf 'iOS simulator builds are disabled until this is fixed:\n%s' "$problems" >&2 exit 1 fi ''; }; # macOS only: run on the iOS simulator when ios-check passes. scripts.run-ios = lib.mkIf pkgs.stdenv.isDarwin { exec = '' if ! ios-check; then echo "Skipping iOS." exit 0 fi ${xcodeDevDir} # devenv's Nix Apple SDK, xcrun and compiler wrappers break xcodebuild, so build in a clean env. exec /usr/bin/env -i HOME="$HOME" USER="$USER" TERM="$TERM" LANG=en_US.UTF-8 \ DEVELOPER_DIR="$dev_dir" \ PATH="${lib.makeBinPath [ pkgs.bun pkgs.nodejs_24 pkgs.cocoapods ]}:/usr/bin:/bin:/usr/sbin:/sbin" \ bunx expo run:ios --no-bundler "$@" ''; }; <@endif> languages.go.enable = true; languages.typescript.enable = true; services.postgres = { enable = true; listen_addresses = "127.0.0.1"; initialDatabases = [ { name = "<@var(context.project.name)>"; user = "pp"; pass = "<@var(context.project.name)>"; } ]; }; processes = { <@if(neq(context.project.frontend,"none"))> bundev = { exec = "bun dev"; cwd = "./apps/web"; after= ["devenv:processes:air@started"]; }; <@endif> air = { exec = "air"; cwd = "./services/api"; after = ["devenv:processes:postgres"]; }; protowatcher = { exec = "buf generate"; cwd = "./packages/proto"; watch = { paths = [ ./packages/proto ]; extensions = [ "proto" ]; }; }; protojswatcher = { exec = "bun run ./scripts/gen-rpc-index.ts"; cwd = "./"; watch = { paths = [ ./packages/rpc/src ]; extensions = ["js" "ts"]; }; after= ["devenv:processes:protowatcher@completed"]; }; <@if(eq(context.project.mobile,"expo"))> createavd = { exec = "create-avd"; }; emulator = { exec = "emulator -avd ${avdName} -port 5554"; after = ["devenv:processes:createavd@completed"]; }; # One Metro bundler serves both apps, so iOS doesn't depend on the Android build. metro = { exec = lib.optionalString pkgs.stdenv.isDarwin '' # Expo's CLI calls `xcrun simctl` to talk to the iOS simulator. ${xcodeDevDir} export DEVELOPER_DIR="$dev_dir" PATH="${systemXcrun}/bin:$PATH" '' + "exec bunx expo start --dev-client"; cwd = "./apps/mobile"; ready.http.get = { port = 8081; path = "/status"; }; }; expodev = { exec = '' # Wait for the emulator to boot, otherwise expo tries to start a second copy of the AVD. for _ in $(seq 150); do if [ "$(adb -s ${emulatorSerial} shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = 1 ]; then # Lets the emulator and USB-connected devices reach the API on the host at 127.0.0.1:8080. for device in $(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }'); do adb -s "$device" reverse tcp:8080 tcp:8080 done exec bunx expo run:android --no-bundler --device ${avdName} fi sleep 2 done echo "The Android emulator didn't finish booting within 5 minutes, see the emulator log." >&2 exit 1 ''; cwd = "./apps/mobile"; after = [ "devenv:processes:emulator@started" "devenv:processes:metro" ]; }; expoios = lib.mkIf pkgs.stdenv.isDarwin { exec = "run-ios"; cwd = "./apps/mobile"; after = [ "devenv:processes:metro" ]; }; <@endif> sqlwatcher = { exec = "sqlc generate"; watch = { paths = [ ./db/migrations ./db/query ]; extensions = ["sql" "sqlc"]; }; cwd = "./services/api"; after= ["devenv:processes:air@started"]; }; }; }