iOS client for Grain
grain.social
ios
photography
atproto
1set quiet
2set dotenv-load
3
4# Simulator build settings — skip code signing (no team account needed)
5sim_sign := 'CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""'
6
7# Apple Developer Team ID (override with APPLE_TEAM_ID env var)
8team_id := env_var_or_default("APPLE_TEAM_ID", "54P9BCDR92")
9
10# Bundle identifier (override with BUNDLE_ID env var)
11bundle_id := env_var_or_default("BUNDLE_ID", "social.grain.grain")
12
13# Default: list available recipes
14default:
15 just --list
16
17# Regenerate Xcode project from project.yml
18generate:
19 BUNDLE_ID={{bundle_id}} xcodegen generate
20 git config core.hooksPath .githooks
21
22# Worktree-local DerivedData path (avoids collisions with other worktrees)
23derived_data := justfile_directory() + "/.derivedData"
24
25# Build for simulator (production API — matches Xcode Run)
26build:
27 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
28
29# Build for simulator (local/dev API — overrides default PRODUCTION_API flag)
30build-local:
31 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
32
33# Build + install + launch on simulator (local/dev API)
34sim-local: build-local
35 #!/usr/bin/env bash
36 set -euo pipefail
37 SIM=${SIM_UDID:-booted}
38 xcrun simctl boot "$SIM" 2>/dev/null || true
39 xcrun simctl bootstatus "$SIM" -b >/dev/null
40 APP_PATH=$(find "{{derived_data}}/Build/Products/Debug-iphonesimulator" -name "${BUNDLE_NAME:-Grain}.app" -type d | head -1)
41 xcrun simctl install "$SIM" "$APP_PATH"
42 xcrun simctl launch "$SIM" {{bundle_id}}
43 xcrun simctl spawn "$SIM" log config --subsystem {{bundle_id}} --mode level:debug
44 echo "Installed and launched on simulator (local/dev API)"
45
46# Build + install + launch on simulator (production API — grain.social; same as Xcode Run)
47sim:
48 #!/usr/bin/env bash
49 set -euo pipefail
50 SIM=${SIM_UDID:-booted}
51 xcrun simctl boot "$SIM" 2>/dev/null || true
52 xcrun simctl bootstatus "$SIM" -b >/dev/null
53 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
54 APP_PATH=$(find "{{derived_data}}/Build/Products/Debug-iphonesimulator" -name "${BUNDLE_NAME:-Grain}.app" -type d | head -1)
55 xcrun simctl install "$SIM" "$APP_PATH"
56 xcrun simctl launch "$SIM" {{bundle_id}}
57 xcrun simctl spawn "$SIM" log config --subsystem {{bundle_id}} --mode level:debug
58 echo "Installed and launched on simulator (grain.social)"
59
60# Run tests
61test:
62 set -o pipefail && xcodebuild test -scheme Grain -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} 2>&1 | xcbeautify
63
64# Check formatting (list unformatted files)
65format:
66 swiftformat Grain GrainTests --lint
67
68# Fix formatting in-place
69format-fix:
70 swiftformat Grain GrainTests
71
72# Lint Swift code
73lint:
74 swiftlint lint Grain GrainTests
75
76# Fix lint violations
77lint-fix:
78 swiftlint lint --fix Grain GrainTests
79
80# Legacy alias for sim-local
81install: sim-local
82
83# Build and install to a plugged-in iOS device (same settings as Xcode Run)
84device device_id:
85 #!/usr/bin/env bash
86 set -euo pipefail
87 echo "Building for device {{device_id}}..."
88 set -o pipefail && xcodebuild build -scheme Grain -destination 'platform=iOS,id={{device_id}}' PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} -allowProvisioningUpdates 2>&1 | xcbeautify
89 APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData/Grain-*/Build/Products/Debug-iphoneos -name "${BUNDLE_NAME:-Grain}.app" -type d | head -1)
90 echo "Installing $APP_PATH..."
91 xcrun devicectl device install app --device {{device_id}} "$APP_PATH"
92 xcrun devicectl device process execute --device {{device_id}} /usr/bin/log -- config --subsystem {{bundle_id}} --mode level:debug 2>/dev/null || true
93 echo "Installed to device {{device_id}}!"
94
95# Bump build number, regenerate project, archive, and upload to App Store Connect
96release:
97 #!/usr/bin/env bash
98 set -euo pipefail
99 # Compute next build number (don't write yet — only bump on successful upload)
100 current=$(grep 'CURRENT_PROJECT_VERSION' project.yml | head -1 | sed 's/.*"\([0-9]*\)"/\1/')
101 next=$((current + 1))
102 echo "Preparing build $next (current: $current)"
103 sed -i '' "s/CURRENT_PROJECT_VERSION: \"$current\"/CURRENT_PROJECT_VERSION: \"$next\"/" project.yml
104 # Restore on any failure so the next attempt reuses the same number
105 trap 'sed -i "" "s/CURRENT_PROJECT_VERSION: \"$next\"/CURRENT_PROJECT_VERSION: \"$current\"/" project.yml; BUNDLE_ID={{bundle_id}} xcodegen generate >/dev/null 2>&1 || true' ERR
106 BUNDLE_ID={{bundle_id}} xcodegen generate
107 echo "Archiving..."
108 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
109 echo "Uploading to App Store Connect..."
110 cat > /tmp/ExportOptions.plist << 'PLIST'
111 <?xml version="1.0" encoding="UTF-8"?>
112 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
113 <plist version="1.0">
114 <dict>
115 <key>method</key>
116 <string>app-store-connect</string>
117 <key>destination</key>
118 <string>upload</string>
119 <key>teamID</key>
120 <string>{{team_id}}</string>
121 </dict>
122 </plist>
123 PLIST
124 xcodebuild -exportArchive -archivePath /tmp/Grain.xcarchive -exportOptionsPlist /tmp/ExportOptions.plist -exportPath /tmp/GrainExport -allowProvisioningUpdates
125 trap - ERR
126 echo "Build $next uploaded successfully!"