1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>destination</key>
<string>upload</string>
<key>teamID</key>
<string>{{team_id}}</string>
</dict>
</plist>
PLIST
xcodebuild -exportArchive -archivePath /tmp/Grain.xcarchive -exportOptionsPlist /tmp/ExportOptions.plist -exportPath /tmp/GrainExport -allowProvisioningUpdates
trap - ERR
echo "Build $next uploaded successfully!"