xgem-cli 2.0.0-alpha.1 → 2.0.0-alpha.12
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 +53 -31
- package/bin/xgem +21 -14
- package/bin/xgem.js +235 -0
- package/lib/create.sh +334 -0
- package/lib/doctor.sh +13 -2
- package/lib/flutter.sh +163 -18
- package/lib/git.sh +157 -23
- package/lib/ios.sh +126 -47
- package/lib/scaffold.sh +24 -5
- 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 +279 -0
- package/lib-win/logger.js +35 -0
- package/lib-win/scaffold.js +102 -0
- package/lib-win/utils.js +67 -0
- package/package.json +8 -7
- package/templates/node/build.sh.tmpl +9 -1
- package/templates/node/hard-clean.sh.tmpl +34 -2
- package/templates/node/lint.sh.tmpl +10 -0
- package/templates/node/start.sh.tmpl +9 -1
- package/templates/node/test.sh.tmpl +10 -0
- package/templates/webframework/build.sh.tmpl +10 -1
- package/templates/webframework/dev.sh.tmpl +9 -1
- package/templates/webframework/hard-clean.sh.tmpl +35 -2
- package/templates/webframework/lint.sh.tmpl +10 -0
- package/templates/webframework/test.sh.tmpl +10 -0
- 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 +10 -0
- package/templates-win/node/hard-clean.mjs.tmpl +28 -0
- package/templates-win/node/lint.mjs.tmpl +10 -0
- package/templates-win/node/start.mjs.tmpl +10 -0
- package/templates-win/node/test.mjs.tmpl +10 -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 +10 -0
- package/templates-win/webframework/dev.mjs.tmpl +10 -0
- package/templates-win/webframework/hard-clean.mjs.tmpl +32 -0
- package/templates-win/webframework/lint.mjs.tmpl +10 -0
- package/templates-win/webframework/test.mjs.tmpl +10 -0
- package/scripts/check-platform.js +0 -17
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 .
|
|
@@ -60,31 +102,67 @@ cmd_git_cmt() {
|
|
|
60
102
|
fi
|
|
61
103
|
}
|
|
62
104
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
105
|
+
_cmd_git_init_manual_remote() {
|
|
106
|
+
local remote_name=$1
|
|
107
|
+
local remote_url user_remote_name
|
|
108
|
+
read -r -p "Enter remote repository URL (or leave blank to skip): " remote_url
|
|
109
|
+
[ -n "$remote_url" ] || return 0
|
|
66
110
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if [ -n "$remote_url" ]; then
|
|
70
|
-
read -r -p "Enter remote name (default: origin): " user_remote_name
|
|
71
|
-
[ -n "$user_remote_name" ] && remote_name="$user_remote_name"
|
|
111
|
+
read -r -p "Enter remote name (default: $remote_name): " user_remote_name
|
|
112
|
+
[ -n "$user_remote_name" ] && remote_name="$user_remote_name"
|
|
72
113
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
fi
|
|
114
|
+
if git remote | grep -q "^$remote_name$"; then
|
|
115
|
+
git remote set-url "$remote_name" "$remote_url"
|
|
116
|
+
log_success "Remote '$remote_name' already existed. URL updated."
|
|
117
|
+
else
|
|
118
|
+
git remote add "$remote_name" "$remote_url"
|
|
119
|
+
log_success "Remote '$remote_name' successfully added."
|
|
80
120
|
fi
|
|
121
|
+
}
|
|
81
122
|
|
|
123
|
+
cmd_git_init() {
|
|
124
|
+
log_info "Initializing local Git repository..."
|
|
125
|
+
git init
|
|
126
|
+
|
|
127
|
+
local branch_name
|
|
82
128
|
read -r -p "Enter branch name (default: main): " branch_name
|
|
83
129
|
branch_name=${branch_name:-main}
|
|
84
130
|
git branch -M "$branch_name"
|
|
85
131
|
|
|
86
132
|
git add .
|
|
87
133
|
git commit -m "initial changes" 2>/dev/null
|
|
134
|
+
|
|
135
|
+
# Real GitHub repo creation, not just wiring a remote to a URL you
|
|
136
|
+
# already had to go create by hand — this is the whole point of
|
|
137
|
+
# `xgem git init` over plain `git init`. Falls back to the manual-URL
|
|
138
|
+
# flow if `gh` isn't installed/authenticated.
|
|
139
|
+
if has_cmd gh && gh auth status >/dev/null 2>&1; then
|
|
140
|
+
local create_choice
|
|
141
|
+
read -r -p "Create a new GitHub repository for this project right now? (Y/n): " create_choice
|
|
142
|
+
create_choice=${create_choice:-y}
|
|
143
|
+
if [[ "$create_choice" == "y" || "$create_choice" == "Y" ]]; then
|
|
144
|
+
local repo_name visibility vis_flag
|
|
145
|
+
read -r -p "Repository name (default: $(basename "$PWD")): " repo_name
|
|
146
|
+
repo_name=${repo_name:-$(basename "$PWD")}
|
|
147
|
+
read -r -p "Public or private? [public/private] (default: private): " visibility
|
|
148
|
+
visibility=${visibility:-private}
|
|
149
|
+
vis_flag="--private"
|
|
150
|
+
[[ "$visibility" == pub* || "$visibility" == Pub* ]] && vis_flag="--public"
|
|
151
|
+
|
|
152
|
+
if gh repo create "$repo_name" "$vis_flag" --source=. --remote=origin --push; then
|
|
153
|
+
log_success "Created GitHub repo '$repo_name' and pushed '$branch_name' to it."
|
|
154
|
+
else
|
|
155
|
+
log_error "gh repo create failed — falling back to manual remote setup."
|
|
156
|
+
_cmd_git_init_manual_remote origin
|
|
157
|
+
fi
|
|
158
|
+
log_success "Local baseline configuration setup completed."
|
|
159
|
+
return 0
|
|
160
|
+
fi
|
|
161
|
+
elif has_cmd gh; then
|
|
162
|
+
log_warn "GitHub CLI (gh) is installed but not authenticated — run 'gh auth login' to enable one-step repo creation next time."
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
_cmd_git_init_manual_remote origin
|
|
88
166
|
log_success "Local baseline configuration setup completed."
|
|
89
167
|
}
|
|
90
168
|
|
|
@@ -144,13 +222,69 @@ cmd_git_rm_branch() {
|
|
|
144
222
|
fi
|
|
145
223
|
}
|
|
146
224
|
|
|
147
|
-
#
|
|
225
|
+
# cmd_git_branch: lists local branches, lets the user pick one (or create a
|
|
226
|
+
# new one), checks it out, and remembers it as this repo's default so
|
|
227
|
+
# there's a quick way back to it later. `xgem git cmt` always operates on
|
|
228
|
+
# whatever's actually checked out (that's the only thing that's correct for
|
|
229
|
+
# a commit), so this command's job is the checkout + remembering, not
|
|
230
|
+
# changing how cmt picks its branch.
|
|
231
|
+
cmd_git_branch() {
|
|
232
|
+
local -a branches=()
|
|
233
|
+
while IFS= read -r line; do
|
|
234
|
+
[ -n "$line" ] && branches+=("$line")
|
|
235
|
+
done < <(git branch --format='%(refname:short)' 2>/dev/null)
|
|
236
|
+
|
|
237
|
+
[ ${#branches[@]} -gt 0 ] || die "No local branches found."
|
|
238
|
+
|
|
239
|
+
local current_branch
|
|
240
|
+
current_branch=$(git branch --show-current 2>/dev/null)
|
|
241
|
+
|
|
242
|
+
echo "Available branches:"
|
|
243
|
+
local i=1 b
|
|
244
|
+
for b in "${branches[@]}"; do
|
|
245
|
+
if [ "$b" = "$current_branch" ]; then
|
|
246
|
+
echo "$i) $b (current)"
|
|
247
|
+
else
|
|
248
|
+
echo "$i) $b"
|
|
249
|
+
fi
|
|
250
|
+
i=$((i + 1))
|
|
251
|
+
done
|
|
252
|
+
local create_option=$i
|
|
253
|
+
echo "$create_option) Create new branch"
|
|
254
|
+
|
|
255
|
+
local choice
|
|
256
|
+
read -r -p "Select [1-$create_option]: " choice
|
|
257
|
+
|
|
258
|
+
if [ "$choice" = "$create_option" ]; then
|
|
259
|
+
local new_branch
|
|
260
|
+
read -r -p "Enter new branch name: " new_branch
|
|
261
|
+
[ -n "$new_branch" ] || die "Branch name cannot be empty."
|
|
262
|
+
git checkout -b "$new_branch" || die "Could not create branch '$new_branch'."
|
|
263
|
+
git config --local xgem.default-branch "$new_branch"
|
|
264
|
+
log_success "Created and switched to '$new_branch', set as default for this repo."
|
|
265
|
+
return 0
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#branches[@]} ]; then
|
|
269
|
+
die "Invalid selection."
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
local selected="${branches[$((choice - 1))]}"
|
|
273
|
+
if [ "$selected" != "$current_branch" ]; then
|
|
274
|
+
git checkout "$selected" || die "Could not switch to branch '$selected'."
|
|
275
|
+
fi
|
|
276
|
+
git config --local xgem.default-branch "$selected"
|
|
277
|
+
log_success "Switched to '$selected' and set as default for this repo."
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# xgem git <cmt|init|rm-remote|rm-branch|branch> ...
|
|
148
281
|
cmd_git() {
|
|
149
282
|
case "$1" in
|
|
150
283
|
cmt) cmd_git_cmt "$2" ;;
|
|
151
284
|
init) cmd_git_init ;;
|
|
152
285
|
rm-remote) cmd_git_rm_remote ;;
|
|
153
286
|
rm-branch) cmd_git_rm_branch ;;
|
|
154
|
-
|
|
287
|
+
branch) cmd_git_branch ;;
|
|
288
|
+
*) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch|branch>" ;;
|
|
155
289
|
esac
|
|
156
290
|
}
|
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
|
package/lib/scaffold.sh
CHANGED
|
@@ -11,14 +11,33 @@
|
|
|
11
11
|
#
|
|
12
12
|
# Depends on lib/logger.sh and XGEM_HOME (set by bin/xgem).
|
|
13
13
|
|
|
14
|
-
ALL_FRAMEWORKS=(flutter node python react vue angular go rust docker swift)
|
|
14
|
+
ALL_FRAMEWORKS=(flutter node python react vue angular next go rust docker swift)
|
|
15
|
+
|
|
16
|
+
# _package_json_has_script <script> — checks the CURRENT directory's
|
|
17
|
+
# package.json, not a guess. Used to only offer lint/test automation when
|
|
18
|
+
# the project actually has those scripts, instead of always generating
|
|
19
|
+
# scripts that fail with "Missing script" on projects that don't.
|
|
20
|
+
_package_json_has_script() {
|
|
21
|
+
[ -f package.json ] || return 1
|
|
22
|
+
grep -qE "\"$1\"[[:space:]]*:" package.json 2>/dev/null
|
|
23
|
+
}
|
|
15
24
|
|
|
16
25
|
framework_scripts() {
|
|
17
26
|
case "$1" in
|
|
18
27
|
flutter) echo "hard-clean build build-runner" ;;
|
|
19
|
-
node)
|
|
28
|
+
node)
|
|
29
|
+
local scripts="hard-clean build start"
|
|
30
|
+
_package_json_has_script lint && scripts="$scripts lint"
|
|
31
|
+
_package_json_has_script test && scripts="$scripts test"
|
|
32
|
+
echo "$scripts"
|
|
33
|
+
;;
|
|
20
34
|
python) echo "hard-clean install" ;;
|
|
21
|
-
react|vue|angular)
|
|
35
|
+
react|vue|angular|next)
|
|
36
|
+
local scripts="hard-clean build dev"
|
|
37
|
+
_package_json_has_script lint && scripts="$scripts lint"
|
|
38
|
+
_package_json_has_script test && scripts="$scripts test"
|
|
39
|
+
echo "$scripts"
|
|
40
|
+
;;
|
|
22
41
|
go) echo "hard-clean build" ;;
|
|
23
42
|
rust) echo "hard-clean build" ;;
|
|
24
43
|
docker) echo "hard-clean build-up" ;;
|
|
@@ -30,8 +49,8 @@ framework_scripts() {
|
|
|
30
49
|
_scaffold_template_dir() {
|
|
31
50
|
local framework=$1
|
|
32
51
|
case "$framework" in
|
|
33
|
-
react|vue|angular) echo "$XGEM_HOME/templates/webframework" ;;
|
|
34
|
-
*)
|
|
52
|
+
react|vue|angular|next) echo "$XGEM_HOME/templates/webframework" ;;
|
|
53
|
+
*) echo "$XGEM_HOME/templates/$framework" ;;
|
|
35
54
|
esac
|
|
36
55
|
}
|
|
37
56
|
|
package/lib/version.sh
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// xgem Windows engine doctor — mirrors lib/doctor.sh's general report.
|
|
2
|
+
// No iOS/SwiftPM engine exists here (see flutter.js) since Xcode has no
|
|
3
|
+
// Windows equivalent; `doctor ios` explains that instead of pretending.
|
|
4
|
+
|
|
5
|
+
const { getVersion, detectArch } = require('./utils');
|
|
6
|
+
const { paint } = require('./logger');
|
|
7
|
+
|
|
8
|
+
function printGeneral() {
|
|
9
|
+
console.log(paint('cyan', '=== xgem doctor (Windows) ==='));
|
|
10
|
+
console.log(`OS: win32`);
|
|
11
|
+
console.log(`Arch: ${detectArch()}`);
|
|
12
|
+
console.log(`git: ${getVersion('git') || 'not found'}`);
|
|
13
|
+
console.log(`flutter: ${getVersion('flutter') || 'not found'}`);
|
|
14
|
+
console.log(`dart: ${getVersion('dart') || 'not found'}`);
|
|
15
|
+
console.log(`node: ${getVersion('node') || 'not found'}`);
|
|
16
|
+
console.log(`python: ${getVersion('python') || getVersion('python3') || 'not found'}`);
|
|
17
|
+
console.log(`go: ${getVersion('go', ['version']) || 'not found'}`);
|
|
18
|
+
console.log(`cargo: ${getVersion('cargo') || 'not found'}`);
|
|
19
|
+
console.log(`docker: ${getVersion('docker') || 'not found'}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printIos() {
|
|
23
|
+
console.log(paint('cyan', '=== xgem doctor ios (Windows) ==='));
|
|
24
|
+
console.log(paint('yellow', 'iOS/macOS builds are not available on Windows — Xcode has no Windows equivalent.'));
|
|
25
|
+
console.log('Use WSL2 with a Mac, or a real Mac, for Flutter iOS builds.');
|
|
26
|
+
const flutterVersion = getVersion('flutter');
|
|
27
|
+
if (flutterVersion) {
|
|
28
|
+
console.log(`\nflutter is installed here (${flutterVersion}) and can still build Android/Windows targets:`);
|
|
29
|
+
console.log(' xgem run flutter build (choose APK or Windows)');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cmdDoctor(target) {
|
|
34
|
+
if (target === 'ios') {
|
|
35
|
+
printIos();
|
|
36
|
+
} else if (!target) {
|
|
37
|
+
printGeneral();
|
|
38
|
+
} else {
|
|
39
|
+
const { die } = require('./logger');
|
|
40
|
+
die(`Unknown doctor target '${target}'. Usage: xgem doctor [ios]`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { cmdDoctor, printGeneral, printIos };
|