@daemux/store-automator 0.10.80 → 0.10.82
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.
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "App Store & Google Play automation for Flutter apps",
|
|
8
|
-
"version": "0.10.
|
|
8
|
+
"version": "0.10.82"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "store-automator",
|
|
13
13
|
"source": "./plugins/store-automator",
|
|
14
14
|
"description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
|
|
15
|
-
"version": "0.10.
|
|
15
|
+
"version": "0.10.82",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ from __future__ import annotations
|
|
|
29
29
|
import base64
|
|
30
30
|
import json
|
|
31
31
|
import plistlib
|
|
32
|
+
import re
|
|
32
33
|
import subprocess
|
|
33
34
|
from pathlib import Path
|
|
34
35
|
|
|
@@ -36,7 +37,14 @@ from asc_common import get_json, request
|
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
PROFILE_PREFIX = "CI-"
|
|
39
|
-
|
|
40
|
+
|
|
41
|
+
# Xcode 16+ moved the canonical profile directory. Older Xcode releases used
|
|
42
|
+
# ~/Library/MobileDevice/Provisioning Profiles/. We write to BOTH so the same
|
|
43
|
+
# script works on macos-14 (Xcode 15) and macos-15 (Xcode 26).
|
|
44
|
+
_PROFILE_DIRS = [
|
|
45
|
+
Path.home() / "Library/Developer/Xcode/UserData/Provisioning Profiles",
|
|
46
|
+
Path.home() / "Library/MobileDevice/Provisioning Profiles",
|
|
47
|
+
]
|
|
40
48
|
|
|
41
49
|
# Product types that must be signed with a provisioning profile. App
|
|
42
50
|
# extensions share the common ``com.apple.product-type.app-extension`` prefix
|
|
@@ -125,55 +133,135 @@ def patch_project_signing(
|
|
|
125
133
|
) -> None:
|
|
126
134
|
"""Set manual signing + per-target profile name for every signable target.
|
|
127
135
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
The pbxproj stays in its original OpenStep format — Xcode is very
|
|
137
|
+
opinionated about that file and any conversion (to XML/JSON) risks
|
|
138
|
+
re-ordering keys or dropping comments. Instead, we edit the specific
|
|
139
|
+
``XCBuildConfiguration`` blocks by their UUID and inject/replace the
|
|
140
|
+
handful of keys we care about. Targets outside ``targets`` (most
|
|
141
|
+
importantly SwiftPM / resource-bundle configs) are untouched.
|
|
131
142
|
"""
|
|
132
|
-
|
|
143
|
+
pbx_path = Path(project_path) / "project.pbxproj"
|
|
144
|
+
text = pbx_path.read_text(encoding="utf-8")
|
|
145
|
+
|
|
133
146
|
for target in targets:
|
|
134
147
|
profile_name = f"{PROFILE_PREFIX}{target['bundle_id']}"
|
|
135
148
|
for cid in target["config_ids"]:
|
|
136
|
-
|
|
137
|
-
_set_build_setting(pbx, cid, "CODE_SIGN_IDENTITY", "Apple Distribution")
|
|
138
|
-
_set_build_setting(pbx, cid, "DEVELOPMENT_TEAM", team_id)
|
|
139
|
-
_set_build_setting(
|
|
140
|
-
pbx, cid, "PROVISIONING_PROFILE_SPECIFIER", profile_name
|
|
141
|
-
)
|
|
142
|
-
# Clear any legacy autogenerated PROVISIONING_PROFILE uuid that
|
|
143
|
-
# would otherwise override the specifier we just set.
|
|
144
|
-
_clear_build_setting(pbx, cid, "PROVISIONING_PROFILE")
|
|
149
|
+
text = _apply_signing_to_config(text, cid, team_id, profile_name)
|
|
145
150
|
print(
|
|
146
151
|
f"Patched {target['name']!r} -> {profile_name} "
|
|
147
152
|
f"(configs={len(target['config_ids'])})"
|
|
148
153
|
)
|
|
149
154
|
|
|
155
|
+
pbx_path.write_text(text, encoding="utf-8")
|
|
156
|
+
# Sanity check — plutil -lint rejects malformed output so CI fails
|
|
157
|
+
# fast before ``xcodebuild`` tries (and mangles) the file.
|
|
158
|
+
subprocess.check_call(["plutil", "-lint", str(pbx_path)])
|
|
150
159
|
|
|
151
|
-
def _keypath(config_id: str, key: str) -> str:
|
|
152
|
-
return f"objects.{config_id}.buildSettings.{key}"
|
|
153
160
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
161
|
+
def _apply_signing_to_config(
|
|
162
|
+
text: str, config_id: str, team_id: str, profile_name: str
|
|
163
|
+
) -> str:
|
|
164
|
+
"""Return ``text`` with the given XCBuildConfiguration's buildSettings
|
|
165
|
+
patched for manual signing. Raises ``SystemExit`` if the config block
|
|
166
|
+
can't be located (indicates a pbxproj format we don't understand).
|
|
167
|
+
"""
|
|
168
|
+
start = text.find(f"\t\t{config_id} ")
|
|
169
|
+
if start < 0:
|
|
170
|
+
start = text.find(f"\t\t{config_id}\t")
|
|
171
|
+
if start < 0:
|
|
172
|
+
start = text.find(f"{config_id} = {{")
|
|
173
|
+
if start < 0:
|
|
174
|
+
raise SystemExit(
|
|
175
|
+
f"patch_project_signing: config {config_id} not found in pbxproj"
|
|
176
|
+
)
|
|
177
|
+
settings_key = "buildSettings = {"
|
|
178
|
+
s_idx = text.find(settings_key, start)
|
|
179
|
+
if s_idx < 0:
|
|
180
|
+
raise SystemExit(
|
|
181
|
+
f"patch_project_signing: buildSettings not found for {config_id}"
|
|
168
182
|
)
|
|
183
|
+
# Find matching closing '};' for buildSettings — brace-balance forward.
|
|
184
|
+
depth = 0
|
|
185
|
+
i = s_idx + len(settings_key) - 1 # position at the opening '{'
|
|
186
|
+
end = -1
|
|
187
|
+
while i < len(text):
|
|
188
|
+
ch = text[i]
|
|
189
|
+
if ch == "{":
|
|
190
|
+
depth += 1
|
|
191
|
+
elif ch == "}":
|
|
192
|
+
depth -= 1
|
|
193
|
+
if depth == 0:
|
|
194
|
+
end = i
|
|
195
|
+
break
|
|
196
|
+
i += 1
|
|
197
|
+
if end < 0:
|
|
198
|
+
raise SystemExit(
|
|
199
|
+
f"patch_project_signing: unbalanced buildSettings for {config_id}"
|
|
200
|
+
)
|
|
201
|
+
inner = text[s_idx + len(settings_key): end]
|
|
202
|
+
patched = _patch_inner_settings(inner, team_id, profile_name)
|
|
203
|
+
return text[: s_idx + len(settings_key)] + patched + text[end:]
|
|
169
204
|
|
|
170
205
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
206
|
+
_SIGNING_KEYS = {
|
|
207
|
+
"CODE_SIGN_STYLE": "Manual",
|
|
208
|
+
"CODE_SIGN_IDENTITY": '"Apple Distribution"',
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_STRIP_KEYS = ("PROVISIONING_PROFILE",)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _patch_inner_settings(
|
|
215
|
+
inner: str, team_id: str, profile_name: str
|
|
216
|
+
) -> str:
|
|
217
|
+
"""Rewrite build setting key/value lines inside one buildSettings block."""
|
|
218
|
+
# Always-set values
|
|
219
|
+
values = dict(_SIGNING_KEYS)
|
|
220
|
+
values["DEVELOPMENT_TEAM"] = team_id
|
|
221
|
+
values["PROVISIONING_PROFILE_SPECIFIER"] = f'"{profile_name}"'
|
|
222
|
+
|
|
223
|
+
lines = inner.splitlines(keepends=True)
|
|
224
|
+
emitted_keys: set[str] = set()
|
|
225
|
+
out: list[str] = []
|
|
226
|
+
# Each setting line looks like (whitespace-prefixed):
|
|
227
|
+
# KEY = VALUE;
|
|
228
|
+
# We match leading indent, the key, `= `, the rest.
|
|
229
|
+
pattern = re.compile(r"^(\s*)([A-Z_][A-Z0-9_]*)\s*=\s*(.+);\s*$")
|
|
230
|
+
|
|
231
|
+
for line in lines:
|
|
232
|
+
m = pattern.match(line)
|
|
233
|
+
if not m:
|
|
234
|
+
out.append(line)
|
|
235
|
+
continue
|
|
236
|
+
indent, key, _old_value = m.group(1), m.group(2), m.group(3)
|
|
237
|
+
if key in _STRIP_KEYS:
|
|
238
|
+
# Drop these keys entirely.
|
|
239
|
+
continue
|
|
240
|
+
if key in values:
|
|
241
|
+
out.append(f"{indent}{key} = {values[key]};\n")
|
|
242
|
+
emitted_keys.add(key)
|
|
243
|
+
continue
|
|
244
|
+
out.append(line)
|
|
245
|
+
|
|
246
|
+
# Any key we wanted to set but didn't find — append just before close.
|
|
247
|
+
missing = [k for k in values if k not in emitted_keys]
|
|
248
|
+
if missing:
|
|
249
|
+
# Determine indent from the first KEY= line we can find, else use
|
|
250
|
+
# two tabs (the pbxproj default).
|
|
251
|
+
indent = "\t\t\t\t"
|
|
252
|
+
for line in lines:
|
|
253
|
+
m = pattern.match(line)
|
|
254
|
+
if m:
|
|
255
|
+
indent = m.group(1)
|
|
256
|
+
break
|
|
257
|
+
tail = out[-1] if out else ""
|
|
258
|
+
# Ensure we inject before the trailing whitespace on the last line.
|
|
259
|
+
new_lines = [f"{indent}{k} = {values[k]};\n" for k in missing]
|
|
260
|
+
if tail.strip() == "":
|
|
261
|
+
out = out[:-1] + new_lines + [tail]
|
|
262
|
+
else:
|
|
263
|
+
out = out + new_lines
|
|
264
|
+
return "".join(out)
|
|
177
265
|
|
|
178
266
|
|
|
179
267
|
# --------------------------------------------------------------------------- #
|
|
@@ -261,15 +349,26 @@ def create_profile(
|
|
|
261
349
|
|
|
262
350
|
|
|
263
351
|
def install_profile(profile_der: bytes) -> str:
|
|
264
|
-
|
|
265
|
-
|
|
352
|
+
primary = _PROFILE_DIRS[0]
|
|
353
|
+
primary.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
tmp_path = primary / "tmp.mobileprovision"
|
|
266
355
|
tmp_path.write_bytes(profile_der)
|
|
267
356
|
decoded = subprocess.check_output(
|
|
268
357
|
["security", "cms", "-D", "-i", str(tmp_path)]
|
|
269
358
|
)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
359
|
+
plist = plistlib.loads(decoded)
|
|
360
|
+
uuid = plist["UUID"]
|
|
361
|
+
team_ids = plist.get("TeamIdentifier") or []
|
|
362
|
+
profile_name = plist.get("Name") or "<unknown>"
|
|
363
|
+
final_name = f"{uuid}.mobileprovision"
|
|
364
|
+
for directory in _PROFILE_DIRS:
|
|
365
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
(directory / final_name).write_bytes(profile_der)
|
|
367
|
+
tmp_path.unlink(missing_ok=True)
|
|
368
|
+
print(
|
|
369
|
+
f" profile {profile_name!r}: uuid={uuid} team={team_ids} "
|
|
370
|
+
f"dirs={[str(d) for d in _PROFILE_DIRS]}"
|
|
371
|
+
)
|
|
273
372
|
return uuid
|
|
274
373
|
|
|
275
374
|
|