@mpgd/cli 0.61.1 → 0.62.0
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/dist/capacitor-shell-starter.js +18 -1
- package/dist/crash-reporting-starter.d.ts +5 -3
- package/dist/crash-reporting-starter.js +10 -7
- package/dist/crash-symbols-upload.d.ts +67 -0
- package/dist/crash-symbols-upload.js +232 -0
- package/dist/index.js +48 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-symbols.d.ts +95 -0
- package/dist/native-build-symbols.js +261 -0
- package/dist/native-build-target.js +700 -428
- package/dist/native-deploy-run.d.ts +2 -0
- package/dist/native-deploy-run.js +60 -10
- package/dist/native-deploy-submission.d.ts +8 -0
- package/dist/native-deploy-submission.js +50 -8
- package/dist/pnpm-build-approvals.d.ts +59 -0
- package/dist/pnpm-build-approvals.js +157 -0
- package/dist/release-state.d.ts +69 -0
- package/dist/release-state.js +150 -5
- package/dist/release-workspace.d.ts +3 -0
- package/dist/release-workspace.js +44 -1
- package/package.json +12 -12
- package/templates/phaser-game/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { closeSync, existsSync, lstatSync, openSync, readdirSync, readSync, statSync, } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Validate a symbols record read from a build status file. `isPath` checks
|
|
5
|
+
* each recorded path (game-relative, inside the build's output root).
|
|
6
|
+
*/
|
|
7
|
+
export function assertNativeBuildSymbols(value, isPath) {
|
|
8
|
+
const isRecord = (entry) => typeof entry === 'object' && entry !== null && !Array.isArray(entry);
|
|
9
|
+
const pathField = (entry) => typeof entry === 'string' && isPath(entry);
|
|
10
|
+
if (!isRecord(value)
|
|
11
|
+
|| Object.keys(value).some((key) => key !== 'r8Mapping' && key !== 'crashlytics')
|
|
12
|
+
|| (value.r8Mapping === undefined) === (value.crashlytics === undefined)
|
|
13
|
+
|| (value.r8Mapping !== undefined && !pathField(value.r8Mapping))) {
|
|
14
|
+
throw new Error('Native build symbols are malformed.');
|
|
15
|
+
}
|
|
16
|
+
const crashlytics = value.crashlytics;
|
|
17
|
+
if (crashlytics === undefined) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!isRecord(crashlytics)
|
|
21
|
+
|| Object.keys(crashlytics).some((key) => !['dsymArchive', 'dsyms', 'uploadTool', 'googleServiceInfo'].includes(key))
|
|
22
|
+
|| !pathField(crashlytics.dsymArchive) || !pathField(crashlytics.uploadTool)
|
|
23
|
+
|| !pathField(crashlytics.googleServiceInfo)
|
|
24
|
+
|| !isDsymIdentityList(crashlytics.dsyms)) {
|
|
25
|
+
throw new Error('Native build symbols are malformed.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** A non-empty, sorted list of dSYM bundles with their UUIDs. */
|
|
29
|
+
export function isDsymIdentityList(value) {
|
|
30
|
+
return Array.isArray(value) && value.length > 0 && value.length <= 256
|
|
31
|
+
&& value.every((entry) => typeof entry === 'object' && entry !== null
|
|
32
|
+
&& !Array.isArray(entry)
|
|
33
|
+
&& Object.keys(entry).every((key) => key === 'bundle' || key === 'uuids')
|
|
34
|
+
&& typeof entry.bundle === 'string'
|
|
35
|
+
&& isDsymBundleName(entry.bundle)
|
|
36
|
+
&& Array.isArray(entry.uuids)
|
|
37
|
+
&& entry.uuids.length > 0
|
|
38
|
+
&& entry.uuids.length <= 32
|
|
39
|
+
&& entry.uuids.every(isDsymUuid));
|
|
40
|
+
}
|
|
41
|
+
/** A dSYM bundle file name: no path separator or control character, not hidden. */
|
|
42
|
+
export function isDsymBundleName(name) {
|
|
43
|
+
return name.length <= 255 && /^[^./\\\u0000-\u001f\u007f][^/\\\u0000-\u001f\u007f]*\.dSYM$/u.test(name);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A game-relative path under the target's native build output root
|
|
47
|
+
* (`release-output/native/<target>/`), without empty or `..` segments.
|
|
48
|
+
*/
|
|
49
|
+
export function isNativeOutputPath(file, target) {
|
|
50
|
+
return file.startsWith(`release-output/native/${target}/`) && !path.isAbsolute(file)
|
|
51
|
+
&& !file.split(/[\\/]/u).some((segment) => segment === '' || segment === '..');
|
|
52
|
+
}
|
|
53
|
+
/** Where AGP writes the release variant's R8 mapping file. */
|
|
54
|
+
export const androidReleaseMappingPath = 'app/build/outputs/mapping/release/mapping.txt';
|
|
55
|
+
/** File names inside the build's `symbols/` directory. */
|
|
56
|
+
export const nativeSymbolFileNames = Object.freeze({
|
|
57
|
+
r8Mapping: 'mapping.txt',
|
|
58
|
+
dsymArchive: 'dSYMs.zip',
|
|
59
|
+
uploadTool: 'upload-symbols',
|
|
60
|
+
googleServiceInfo: 'GoogleService-Info.plist',
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Where a Crashlytics-enabled iOS archive puts its Swift Package checkouts
|
|
64
|
+
* (`xcodebuild -clonedSourcePackagesDirPath`), so `upload-symbols` is found
|
|
65
|
+
* without searching DerivedData.
|
|
66
|
+
*/
|
|
67
|
+
export const iosCrashlyticsSourcePackagesDirectory = '.mpgd-source-packages';
|
|
68
|
+
/**
|
|
69
|
+
* `upload-symbols` ships next to the Crashlytics `run` script: in the Swift
|
|
70
|
+
* Package checkout (`SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/`,
|
|
71
|
+
* the path Firebase's run script uses) or in CocoaPods
|
|
72
|
+
* (`Pods/FirebaseCrashlytics/`, the path Firebase's manual-upload command uses).
|
|
73
|
+
*/
|
|
74
|
+
export function crashlyticsUploadToolCandidates(input) {
|
|
75
|
+
return [
|
|
76
|
+
path.join(input.sourcePackages, 'checkouts/firebase-ios-sdk/Crashlytics/upload-symbols'),
|
|
77
|
+
path.join(input.iosProject, 'App/Pods/FirebaseCrashlytics/upload-symbols'),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Copy what Crashlytics needs out of a staged archive build before the stage
|
|
82
|
+
* is removed. Refused when the archive has no dSYM (Firebase: "Set Debug
|
|
83
|
+
* Information Format to DWARF with dSYM File"), when the checkout has no
|
|
84
|
+
* `upload-symbols`, or when the app does not bundle `GoogleService-Info.plist`.
|
|
85
|
+
*/
|
|
86
|
+
export function collectIosCrashlyticsSymbols(input) {
|
|
87
|
+
const dsymFolder = path.join(input.archive, 'dSYMs');
|
|
88
|
+
const dsyms = readArchiveDsyms(dsymFolder);
|
|
89
|
+
if (dsyms.length === 0) {
|
|
90
|
+
throw new Error('Crash reporting is on, but the iOS archive has no dSYM; set Debug Information Format to '
|
|
91
|
+
+ '"DWARF with dSYM File" for the Release configuration.');
|
|
92
|
+
}
|
|
93
|
+
const tool = crashlyticsUploadToolCandidates(input)
|
|
94
|
+
.find((candidate) => existsSync(candidate) && lstatSync(candidate).isFile());
|
|
95
|
+
if (tool === undefined) {
|
|
96
|
+
throw new Error('Crash reporting is on, but the iOS build has no Firebase Crashlytics upload-symbols in its '
|
|
97
|
+
+ 'Swift Package (firebase-ios-sdk/Crashlytics) or CocoaPods (FirebaseCrashlytics) checkout.');
|
|
98
|
+
}
|
|
99
|
+
const plist = path.join(archivedApp(input.archive), 'GoogleService-Info.plist');
|
|
100
|
+
if (!existsSync(plist) || !lstatSync(plist).isFile()) {
|
|
101
|
+
throw new Error('Crash reporting is on, but the archived iOS app does not bundle GoogleService-Info.plist.');
|
|
102
|
+
}
|
|
103
|
+
const output = {
|
|
104
|
+
dsymArchive: path.join(input.outputDirectory, nativeSymbolFileNames.dsymArchive),
|
|
105
|
+
uploadTool: path.join(input.outputDirectory, nativeSymbolFileNames.uploadTool),
|
|
106
|
+
googleServiceInfo: path.join(input.outputDirectory, nativeSymbolFileNames.googleServiceInfo),
|
|
107
|
+
};
|
|
108
|
+
input.zipDirectory(dsymFolder, output.dsymArchive);
|
|
109
|
+
if (!existsSync(output.dsymArchive) || !statSync(output.dsymArchive).isFile()
|
|
110
|
+
|| statSync(output.dsymArchive).size === 0) {
|
|
111
|
+
throw new Error('The iOS dSYM archive was not written.');
|
|
112
|
+
}
|
|
113
|
+
input.copyFile(tool, output.uploadTool);
|
|
114
|
+
input.copyFile(plist, output.googleServiceInfo);
|
|
115
|
+
return { ...output, dsyms };
|
|
116
|
+
}
|
|
117
|
+
/** The single `.app` bundle under `Products/Applications` of an archive. */
|
|
118
|
+
function archivedApp(archive) {
|
|
119
|
+
const applications = path.join(archive, 'Products/Applications');
|
|
120
|
+
const apps = existsSync(applications)
|
|
121
|
+
? readdirSync(applications).filter((name) => name.endsWith('.app'))
|
|
122
|
+
: [];
|
|
123
|
+
if (apps.length !== 1) {
|
|
124
|
+
throw new Error('The iOS archive must contain exactly one application bundle.');
|
|
125
|
+
}
|
|
126
|
+
return path.join(applications, apps[0] ?? '');
|
|
127
|
+
}
|
|
128
|
+
/** Every `*.dSYM` bundle of an archive's `dSYMs` folder, sorted, with its UUIDs. */
|
|
129
|
+
export function readArchiveDsyms(dsymFolder) {
|
|
130
|
+
if (!existsSync(dsymFolder)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const bundles = readdirSync(dsymFolder)
|
|
134
|
+
.filter((name) => name.endsWith('.dSYM') && !name.startsWith('.'))
|
|
135
|
+
.sort();
|
|
136
|
+
return bundles.map((bundle) => {
|
|
137
|
+
if (!isDsymBundleName(bundle)) {
|
|
138
|
+
throw new Error(`dSYM bundle name is not supported: ${JSON.stringify(bundle)}.`);
|
|
139
|
+
}
|
|
140
|
+
const dwarf = path.join(dsymFolder, bundle, 'Contents/Resources/DWARF');
|
|
141
|
+
const files = existsSync(dwarf)
|
|
142
|
+
? readdirSync(dwarf).filter((name) => lstatSync(path.join(dwarf, name)).isFile()).sort()
|
|
143
|
+
: [];
|
|
144
|
+
// Only Mach-O files carry UUIDs; Finder litter such as .DS_Store is skipped.
|
|
145
|
+
const uuids = [...new Set(files.flatMap((file) => {
|
|
146
|
+
const candidate = path.join(dwarf, file);
|
|
147
|
+
return isMachOFile(candidate) ? readMachOUuids(candidate) : [];
|
|
148
|
+
}))].sort();
|
|
149
|
+
if (uuids.length === 0) {
|
|
150
|
+
throw new Error(`dSYM ${bundle} has no Mach-O UUID.`);
|
|
151
|
+
}
|
|
152
|
+
return { bundle, uuids };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function isMachOFile(file) {
|
|
156
|
+
const descriptor = openSync(file, 'r');
|
|
157
|
+
try {
|
|
158
|
+
const magic = Buffer.alloc(4);
|
|
159
|
+
if (readSync(descriptor, magic, 0, 4, 0) !== 4) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return [0xfeedfacf, 0xfeedface, 0xcffaedfe, 0xcefaedfe, 0xcafebabe, 0xcafebabf]
|
|
163
|
+
.includes(magic.readUInt32BE(0));
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
closeSync(descriptor);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const lcUuid = 0x1b;
|
|
170
|
+
const uuidPattern = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/u;
|
|
171
|
+
/** dSYM UUIDs as `dwarfdump --uuid` prints them. */
|
|
172
|
+
export function isDsymUuid(value) {
|
|
173
|
+
return typeof value === 'string' && uuidPattern.test(value);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The `LC_UUID` of each architecture in a thin or universal Mach-O file,
|
|
177
|
+
* read from its headers only (DWARF files can be hundreds of megabytes).
|
|
178
|
+
*/
|
|
179
|
+
export function readMachOUuids(file) {
|
|
180
|
+
const descriptor = openSync(file, 'r');
|
|
181
|
+
try {
|
|
182
|
+
const read = (offset, length) => {
|
|
183
|
+
const buffer = Buffer.alloc(length);
|
|
184
|
+
const bytes = readSync(descriptor, buffer, 0, length, offset);
|
|
185
|
+
if (bytes !== length) {
|
|
186
|
+
throw new Error(`${path.basename(file)} is not a complete Mach-O file.`);
|
|
187
|
+
}
|
|
188
|
+
return buffer;
|
|
189
|
+
};
|
|
190
|
+
const magic = read(0, 4).readUInt32BE(0);
|
|
191
|
+
if (magic === 0xcafebabe || magic === 0xcafebabf) {
|
|
192
|
+
const wide = magic === 0xcafebabf;
|
|
193
|
+
const count = read(4, 4).readUInt32BE(0);
|
|
194
|
+
if (count === 0 || count > 32) {
|
|
195
|
+
throw new Error(`${path.basename(file)} has an invalid universal header.`);
|
|
196
|
+
}
|
|
197
|
+
const entrySize = wide ? 32 : 20;
|
|
198
|
+
const uuids = [];
|
|
199
|
+
for (let index = 0; index < count; index += 1) {
|
|
200
|
+
const entry = read(8 + index * entrySize, entrySize);
|
|
201
|
+
const offset = wide ? Number(entry.readBigUInt64BE(8)) : entry.readUInt32BE(8);
|
|
202
|
+
uuids.push(...readThinMachOUuid(read, offset, file));
|
|
203
|
+
}
|
|
204
|
+
return uuids;
|
|
205
|
+
}
|
|
206
|
+
return readThinMachOUuid(read, 0, file);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
closeSync(descriptor);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function readThinMachOUuid(read, base, file) {
|
|
213
|
+
const magicBytes = read(base, 4);
|
|
214
|
+
const magic = magicBytes.readUInt32LE(0);
|
|
215
|
+
let headerSize;
|
|
216
|
+
let littleEndian = true;
|
|
217
|
+
if (magic === 0xfeedfacf) {
|
|
218
|
+
headerSize = 32;
|
|
219
|
+
}
|
|
220
|
+
else if (magic === 0xfeedface) {
|
|
221
|
+
headerSize = 28;
|
|
222
|
+
}
|
|
223
|
+
else if (magicBytes.readUInt32BE(0) === 0xfeedfacf) {
|
|
224
|
+
headerSize = 32;
|
|
225
|
+
littleEndian = false;
|
|
226
|
+
}
|
|
227
|
+
else if (magicBytes.readUInt32BE(0) === 0xfeedface) {
|
|
228
|
+
headerSize = 28;
|
|
229
|
+
littleEndian = false;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
throw new Error(`${path.basename(file)} is not a Mach-O file.`);
|
|
233
|
+
}
|
|
234
|
+
const u32 = (buffer, offset) => littleEndian
|
|
235
|
+
? buffer.readUInt32LE(offset)
|
|
236
|
+
: buffer.readUInt32BE(offset);
|
|
237
|
+
const header = read(base, headerSize);
|
|
238
|
+
const commandCount = u32(header, 16);
|
|
239
|
+
const commandBytes = u32(header, 20);
|
|
240
|
+
if (commandCount > 4096 || commandBytes > 16 * 1024 * 1024) {
|
|
241
|
+
throw new Error(`${path.basename(file)} has an invalid Mach-O header.`);
|
|
242
|
+
}
|
|
243
|
+
const commands = read(base + headerSize, commandBytes);
|
|
244
|
+
let offset = 0;
|
|
245
|
+
for (let index = 0; index < commandCount; index += 1) {
|
|
246
|
+
if (offset + 8 > commands.length) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
const command = u32(commands, offset);
|
|
250
|
+
const size = u32(commands, offset + 4);
|
|
251
|
+
if (size < 8 || offset + size > commands.length) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
if (command === lcUuid && size >= 24) {
|
|
255
|
+
const hex = commands.subarray(offset + 8, offset + 24).toString('hex').toUpperCase();
|
|
256
|
+
return [`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`];
|
|
257
|
+
}
|
|
258
|
+
offset += size;
|
|
259
|
+
}
|
|
260
|
+
return [];
|
|
261
|
+
}
|