xgem-cli 2.0.0-alpha → 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -11
- package/bin/xgem +19 -4
- package/bin/xgem.js +220 -0
- package/lib/create.sh +257 -0
- package/lib/doctor.sh +13 -2
- package/lib/flutter.sh +161 -18
- package/lib/git.sh +106 -8
- package/lib/ios.sh +126 -47
- package/lib/scaffold.sh +4 -4
- package/lib/version.sh +1 -1
- package/lib-win/doctor.js +44 -0
- package/lib-win/flutter.js +101 -0
- package/lib-win/git.js +241 -0
- package/lib-win/logger.js +35 -0
- package/lib-win/scaffold.js +80 -0
- package/lib-win/utils.js +67 -0
- package/package.json +9 -10
- package/templates-win/docker/build-up.mjs.tmpl +3 -0
- package/templates-win/docker/hard-clean.mjs.tmpl +4 -0
- package/templates-win/flutter/build-runner.mjs.tmpl +4 -0
- package/templates-win/flutter/build.mjs.tmpl +4 -0
- package/templates-win/flutter/hard-clean.mjs.tmpl +6 -0
- package/templates-win/go/build.mjs.tmpl +3 -0
- package/templates-win/go/hard-clean.mjs.tmpl +4 -0
- package/templates-win/node/build.mjs.tmpl +4 -0
- package/templates-win/node/hard-clean.mjs.tmpl +9 -0
- package/templates-win/node/start.mjs.tmpl +3 -0
- package/templates-win/python/hard-clean.mjs.tmpl +8 -0
- package/templates-win/python/install.mjs.tmpl +12 -0
- package/templates-win/rust/build.mjs.tmpl +3 -0
- package/templates-win/rust/hard-clean.mjs.tmpl +3 -0
- package/templates-win/webframework/build.mjs.tmpl +3 -0
- package/templates-win/webframework/dev.mjs.tmpl +3 -0
- package/templates-win/webframework/hard-clean.mjs.tmpl +9 -0
package/lib/doctor.sh
CHANGED
|
@@ -26,7 +26,10 @@ flutter_supports_flag() {
|
|
|
26
26
|
flutter_config_spm_enabled() {
|
|
27
27
|
has_cmd flutter || { echo "unknown (flutter not found)"; return; }
|
|
28
28
|
local line
|
|
29
|
-
|
|
29
|
+
# Anchored to "enable-swift-package-manager:" (the current-value line)
|
|
30
|
+
# so this doesn't instead match the "--[no-]enable-swift-package-manager"
|
|
31
|
+
# flag-syntax help line that `flutter config` also prints.
|
|
32
|
+
line=$(flutter config 2>/dev/null | grep -im1 -E '^[[:space:]]*enable-swift-package-manager:')
|
|
30
33
|
if [ -z "$line" ]; then
|
|
31
34
|
echo "unknown (not reported by this Flutter version)"
|
|
32
35
|
else
|
|
@@ -86,7 +89,7 @@ doctor_print_ios() {
|
|
|
86
89
|
echo "Deployment target [$cfg]: ${t:-unknown}"
|
|
87
90
|
done
|
|
88
91
|
|
|
89
|
-
local required
|
|
92
|
+
local required generated
|
|
90
93
|
required=$(ios_required_spm_deployment_target "$project_dir" 2>/dev/null)
|
|
91
94
|
if [ -n "$required" ]; then
|
|
92
95
|
echo "Required by resolved SwiftPM plugins: $required"
|
|
@@ -94,6 +97,14 @@ doctor_print_ios() {
|
|
|
94
97
|
echo "Required by resolved SwiftPM plugins: unable to determine (run 'flutter pub get' first, or no SPM plugins declare an explicit floor)"
|
|
95
98
|
fi
|
|
96
99
|
|
|
100
|
+
if [ "$uses_spm" = "yes" ]; then
|
|
101
|
+
generated=$(ios_generated_package_target "$project_dir" 2>/dev/null)
|
|
102
|
+
echo "FlutterGeneratedPluginSwiftPackage declares: ${generated:-unknown}"
|
|
103
|
+
if [ -n "$required" ] && [ -n "$generated" ] && _ios_ver_lt "$generated" "$required"; then
|
|
104
|
+
log_warn "This is stale — it declares iOS $generated but plugins need $required. This can happen even when your app's own deployment target is already high enough; 'xgem run flutter build' will detect and fix this."
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
97
108
|
if [ "$uses_spm" = "yes" ]; then
|
|
98
109
|
ios_known_issue_check
|
|
99
110
|
fi
|
package/lib/flutter.sh
CHANGED
|
@@ -66,14 +66,19 @@ _flutter_build_ios() {
|
|
|
66
66
|
|
|
67
67
|
require_cmd xcodebuild "iOS builds require Xcode's command-line tools."
|
|
68
68
|
|
|
69
|
+
# Must run pub get BEFORE reconciling: the required-deployment-target
|
|
70
|
+
# check reads .dart_tool/package_config.json, and that file only
|
|
71
|
+
# reflects the plugin versions actually in use after a fresh pub get.
|
|
72
|
+
# Reconciling against a stale package_config.json can silently pass a
|
|
73
|
+
# check that the real, just-resolved dependency graph would fail.
|
|
74
|
+
log_info "Regenerating dependencies..."
|
|
75
|
+
flutter pub get
|
|
76
|
+
|
|
69
77
|
log_info "Reconciling iOS deployment target against resolved SwiftPM plugins..."
|
|
70
78
|
if ! ios_reconcile_deployment_target "."; then
|
|
71
79
|
die "iOS deployment target could not be reconciled; aborting before build."
|
|
72
80
|
fi
|
|
73
81
|
|
|
74
|
-
log_info "Regenerating dependencies..."
|
|
75
|
-
flutter pub get
|
|
76
|
-
|
|
77
82
|
if ios_project_uses_pods "."; then
|
|
78
83
|
log_info "CocoaPods detected - installing pods..."
|
|
79
84
|
( cd ios || exit 1; pod install )
|
|
@@ -100,7 +105,7 @@ _flutter_build_ios() {
|
|
|
100
105
|
mkdir -p "$archive_dir"
|
|
101
106
|
archive_path="$archive_dir/Runner $archive_time.xcarchive"
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
local -a archive_cmd=(xcodebuild -workspace ios/Runner.xcworkspace \
|
|
104
109
|
-scheme Runner \
|
|
105
110
|
-sdk iphoneos \
|
|
106
111
|
-configuration Release \
|
|
@@ -108,10 +113,35 @@ _flutter_build_ios() {
|
|
|
108
113
|
-archivePath "$archive_path" \
|
|
109
114
|
"BUILD_NUMBER=$build_number" \
|
|
110
115
|
"MARKETING_VERSION=$build_name" \
|
|
111
|
-
-allowProvisioningUpdates
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
-allowProvisioningUpdates)
|
|
117
|
+
|
|
118
|
+
if ! "${archive_cmd[@]}" > "$log_file" 2>&1; then
|
|
119
|
+
# Static pre-checks can miss cases the real SwiftPM resolution
|
|
120
|
+
# catches (different plugin manifest layouts, symlinked package
|
|
121
|
+
# dirs, etc.) — Xcode's own error is authoritative, so parse it
|
|
122
|
+
# directly and retry once before giving up.
|
|
123
|
+
local retry_required
|
|
124
|
+
retry_required=$(ios_parse_required_from_build_log "$log_file")
|
|
125
|
+
if [ -n "$retry_required" ]; then
|
|
126
|
+
log_warn "Archive failed on a SwiftPM platform-version mismatch Xcode reports directly (requires iOS $retry_required). Retrying with that applied..."
|
|
127
|
+
ios_pbxproj_patch_deployment_target "$retry_required" "."
|
|
128
|
+
ios_regenerate_generated_package "."
|
|
129
|
+
local regenerated
|
|
130
|
+
regenerated=$(ios_generated_package_target ".")
|
|
131
|
+
if [ -z "$regenerated" ] || _ios_ver_lt "$regenerated" "$retry_required"; then
|
|
132
|
+
ios_patch_generated_package_target "$retry_required" "."
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if ! "${archive_cmd[@]}" > "$log_file" 2>&1; then
|
|
136
|
+
log_error "Archive FAILED again after retry"
|
|
137
|
+
cat "$log_file"
|
|
138
|
+
die "xcodebuild archive failed" "$?"
|
|
139
|
+
fi
|
|
140
|
+
else
|
|
141
|
+
log_error "Archive FAILED"
|
|
142
|
+
cat "$log_file"
|
|
143
|
+
die "xcodebuild archive failed" "$?"
|
|
144
|
+
fi
|
|
115
145
|
fi
|
|
116
146
|
log_success "Archive created at: $archive_path"
|
|
117
147
|
|
|
@@ -159,20 +189,21 @@ cmd_flutter_build() {
|
|
|
159
189
|
echo ""
|
|
160
190
|
echo "Select Target Platform:"
|
|
161
191
|
echo "1) APK (Android)"
|
|
162
|
-
echo "2)
|
|
163
|
-
echo "3)
|
|
164
|
-
echo "4)
|
|
165
|
-
echo "5)
|
|
192
|
+
echo "2) App Bundle (Android, .aab — required for Play Store uploads)"
|
|
193
|
+
echo "3) iOS"
|
|
194
|
+
echo "4) macOS"
|
|
195
|
+
echo "5) Windows"
|
|
196
|
+
echo "6) Linux"
|
|
166
197
|
local platform_choice
|
|
167
|
-
read -r -p "Choose [1-
|
|
168
|
-
if ! [[ "$platform_choice" =~ ^[1-
|
|
198
|
+
read -r -p "Choose [1-6]: " platform_choice
|
|
199
|
+
if ! [[ "$platform_choice" =~ ^[1-6]$ ]]; then
|
|
169
200
|
die "Invalid platform choice '$platform_choice'."
|
|
170
201
|
fi
|
|
171
202
|
|
|
172
203
|
local mode="--release"
|
|
173
204
|
[[ "$is_release" == "n" || "$is_release" == "N" ]] && mode="--debug"
|
|
174
205
|
|
|
175
|
-
if [ "$platform_choice" -eq
|
|
206
|
+
if [ "$platform_choice" -eq 3 ]; then
|
|
176
207
|
_flutter_build_ios "$build_name" "$build_number" "$mode"
|
|
177
208
|
return
|
|
178
209
|
fi
|
|
@@ -180,9 +211,10 @@ cmd_flutter_build() {
|
|
|
180
211
|
log_info "Initializing Flutter Build ($mode) for version $build_name+$build_number..."
|
|
181
212
|
case $platform_choice in
|
|
182
213
|
1) flutter build apk "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
|
|
183
|
-
|
|
184
|
-
4) flutter build
|
|
185
|
-
5) flutter build
|
|
214
|
+
2) flutter build appbundle "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
|
|
215
|
+
4) flutter build macos "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
|
|
216
|
+
5) flutter build windows "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
|
|
217
|
+
6) flutter build linux "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
|
|
186
218
|
esac
|
|
187
219
|
}
|
|
188
220
|
|
|
@@ -205,3 +237,114 @@ flutter_run_native_command() {
|
|
|
205
237
|
*) die "No native flutter command for '$script'." ;;
|
|
206
238
|
esac
|
|
207
239
|
}
|
|
240
|
+
|
|
241
|
+
# create_flutter_project — used by lib/create.sh's create_project_wizard.
|
|
242
|
+
# `flutter create` needs a target dir; "." scaffolds into the current one.
|
|
243
|
+
create_flutter_project() {
|
|
244
|
+
require_cmd flutter "Install Flutter: https://docs.flutter.dev/get-started/install"
|
|
245
|
+
local project_dir
|
|
246
|
+
project_dir=$(_prompt_project_location "Flutter")
|
|
247
|
+
[ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
|
|
248
|
+
if [ "$project_dir" = "." ]; then
|
|
249
|
+
flutter create . || die "flutter create failed."
|
|
250
|
+
else
|
|
251
|
+
flutter create "$project_dir" || die "flutter create failed."
|
|
252
|
+
cd "$project_dir" || die "Could not enter $project_dir"
|
|
253
|
+
fi
|
|
254
|
+
_flutter_offer_run_on_device
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# _flutter_select_device -> echoes the chosen device id, or nothing if
|
|
258
|
+
# none available/selected. Uses `flutter devices --machine` (JSON) parsed
|
|
259
|
+
# with python3 — already a soft dependency elsewhere in lib/ios.sh for the
|
|
260
|
+
# same reason (no jq assumed present).
|
|
261
|
+
_flutter_select_device() {
|
|
262
|
+
require_cmd python3 "Needed to parse 'flutter devices --machine' output."
|
|
263
|
+
local devices_json_file
|
|
264
|
+
devices_json_file=$(mktemp)
|
|
265
|
+
flutter devices --machine > "$devices_json_file" 2>/dev/null
|
|
266
|
+
|
|
267
|
+
local -a ids=() labels=()
|
|
268
|
+
while IFS='|' read -r id label; do
|
|
269
|
+
[ -n "$id" ] || continue
|
|
270
|
+
ids+=("$id")
|
|
271
|
+
labels+=("$label")
|
|
272
|
+
done < <(python3 - "$devices_json_file" <<'PYEOF'
|
|
273
|
+
import json, sys
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
with open(sys.argv[1]) as f:
|
|
277
|
+
devices = json.load(f)
|
|
278
|
+
except Exception:
|
|
279
|
+
devices = []
|
|
280
|
+
|
|
281
|
+
for d in devices:
|
|
282
|
+
platform = d.get("platform") or d.get("targetPlatform") or ""
|
|
283
|
+
name = d.get("name", "unknown")
|
|
284
|
+
device_id = d.get("id", "")
|
|
285
|
+
print(f"{device_id}|{name} ({platform})")
|
|
286
|
+
PYEOF
|
|
287
|
+
)
|
|
288
|
+
rm -f "$devices_json_file"
|
|
289
|
+
|
|
290
|
+
if [ ${#ids[@]} -eq 0 ]; then
|
|
291
|
+
log_warn "No running devices/simulators found."
|
|
292
|
+
_flutter_offer_launch_emulator
|
|
293
|
+
return 1
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
echo "Available devices:" >&2
|
|
297
|
+
local i
|
|
298
|
+
for i in "${!ids[@]}"; do
|
|
299
|
+
echo "$((i + 1))) ${labels[$i]}" >&2
|
|
300
|
+
done
|
|
301
|
+
|
|
302
|
+
local choice
|
|
303
|
+
read -r -p "Select a device [1-${#ids[@]}]: " choice
|
|
304
|
+
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#ids[@]}" ]; then
|
|
305
|
+
log_warn "Invalid selection."
|
|
306
|
+
return 1
|
|
307
|
+
fi
|
|
308
|
+
echo "${ids[$((choice - 1))]}"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# No running devices — check for available-but-not-booted emulators and
|
|
312
|
+
# offer to launch one, matching the "check available simulators... then
|
|
313
|
+
# launch it" behavior the user asked for.
|
|
314
|
+
_flutter_offer_launch_emulator() {
|
|
315
|
+
local emulators_output
|
|
316
|
+
emulators_output=$(flutter emulators 2>/dev/null)
|
|
317
|
+
[ -n "$emulators_output" ] || return 1
|
|
318
|
+
|
|
319
|
+
echo "$emulators_output"
|
|
320
|
+
local launch_choice
|
|
321
|
+
read -r -p "Launch one of these emulators? Enter its id, or leave blank to skip: " launch_choice
|
|
322
|
+
[ -n "$launch_choice" ] || return 1
|
|
323
|
+
|
|
324
|
+
log_info "Launching emulator '$launch_choice'..."
|
|
325
|
+
flutter emulators --launch "$launch_choice"
|
|
326
|
+
log_info "Waiting for it to boot..."
|
|
327
|
+
sleep 5
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Only picks the device and records the launch command in
|
|
331
|
+
# XGEM_POST_INIT_LAUNCH_CMD — does NOT run `flutter run` itself. That's a
|
|
332
|
+
# long-running blocking process; running it here (before xgem's own
|
|
333
|
+
# template injection / .gitignore setup) means a Ctrl-C on it would kill
|
|
334
|
+
# this whole xgem process before that setup ever runs, same class of bug
|
|
335
|
+
# as create-vite's own --immediate flag caused for React. Device selection
|
|
336
|
+
# itself is just a quick menu prompt, so it's safe to do now.
|
|
337
|
+
_flutter_offer_run_on_device() {
|
|
338
|
+
local run_choice
|
|
339
|
+
read -r -p "Run the app now on a device/simulator? (Y/n): " run_choice
|
|
340
|
+
run_choice=${run_choice:-y}
|
|
341
|
+
[[ "$run_choice" == "y" || "$run_choice" == "Y" ]] || return 0
|
|
342
|
+
|
|
343
|
+
local device_id
|
|
344
|
+
device_id=$(_flutter_select_device)
|
|
345
|
+
if [ -n "$device_id" ]; then
|
|
346
|
+
XGEM_POST_INIT_LAUNCH_CMD=(flutter run -d "$device_id")
|
|
347
|
+
else
|
|
348
|
+
log_warn "No device selected — run 'flutter run' manually once one is available."
|
|
349
|
+
fi
|
|
350
|
+
}
|
package/lib/git.sh
CHANGED
|
@@ -24,9 +24,13 @@ cmd_git_cmt() {
|
|
|
24
24
|
die "Invalid choice. Operation aborted."
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
if git diff --cached --quiet; then
|
|
28
|
+
log_warn "Nothing staged to commit — checking whether there's anything already committed to sync."
|
|
29
|
+
else
|
|
30
|
+
log_info "Committing changes..."
|
|
31
|
+
if ! git commit -m "$commit_msg"; then
|
|
32
|
+
die "Commit failed. Aborting before pull/push."
|
|
33
|
+
fi
|
|
30
34
|
fi
|
|
31
35
|
|
|
32
36
|
local current_branch remote_name
|
|
@@ -38,14 +42,52 @@ cmd_git_cmt() {
|
|
|
38
42
|
fi
|
|
39
43
|
|
|
40
44
|
if [ -z "$remote_name" ]; then
|
|
41
|
-
log_warn "
|
|
45
|
+
log_warn "No remote configured — sync was skipped."
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# A pull needs something to pull FROM. If this branch has never been
|
|
50
|
+
# pushed before, there's no remote ref to pull, and `git pull --rebase`
|
|
51
|
+
# fails with "couldn't find remote ref <branch>" — which is not a
|
|
52
|
+
# connection problem or a conflict, just a brand-new branch. Check for
|
|
53
|
+
# that specific case first so it gets its own accurate message instead
|
|
54
|
+
# of being lumped in with real connectivity failures.
|
|
55
|
+
git ls-remote --exit-code --heads "$remote_name" "$current_branch" >/dev/null 2>&1
|
|
56
|
+
local ls_remote_status=$?
|
|
57
|
+
|
|
58
|
+
if [ "$ls_remote_status" -eq 2 ]; then
|
|
59
|
+
log_info "'$current_branch' doesn't exist on '$remote_name' yet — pushing to create it..."
|
|
60
|
+
if git push -u "$remote_name" "$current_branch"; then
|
|
61
|
+
log_success "Git workflow complete! Branch created and pushed."
|
|
62
|
+
else
|
|
63
|
+
die "Push operation failed."
|
|
64
|
+
fi
|
|
65
|
+
return 0
|
|
66
|
+
elif [ "$ls_remote_status" -ne 0 ]; then
|
|
67
|
+
log_error "Could not reach '$remote_name' to check for '$current_branch' — this looks like a real connection problem."
|
|
68
|
+
log_warn "Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push $remote_name $current_branch"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
local ahead_count
|
|
73
|
+
ahead_count=$(git rev-list --count "$remote_name/$current_branch..$current_branch" 2>/dev/null)
|
|
74
|
+
if [ "$ahead_count" = "0" ]; then
|
|
75
|
+
log_success "Already up to date with '$remote_name/$current_branch' — nothing to push."
|
|
42
76
|
return 0
|
|
43
77
|
fi
|
|
44
78
|
|
|
45
79
|
log_info "Pulling updates from remote '$remote_name' on '$current_branch' via rebase..."
|
|
46
80
|
if ! git pull --rebase "$remote_name" "$current_branch"; then
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
local git_dir
|
|
82
|
+
git_dir=$(git rev-parse --git-dir 2>/dev/null)
|
|
83
|
+
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ]; then
|
|
84
|
+
log_error "MERGE CONFLICT DETECTED!"
|
|
85
|
+
log_warn "Execution paused. Resolve conflicts to proceed."
|
|
86
|
+
else
|
|
87
|
+
log_error "Could not sync with '$remote_name' — this looks like a connection problem, not a merge conflict (see the git error above)."
|
|
88
|
+
log_warn "Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push $remote_name $current_branch"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
49
91
|
local open_editor
|
|
50
92
|
read -r -p "Do you want to open VS Code to resolve this now? (y/n): " open_editor
|
|
51
93
|
[[ "$open_editor" == "y" || "$open_editor" == "Y" ]] && code .
|
|
@@ -144,13 +186,69 @@ cmd_git_rm_branch() {
|
|
|
144
186
|
fi
|
|
145
187
|
}
|
|
146
188
|
|
|
147
|
-
#
|
|
189
|
+
# cmd_git_branch: lists local branches, lets the user pick one (or create a
|
|
190
|
+
# new one), checks it out, and remembers it as this repo's default so
|
|
191
|
+
# there's a quick way back to it later. `xgem git cmt` always operates on
|
|
192
|
+
# whatever's actually checked out (that's the only thing that's correct for
|
|
193
|
+
# a commit), so this command's job is the checkout + remembering, not
|
|
194
|
+
# changing how cmt picks its branch.
|
|
195
|
+
cmd_git_branch() {
|
|
196
|
+
local -a branches=()
|
|
197
|
+
while IFS= read -r line; do
|
|
198
|
+
[ -n "$line" ] && branches+=("$line")
|
|
199
|
+
done < <(git branch --format='%(refname:short)' 2>/dev/null)
|
|
200
|
+
|
|
201
|
+
[ ${#branches[@]} -gt 0 ] || die "No local branches found."
|
|
202
|
+
|
|
203
|
+
local current_branch
|
|
204
|
+
current_branch=$(git branch --show-current 2>/dev/null)
|
|
205
|
+
|
|
206
|
+
echo "Available branches:"
|
|
207
|
+
local i=1 b
|
|
208
|
+
for b in "${branches[@]}"; do
|
|
209
|
+
if [ "$b" = "$current_branch" ]; then
|
|
210
|
+
echo "$i) $b (current)"
|
|
211
|
+
else
|
|
212
|
+
echo "$i) $b"
|
|
213
|
+
fi
|
|
214
|
+
i=$((i + 1))
|
|
215
|
+
done
|
|
216
|
+
local create_option=$i
|
|
217
|
+
echo "$create_option) Create new branch"
|
|
218
|
+
|
|
219
|
+
local choice
|
|
220
|
+
read -r -p "Select [1-$create_option]: " choice
|
|
221
|
+
|
|
222
|
+
if [ "$choice" = "$create_option" ]; then
|
|
223
|
+
local new_branch
|
|
224
|
+
read -r -p "Enter new branch name: " new_branch
|
|
225
|
+
[ -n "$new_branch" ] || die "Branch name cannot be empty."
|
|
226
|
+
git checkout -b "$new_branch" || die "Could not create branch '$new_branch'."
|
|
227
|
+
git config --local xgem.default-branch "$new_branch"
|
|
228
|
+
log_success "Created and switched to '$new_branch', set as default for this repo."
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#branches[@]} ]; then
|
|
233
|
+
die "Invalid selection."
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
local selected="${branches[$((choice - 1))]}"
|
|
237
|
+
if [ "$selected" != "$current_branch" ]; then
|
|
238
|
+
git checkout "$selected" || die "Could not switch to branch '$selected'."
|
|
239
|
+
fi
|
|
240
|
+
git config --local xgem.default-branch "$selected"
|
|
241
|
+
log_success "Switched to '$selected' and set as default for this repo."
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# xgem git <cmt|init|rm-remote|rm-branch|branch> ...
|
|
148
245
|
cmd_git() {
|
|
149
246
|
case "$1" in
|
|
150
247
|
cmt) cmd_git_cmt "$2" ;;
|
|
151
248
|
init) cmd_git_init ;;
|
|
152
249
|
rm-remote) cmd_git_rm_remote ;;
|
|
153
250
|
rm-branch) cmd_git_rm_branch ;;
|
|
154
|
-
|
|
251
|
+
branch) cmd_git_branch ;;
|
|
252
|
+
*) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch|branch>" ;;
|
|
155
253
|
esac
|
|
156
254
|
}
|
package/lib/ios.sh
CHANGED
|
@@ -60,8 +60,13 @@ ios_deployment_target_for_config() {
|
|
|
60
60
|
|
|
61
61
|
if has_cmd xcodebuild; then
|
|
62
62
|
local value
|
|
63
|
+
# Anchored: newer Xcode also emits DEPLOYMENT_TARGET_SETTING_NAME =
|
|
64
|
+
# IPHONEOS_DEPLOYMENT_TARGET (a meta-setting whose *value* is that
|
|
65
|
+
# string), which an unanchored grep matches before the real
|
|
66
|
+
# "IPHONEOS_DEPLOYMENT_TARGET = 15.6" line — anchoring to the key
|
|
67
|
+
# position avoids grabbing that decoy.
|
|
63
68
|
value=$(xcodebuild -showBuildSettings "${xcb_args[@]}" -configuration "$config" 2>/dev/null \
|
|
64
|
-
| grep -m1 'IPHONEOS_DEPLOYMENT_TARGET' | awk '{print $NF}')
|
|
69
|
+
| grep -m1 -E '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET[[:space:]]*=' | awk '{print $NF}')
|
|
65
70
|
[ -n "$value" ] && { echo "$value"; return 0; }
|
|
66
71
|
fi
|
|
67
72
|
|
|
@@ -80,15 +85,36 @@ ios_pbxproj_current_max_target() {
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
# ios_required_spm_deployment_target [project_dir]
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
+
# Returns the highest .iOS(.vNN) platform floor declared across every
|
|
89
|
+
# resolved SwiftPM plugin, instead of hardcoding a guessed value.
|
|
90
|
+
#
|
|
91
|
+
# Primary source: ios/Flutter/ephemeral/Packages/.packages/<name>-<version>/,
|
|
92
|
+
# which `flutter pub get` itself populates with each SPM-enabled plugin as
|
|
93
|
+
# part of dependency resolution — this is the actual, already-resolved
|
|
94
|
+
# location Xcode/SPM uses, not a path we have to reconstruct. Scanning it
|
|
95
|
+
# directly sidesteps having to guess each plugin's on-disk manifest layout
|
|
96
|
+
# (which varies — not every plugin puts Package.swift at <root>/ios/).
|
|
97
|
+
#
|
|
98
|
+
# Falls back to parsing .dart_tool/package_config.json's rootUri entries
|
|
99
|
+
# (assuming the common <root>/ios/Package.swift convention) only if that
|
|
100
|
+
# ephemeral directory doesn't exist yet — e.g. before any pub get has run.
|
|
88
101
|
ios_required_spm_deployment_target() {
|
|
89
102
|
local project_dir=${1:-.}
|
|
90
|
-
local
|
|
103
|
+
local packages_dir="$project_dir/ios/Flutter/ephemeral/Packages/.packages"
|
|
104
|
+
|
|
105
|
+
if [ -d "$packages_dir" ]; then
|
|
106
|
+
# -L: per-plugin dirs here are frequently symlinks into pub-cache;
|
|
107
|
+
# without -L, find silently skips descending into them.
|
|
108
|
+
find -L "$packages_dir" -maxdepth 4 -name "Package.swift" -print0 2>/dev/null \
|
|
109
|
+
| xargs -0 grep -ohE '\.iOS\(\.v[0-9_]+\)|\.iOS\("[0-9.]+"\)' 2>/dev/null \
|
|
110
|
+
| grep -oE '[0-9]+(\.[0-9]+)?(_[0-9]+)?' | tr '_' '.' \
|
|
111
|
+
| sort -g | tail -1
|
|
112
|
+
return 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
log_debug "No resolved SwiftPM packages directory at $packages_dir yet; falling back to package_config.json."
|
|
91
116
|
|
|
117
|
+
local pkg_config="$project_dir/.dart_tool/package_config.json"
|
|
92
118
|
[ -f "$pkg_config" ] || { log_debug "No .dart_tool/package_config.json — run flutter pub get first."; return 1; }
|
|
93
119
|
has_cmd python3 || { log_debug "python3 not found; cannot parse package_config.json."; return 1; }
|
|
94
120
|
|
|
@@ -118,9 +144,12 @@ for pkg in data.get("packages", []):
|
|
|
118
144
|
except OSError:
|
|
119
145
|
continue
|
|
120
146
|
|
|
121
|
-
|
|
147
|
+
# Both SPM syntaxes: enum .iOS(.v15) and string .iOS("15.0") (the form
|
|
148
|
+
# Flutter's own generator uses).
|
|
149
|
+
for m in re.finditer(r'\.iOS\(\.v(\d+(?:_\d+)?)\)|\.iOS\("(\d+(?:\.\d+)?)"\)', content):
|
|
150
|
+
raw = m.group(1) or m.group(2)
|
|
122
151
|
try:
|
|
123
|
-
fv = float(
|
|
152
|
+
fv = float(raw.replace("_", "."))
|
|
124
153
|
except ValueError:
|
|
125
154
|
continue
|
|
126
155
|
max_target = max(max_target, fv)
|
|
@@ -164,21 +193,40 @@ ios_generated_package_swift_path() {
|
|
|
164
193
|
echo "$guess"
|
|
165
194
|
return 0
|
|
166
195
|
fi
|
|
167
|
-
|
|
196
|
+
# -L: plugin package directories under ephemeral/Packages/.packages are
|
|
197
|
+
# frequently symlinks into the pub-cache; a plain find silently skips
|
|
198
|
+
# symlinked directories instead of descending into them.
|
|
199
|
+
find -L "$project_dir/ios" -maxdepth 6 -type f -name "Package.swift" \
|
|
168
200
|
-path "*FlutterGeneratedPluginSwiftPackage*" 2>/dev/null | head -1
|
|
169
201
|
}
|
|
170
202
|
|
|
203
|
+
# SPM's SupportedPlatform.iOS accepts two syntaxes: the enum form .iOS(.v15)
|
|
204
|
+
# for the fixed known-version cases, and a string form .iOS("15.0") for
|
|
205
|
+
# arbitrary versions — Flutter's own code generator uses the *string* form
|
|
206
|
+
# for FlutterGeneratedPluginSwiftPackage, not the enum form. Matching only
|
|
207
|
+
# the enum form (as an earlier version of this function did) means the
|
|
208
|
+
# patch step silently never matches anything on a real generated package:
|
|
209
|
+
# sed finds no match, writes back an identical file, and "succeeds" while
|
|
210
|
+
# doing nothing.
|
|
211
|
+
_ios_extract_platform_version() {
|
|
212
|
+
grep -oE '\.iOS\(\.v[0-9_]+\)|\.iOS\("[0-9.]+"\)' | head -1 \
|
|
213
|
+
| grep -oE '[0-9]+(\.[0-9]+)?(_[0-9]+)?' | tr '_' '.'
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
ios_generated_package_target() {
|
|
172
217
|
local project_dir=${1:-.}
|
|
173
218
|
local pkg
|
|
174
219
|
pkg=$(ios_generated_package_swift_path "$project_dir")
|
|
175
220
|
[ -n "$pkg" ] && [ -f "$pkg" ] || return 1
|
|
176
|
-
|
|
221
|
+
_ios_extract_platform_version < "$pkg"
|
|
177
222
|
}
|
|
178
223
|
|
|
179
224
|
# Last-resort workaround for the still-open upstream desync bug: directly
|
|
180
225
|
# patch the generated package's platform declaration. Always logged loudly
|
|
181
226
|
# — this is a documented workaround for a known bug, never a silent hack.
|
|
227
|
+
# Writes the string form (.iOS("15.0")) since that's what Flutter's own
|
|
228
|
+
# generator uses in this file — matching its existing convention rather
|
|
229
|
+
# than introducing a different syntax.
|
|
182
230
|
ios_patch_generated_package_target() {
|
|
183
231
|
local new_target=$1
|
|
184
232
|
local project_dir=${2:-.}
|
|
@@ -190,10 +238,9 @@ ios_patch_generated_package_target() {
|
|
|
190
238
|
fi
|
|
191
239
|
|
|
192
240
|
log_warn "Working around known Flutter SPM bug (flutter/flutter#186804): the regenerated package still doesn't match your deployment target, so patching it directly."
|
|
193
|
-
local token=${new_target//./_}
|
|
194
241
|
local tmp
|
|
195
242
|
tmp=$(mktemp)
|
|
196
|
-
sed -E "s/\.iOS\(\.v[0-9_]+\)/.iOS(.
|
|
243
|
+
sed -E "s/\.iOS\(\.v[0-9_]+\)/.iOS(\"${new_target}\")/g; s/\.iOS\(\"[0-9.]+\"\)/.iOS(\"${new_target}\")/g" "$pkg" > "$tmp"
|
|
197
244
|
mv "$tmp" "$pkg"
|
|
198
245
|
log_success "Patched generated package platform to iOS $new_target."
|
|
199
246
|
}
|
|
@@ -205,14 +252,39 @@ ios_regenerate_generated_package() {
|
|
|
205
252
|
( cd "$project_dir" && flutter pub get )
|
|
206
253
|
}
|
|
207
254
|
|
|
255
|
+
# ios_parse_required_from_build_log <log_file>
|
|
256
|
+
# Statically scanning plugin manifests to predict the required deployment
|
|
257
|
+
# target has proven unreliable in practice (plugin authors structure SPM
|
|
258
|
+
# manifests differently, package dirs can be symlinks, etc.). Xcode itself
|
|
259
|
+
# always computes this correctly and states it plainly in its own error:
|
|
260
|
+
# "requires minimum platform version X for the iOS platform". Parsing that
|
|
261
|
+
# directly is the authoritative fallback when the static pre-check misses
|
|
262
|
+
# something.
|
|
263
|
+
ios_parse_required_from_build_log() {
|
|
264
|
+
local log_file=$1
|
|
265
|
+
grep -oE "requires minimum platform version [0-9]+(\.[0-9]+)?" "$log_file" 2>/dev/null \
|
|
266
|
+
| grep -oE '[0-9]+(\.[0-9]+)?' \
|
|
267
|
+
| sort -g | tail -1
|
|
268
|
+
}
|
|
269
|
+
|
|
208
270
|
ios_known_issue_check() {
|
|
209
|
-
log_warn "Known upstream issue (flutter/flutter#186804, #189422, #162072): FlutterGeneratedPluginSwiftPackage's deployment target can desync from your app's IPHONEOS_DEPLOYMENT_TARGET. Currently open — xgem detects and works around it automatically below."
|
|
271
|
+
log_warn "Known upstream issue (flutter/flutter#186804, #189422, #162072): FlutterGeneratedPluginSwiftPackage's deployment target can desync from your app's IPHONEOS_DEPLOYMENT_TARGET — independently, even when the app's own target is already high enough. Currently open — xgem detects and works around it automatically below."
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# _ios_ver_lt a b -> true if a < b, numeric comparison (not string equality,
|
|
275
|
+
# since "15" and "15.0" are the same version but different strings).
|
|
276
|
+
_ios_ver_lt() {
|
|
277
|
+
awk -v a="$1" -v b="$2" 'BEGIN{exit !(a<b)}'
|
|
210
278
|
}
|
|
211
279
|
|
|
212
280
|
# ios_reconcile_deployment_target [project_dir]
|
|
213
|
-
# The core "self-healing" step
|
|
214
|
-
#
|
|
215
|
-
#
|
|
281
|
+
# The core "self-healing" step. Checks TWO independent things against the
|
|
282
|
+
# required version — the app's own pbxproj target, AND
|
|
283
|
+
# FlutterGeneratedPluginSwiftPackage's own declared platform — because the
|
|
284
|
+
# upstream bug is exactly that these two can desync from each other. A
|
|
285
|
+
# project whose pbxproj is already at (say) 15.6 can still have a generated
|
|
286
|
+
# package stuck at 13.0; checking only the pbxproj target (as an earlier
|
|
287
|
+
# version of this function did) misses that case entirely.
|
|
216
288
|
# Returns 0 if no action was needed or the fix succeeded; 1 if it couldn't
|
|
217
289
|
# reconcile and the caller should stop before attempting a build.
|
|
218
290
|
ios_reconcile_deployment_target() {
|
|
@@ -220,46 +292,53 @@ ios_reconcile_deployment_target() {
|
|
|
220
292
|
|
|
221
293
|
ios_project_uses_spm "$project_dir" || { log_debug "Project does not use SwiftPM; skipping deployment-target reconciliation."; return 0; }
|
|
222
294
|
|
|
223
|
-
local required
|
|
295
|
+
local required
|
|
224
296
|
required=$(ios_required_spm_deployment_target "$project_dir")
|
|
225
|
-
current=$(ios_deployment_target_for_config "Release" "$project_dir")
|
|
226
|
-
|
|
227
297
|
if [ -z "$required" ]; then
|
|
228
298
|
log_debug "Could not determine a required deployment target from resolved SPM plugins; skipping reconciliation."
|
|
229
299
|
return 0
|
|
230
300
|
fi
|
|
231
|
-
|
|
232
|
-
|
|
301
|
+
|
|
302
|
+
ios_known_issue_check
|
|
303
|
+
|
|
304
|
+
local current generated
|
|
305
|
+
current=$(ios_deployment_target_for_config "Release" "$project_dir")
|
|
306
|
+
generated=$(ios_generated_package_target "$project_dir")
|
|
307
|
+
|
|
308
|
+
local pbxproj_needs_bump=0 generated_needs_fix=0
|
|
309
|
+
[ -n "$current" ] && _ios_ver_lt "$current" "$required" && pbxproj_needs_bump=1
|
|
310
|
+
if [ -z "$generated" ] || _ios_ver_lt "$generated" "$required"; then
|
|
311
|
+
generated_needs_fix=1
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
if [ "$pbxproj_needs_bump" = 0 ] && [ "$generated_needs_fix" = 0 ]; then
|
|
315
|
+
log_debug "Deployment target ($current) and generated package ($generated) already satisfy SwiftPM requirement ($required)."
|
|
233
316
|
return 0
|
|
234
317
|
fi
|
|
235
318
|
|
|
236
|
-
|
|
319
|
+
log_warn "Resolved SwiftPM plugins require iOS $required."
|
|
320
|
+
[ "$pbxproj_needs_bump" = 1 ] && echo " - project.pbxproj currently targets iOS $current"
|
|
321
|
+
[ "$generated_needs_fix" = 1 ] && echo " - FlutterGeneratedPluginSwiftPackage currently declares iOS ${generated:-unknown}"
|
|
322
|
+
echo "Plan:"
|
|
323
|
+
[ "$pbxproj_needs_bump" = 1 ] && echo " 1. Set IPHONEOS_DEPLOYMENT_TARGET = $required across every build configuration in project.pbxproj"
|
|
324
|
+
echo " 2. Clear ios/Flutter/ephemeral and re-run 'flutter pub get' to regenerate FlutterGeneratedPluginSwiftPackage"
|
|
325
|
+
echo " 3. Verify the regenerated package declares iOS $required; if it still doesn't (known upstream bug), patch it directly"
|
|
326
|
+
|
|
327
|
+
if ! confirm "Apply this fix?"; then
|
|
328
|
+
log_warn "Skipped. The build will likely fail SwiftPM resolution until this is reconciled manually."
|
|
329
|
+
return 1
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
[ "$pbxproj_needs_bump" = 1 ] && ios_pbxproj_patch_deployment_target "$required" "$project_dir"
|
|
333
|
+
ios_regenerate_generated_package "$project_dir"
|
|
237
334
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
echo " 2. Clear ios/Flutter/ephemeral and re-run 'flutter pub get' to regenerate FlutterGeneratedPluginSwiftPackage"
|
|
243
|
-
echo " 3. Verify the regenerated package declares iOS $required; if it still doesn't (known upstream bug), patch it directly"
|
|
244
|
-
|
|
245
|
-
if ! confirm "Apply this fix?"; then
|
|
246
|
-
log_warn "Skipped. The build will likely fail SwiftPM resolution until the deployment target is reconciled manually."
|
|
247
|
-
return 1
|
|
248
|
-
fi
|
|
249
|
-
|
|
250
|
-
ios_pbxproj_patch_deployment_target "$required" "$project_dir"
|
|
251
|
-
ios_regenerate_generated_package "$project_dir"
|
|
252
|
-
|
|
253
|
-
local regenerated
|
|
254
|
-
regenerated=$(ios_generated_package_target "$project_dir")
|
|
255
|
-
if [ "$regenerated" = "$required" ]; then
|
|
256
|
-
log_success "Generated package now correctly declares iOS $regenerated."
|
|
257
|
-
else
|
|
258
|
-
log_warn "Generated package declares '${regenerated:-unknown}' after regeneration, not $required."
|
|
259
|
-
ios_patch_generated_package_target "$required" "$project_dir"
|
|
260
|
-
fi
|
|
335
|
+
local regenerated
|
|
336
|
+
regenerated=$(ios_generated_package_target "$project_dir")
|
|
337
|
+
if [ -n "$regenerated" ] && ! _ios_ver_lt "$regenerated" "$required"; then
|
|
338
|
+
log_success "Generated package now correctly declares iOS $regenerated."
|
|
261
339
|
else
|
|
262
|
-
|
|
340
|
+
log_warn "Generated package declares '${regenerated:-unknown}' after regeneration, still below $required."
|
|
341
|
+
ios_patch_generated_package_target "$required" "$project_dir"
|
|
263
342
|
fi
|
|
264
343
|
|
|
265
344
|
return 0
|