set quiet set dotenv-load # Simulator build settings — skip code signing (no team account needed) sim_sign := 'CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""' # Apple Developer Team ID (override with APPLE_TEAM_ID env var) team_id := env_var_or_default("APPLE_TEAM_ID", "YN68LN9T7Z") # Bundle identifier (override with BUNDLE_ID env var) bundle_id := env_var_or_default("BUNDLE_ID", "social.grain.grain") # Product/app name (override with BUNDLE_NAME env var) bundle_name := env_var_or_default("BUNDLE_NAME", "Grain") # Default: list available recipes default: just --list # Regenerate Xcode project from project.yml generate: APPLE_TEAM_ID={{team_id}} BUNDLE_ID={{bundle_id}} BUNDLE_NAME={{bundle_name}} xcodegen generate git config core.hooksPath .githooks # Worktree-local DerivedData path (avoids collisions with other worktrees) derived_data := justfile_directory() + "/.derivedData" # Build for simulator (production API — matches Xcode Run) build: set -o pipefail && xcodebuild build -scheme Grain -destination 'generic/platform=iOS Simulator' -derivedDataPath {{derived_data}} PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} {{sim_sign}} 2>&1 | xcbeautify # Build for simulator (local/dev API — overrides default PRODUCTION_API flag) build-local: set -o pipefail && xcodebuild build -scheme Grain -destination 'generic/platform=iOS Simulator' -derivedDataPath {{derived_data}} PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} SWIFT_ACTIVE_COMPILATION_CONDITIONS='DEBUG' {{sim_sign}} 2>&1 | xcbeautify # Build + install + launch on simulator (local/dev API) sim-local: build-local #!/usr/bin/env bash set -euo pipefail SIM=${SIM_UDID:-booted} xcrun simctl boot "$SIM" 2>/dev/null || true xcrun simctl bootstatus "$SIM" -b >/dev/null APP_PATH=$(find "{{derived_data}}/Build/Products/Debug-iphonesimulator" -name "{{bundle_name}}.app" -type d | head -1) xcrun simctl install "$SIM" "$APP_PATH" xcrun simctl launch "$SIM" {{bundle_id}} xcrun simctl spawn "$SIM" log config --subsystem {{bundle_id}} --mode level:debug echo "Installed and launched on simulator (local/dev API)" # Build for simulator (next.grain.social, the grain-serve deploy beside production) build-next: set -o pipefail && xcodebuild build -scheme Grain -destination 'generic/platform=iOS Simulator' -derivedDataPath {{derived_data}} PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} SWIFT_ACTIVE_COMPILATION_CONDITIONS='DEBUG NEXT_API' GRAIN_OAUTH_SCHEME=social.grain.next {{sim_sign}} 2>&1 | xcbeautify # Build + install + launch on simulator (next.grain.social) sim-next: build-next #!/usr/bin/env bash set -euo pipefail SIM=${SIM_UDID:-booted} xcrun simctl boot "$SIM" 2>/dev/null || true xcrun simctl bootstatus "$SIM" -b >/dev/null APP_PATH=$(find "{{derived_data}}/Build/Products/Debug-iphonesimulator" -name "{{bundle_name}}.app" -type d | head -1) xcrun simctl install "$SIM" "$APP_PATH" xcrun simctl launch "$SIM" {{bundle_id}} xcrun simctl spawn "$SIM" log config --subsystem {{bundle_id}} --mode level:debug echo "Installed and launched on simulator (next.grain.social)" # Build + install + launch on simulator (production API — grain.social; same as Xcode Run) sim: #!/usr/bin/env bash set -euo pipefail SIM=${SIM_UDID:-booted} xcrun simctl boot "$SIM" 2>/dev/null || true xcrun simctl bootstatus "$SIM" -b >/dev/null set -o pipefail && xcodebuild build -scheme Grain -destination 'generic/platform=iOS Simulator' -derivedDataPath "{{derived_data}}" PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} {{sim_sign}} 2>&1 | xcbeautify APP_PATH=$(find "{{derived_data}}/Build/Products/Debug-iphonesimulator" -name "{{bundle_name}}.app" -type d | head -1) xcrun simctl install "$SIM" "$APP_PATH" xcrun simctl launch "$SIM" {{bundle_id}} xcrun simctl spawn "$SIM" log config --subsystem {{bundle_id}} --mode level:debug echo "Installed and launched on simulator (grain.social)" # Build + install + launch on simulator (production API, fresh — uninstalls app first # to wipe URLCache, UserDefaults, FeedCache, LabelDefinitionsCache, Documents/Caches, etc.) sim-fresh: #!/usr/bin/env bash set -euo pipefail SIM=${SIM_UDID:-booted} xcrun simctl boot "$SIM" 2>/dev/null || true xcrun simctl bootstatus "$SIM" -b >/dev/null xcrun simctl uninstall "$SIM" {{bundle_id}} 2>/dev/null || true SIM_UDID="$SIM" just sim # Build + install + launch on simulator (local/dev API, fresh — uninstalls app first) sim-local-fresh: #!/usr/bin/env bash set -euo pipefail SIM=${SIM_UDID:-booted} xcrun simctl boot "$SIM" 2>/dev/null || true xcrun simctl bootstatus "$SIM" -b >/dev/null xcrun simctl uninstall "$SIM" {{bundle_id}} 2>/dev/null || true SIM_UDID="$SIM" just sim-local # Run tests test: set -o pipefail && xcodebuild test -scheme Grain -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -enableCodeCoverage YES PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} 2>&1 | xcbeautify # Run tests with coverage and print a per-file report for the app target coverage: #!/usr/bin/env bash set -euo pipefail result="{{derived_data}}/Coverage.xcresult" rm -rf "$result" set -o pipefail && xcodebuild test -scheme Grain -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -derivedDataPath "{{derived_data}}" -resultBundlePath "$result" -enableCodeCoverage YES PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} {{sim_sign}} 2>&1 | xcbeautify echo xcrun xccov view --report --only-targets "$result" echo xcrun xccov view --report --files-for-target {{bundle_name}}.app "$result" # Regenerate the MapLibre style assets from the Protomaps theme package basemap-style: #!/usr/bin/env bash # Committed output: the app builds without node, and a theme bump is a # reviewable diff rather than a runtime surprise. Mirrors grain-android. set -eu cd tools/basemap npm install --silent npm run generate --silent # Check formatting (list unformatted files) format: swiftformat Grain GrainTests --lint # Fix formatting in-place format-fix: swiftformat Grain GrainTests # Lint Swift code lint: swiftlint lint Grain GrainTests # Fix lint violations lint-fix: swiftlint lint --fix Grain GrainTests # Legacy alias for sim-local install: sim-local # Build and install to a plugged-in iOS device (same settings as Xcode Run) device device_id: #!/usr/bin/env bash set -euo pipefail echo "Building for device {{device_id}}..." set -o pipefail && xcodebuild build -scheme Grain -destination 'platform=iOS,id={{device_id}}' -derivedDataPath "{{derived_data}}" PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} DEVELOPMENT_TEAM={{team_id}} -allowProvisioningUpdates 2>&1 | xcbeautify APP_PATH=$(find "{{derived_data}}/Build/Products/Debug-iphoneos" -name "{{bundle_name}}.app" -type d | head -1) echo "Installing $APP_PATH..." xcrun devicectl device install app --device {{device_id}} "$APP_PATH" xcrun devicectl device process execute --device {{device_id}} /usr/bin/log -- config --subsystem {{bundle_id}} --mode level:debug 2>/dev/null || true echo "Installed to device {{device_id}}!" # Bump build number, regenerate project, archive, and upload to App Store Connect release: #!/usr/bin/env bash set -euo pipefail # Compute next build number (don't write yet — only bump on successful upload) current=$(grep 'CURRENT_PROJECT_VERSION' project.yml | head -1 | sed 's/.*"\([0-9]*\)"/\1/') next=$((current + 1)) echo "Preparing build $next (current: $current)" sed -i '' "s/CURRENT_PROJECT_VERSION: \"$current\"/CURRENT_PROJECT_VERSION: \"$next\"/" project.yml # Restore on any failure so the next attempt reuses the same number trap 'sed -i "" "s/CURRENT_PROJECT_VERSION: \"$next\"/CURRENT_PROJECT_VERSION: \"$current\"/" project.yml; BUNDLE_ID={{bundle_id}} BUNDLE_NAME={{bundle_name}} xcodegen generate >/dev/null 2>&1 || true' ERR BUNDLE_ID={{bundle_id}} BUNDLE_NAME={{bundle_name}} xcodegen generate echo "Archiving..." set -o pipefail && xcodebuild archive -scheme Grain -destination 'generic/platform=iOS' -archivePath /tmp/Grain.xcarchive CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM={{team_id}} PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} -allowProvisioningUpdates 2>&1 | xcbeautify echo "Uploading to App Store Connect..." cat > /tmp/ExportOptions.plist << 'PLIST' method app-store-connect destination upload teamID {{team_id}} PLIST xcodebuild -exportArchive -archivePath /tmp/Grain.xcarchive -exportOptionsPlist /tmp/ExportOptions.plist -exportPath /tmp/GrainExport -allowProvisioningUpdates trap - ERR echo "Build $next uploaded successfully!"