@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.
@@ -11,6 +11,7 @@ import { addIosAppResource, applyAndroidManifestAdId, applyAndroidManifestOrient
11
11
  import { readIosPushWiringInput } from './native-shell-ios-push-files.js';
12
12
  import { isNonPublicServiceHostname } from './production-target-readiness.js';
13
13
  import { assertAndroidSettingsAppProject, assertAndroidSettingsNoAppRemap, assertIosReleaseInfoPlistExpansion, assertIosReleasePackagingSettings, assertIosReleaseProductName, assertNativeShellIdentity, hasAndroidDisplayNameResourceOverride, hasAndroidManifestSourceSetOverride, hasAndroidResourceSourceSetOverride, hasGradleIdentityMutation, hasGradleTaskAction, maskGradleStrings, readIosAppTargetId, readIosReleaseInfoPlist, stripGradleComments, } from './native-shell-identity.js';
14
+ import { planNotificationsBuildApprovals } from './pnpm-build-approvals.js';
14
15
  const shellPath = 'apps/mobile-capacitor';
15
16
  const webPath = `${shellPath}/www`;
16
17
  const referenceShellPath = '${MPGD_KIT_PATH}/apps/mobile-capacitor';
@@ -500,6 +501,17 @@ export function planCapacitorShellStarter(input) {
500
501
  });
501
502
  }
502
503
  requestedFiles.push(...gatewayFiles);
504
+ if (notifications !== undefined) {
505
+ // pnpm 11 fails an install on an unreviewed dependency build script, and
506
+ // the firebase package notifications add brings two.
507
+ const approvals = planNotificationsBuildApprovals(readExisting(safeDestination(gameRoot, 'pnpm-workspace.yaml')));
508
+ if (approvals.content !== undefined) {
509
+ requestedFiles.push({ path: 'pnpm-workspace.yaml', content: approvals.content });
510
+ }
511
+ if (approvals.warning !== undefined) {
512
+ warnings.push(approvals.warning);
513
+ }
514
+ }
503
515
  // The game bundle imports the plugin from its gateway file, so the game
504
516
  // declares it too, at the shell's exact version; the materializer's game
505
517
  // install picks it up.
@@ -668,7 +680,12 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
668
680
  // workspace globs. Always retry its standalone install: a CLI shim alone
669
681
  // does not prove that an interrupted dependency install completed.
670
682
  runner.run('pnpm', ['install', '--no-frozen-lockfile'], plan.gameRoot);
671
- runner.run('pnpm', ['--dir', shellPath, 'install', '--ignore-workspace', '--no-frozen-lockfile'], plan.gameRoot);
683
+ runner.run('pnpm',
684
+ // --ignore-workspace also drops every pnpm-workspace.yaml setting, so no
685
+ // allowBuilds entry can review a dependency build script here and pnpm 11
686
+ // fails on any (firebase brings two when the shell resolves it). The
687
+ // shell's packages need none: Gradle and Xcode build the native code.
688
+ ['--dir', shellPath, 'install', '--ignore-workspace', '--ignore-scripts', '--no-frozen-lockfile'], plan.gameRoot);
672
689
  for (const platform of plan.nativePlatformsToAdd) {
673
690
  const generatedNow = !existsSync(path.join(plan.gameRoot, shellPath, platform));
674
691
  if (generatedNow) {
@@ -14,9 +14,11 @@ export declare const crashlyticsGradlePluginVersion = "3.0.8";
14
14
  /** The manual Android steps for a shell whose Gradle files the CLI does not rewrite. */
15
15
  export declare const crashReportingAndroidSteps: readonly string[];
16
16
  /**
17
- * The iOS steps the CLI leaves to the game: the plugin captures crashes once
18
- * `GoogleService-Info.plist` is in the App target, but readable native stack
19
- * traces need the dSYMs uploaded (docs/CRASH_REPORTING.md#symbols).
17
+ * The iOS dSYM note: the plugin captures crashes once `GoogleService-Info.plist`
18
+ * is in the App target, and readable native stack traces need the dSYMs
19
+ * uploaded. `mpgd deploy run` records and uploads them for the archives it
20
+ * builds; an archive made another way needs Firebase's run script or
21
+ * `upload-symbols` (docs/CRASH_REPORTING.md#symbols).
20
22
  */
21
23
  export declare const crashReportingIosSteps: readonly string[];
22
24
  export interface PlannedCrashReporting {
@@ -31,15 +31,18 @@ export const crashReportingAndroidSteps = Object.freeze([
31
31
  `In android/app/build.gradle, add apply plugin: '${crashlyticsGradlePluginId}' right after apply plugin: 'com.google.gms.google-services', inside the same google-services.json check.`,
32
32
  ]);
33
33
  /**
34
- * The iOS steps the CLI leaves to the game: the plugin captures crashes once
35
- * `GoogleService-Info.plist` is in the App target, but readable native stack
36
- * traces need the dSYMs uploaded (docs/CRASH_REPORTING.md#symbols).
34
+ * The iOS dSYM note: the plugin captures crashes once `GoogleService-Info.plist`
35
+ * is in the App target, and readable native stack traces need the dSYMs
36
+ * uploaded. `mpgd deploy run` records and uploads them for the archives it
37
+ * builds; an archive made another way needs Firebase's run script or
38
+ * `upload-symbols` (docs/CRASH_REPORTING.md#symbols).
37
39
  */
38
40
  export const crashReportingIosSteps = Object.freeze([
39
- 'Upload dSYMs for every App Store build: add the Crashlytics run script build phase '
40
- + '("${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run") as the '
41
- + 'last phase of the App target, or run upload-symbols after each archive; see '
42
- + 'docs/CRASH_REPORTING.md in mpgd-kit.',
41
+ 'dSYMs: mpgd deploy run uploads them for every App Store build it makes (retry with mpgd '
42
+ + 'deploy symbols). For an archive made in Xcode or another pipeline, add the Crashlytics run '
43
+ + 'script build phase ("${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/'
44
+ + 'Crashlytics/run") as the last phase of the App target, or run upload-symbols after the '
45
+ + 'archive; see docs/CRASH_REPORTING.md in mpgd-kit.',
43
46
  ]);
44
47
  /**
45
48
  * Plan the crash-reporting provider before anything is written. Refused when
@@ -0,0 +1,67 @@
1
+ import { type ReleaseProcessInput, type ReleaseProcessResult } from './deploy-process.js';
2
+ import type { NativeDsymIdentity } from './native-build-symbols.js';
3
+ import { readNativeReleaseStatus, recordCrashSymbolUpload, type CrashSymbolUploadRecord } from './release-state.js';
4
+ export interface CrashSymbolUploadPlan {
5
+ readonly releaseKey: string;
6
+ readonly buildRunId: string;
7
+ readonly dsymArchiveSha256: string;
8
+ readonly dsyms: readonly NativeDsymIdentity[];
9
+ /** `upload-symbols` and its arguments, with the recorded (game-relative) files. */
10
+ readonly command: readonly string[];
11
+ }
12
+ export type CrashSymbolUploadResult = {
13
+ readonly status: 'not-applicable';
14
+ readonly reason: string;
15
+ } | {
16
+ readonly status: 'already-uploaded';
17
+ readonly upload: CrashSymbolUploadRecord;
18
+ } | {
19
+ readonly status: 'planned';
20
+ readonly plan: CrashSymbolUploadPlan;
21
+ } | {
22
+ readonly status: 'uploaded';
23
+ readonly plan: CrashSymbolUploadPlan;
24
+ /** Absent when the upload succeeded but its outcome could not be recorded. */
25
+ readonly upload?: CrashSymbolUploadRecord;
26
+ /** Why the successful upload could not be recorded; a retry uploads again. */
27
+ readonly recordError?: string;
28
+ readonly output: string;
29
+ } | {
30
+ readonly status: 'failed';
31
+ readonly plan: CrashSymbolUploadPlan;
32
+ readonly error: string;
33
+ readonly output: string;
34
+ /** Absent when the failure itself could not be recorded. */
35
+ readonly upload?: CrashSymbolUploadRecord;
36
+ };
37
+ export interface UploadRecordedCrashSymbolsInput {
38
+ readonly gameRoot: string;
39
+ readonly gameId: string;
40
+ readonly releaseKey: string;
41
+ /** Print the planned upload; run nothing and record nothing. */
42
+ readonly dryRun?: boolean;
43
+ readonly environment?: NodeJS.ProcessEnv;
44
+ readonly signal?: AbortSignal;
45
+ }
46
+ /** Injection boundary for tests; production uses the release state and a child process. */
47
+ export interface CrashSymbolUploadPorts {
48
+ readonly readStatus: typeof readNativeReleaseStatus;
49
+ readonly recordUpload: typeof recordCrashSymbolUpload;
50
+ readonly run: (input: ReleaseProcessInput) => Promise<ReleaseProcessResult>;
51
+ readonly platform: NodeJS.Platform;
52
+ }
53
+ /** Upload (or, with `dryRun`, plan) the Crashlytics dSYMs of one recorded iOS build. */
54
+ export declare function uploadRecordedCrashSymbols(input: UploadRecordedCrashSymbolsInput, ports?: CrashSymbolUploadPorts): Promise<CrashSymbolUploadResult>;
55
+ /** The command that retries a failed upload for one release, for the game directory used. */
56
+ export declare function crashSymbolsRetryCommand(input: {
57
+ readonly gameRoot: string;
58
+ readonly gameId: string;
59
+ readonly releaseKey: string;
60
+ }): string;
61
+ /**
62
+ * The `mpgd deploy run` step: upload the recorded dSYMs of the release's iOS
63
+ * build once. A failure (or an error reading the files or the state) is
64
+ * reported with the retry command and never thrown, so it cannot fail or
65
+ * block the store submission.
66
+ */
67
+ export declare function uploadCrashSymbolsForDeployRun(input: UploadRecordedCrashSymbolsInput, report?: (message: string) => void, upload?: typeof uploadRecordedCrashSymbols): Promise<CrashSymbolUploadResult | undefined>;
@@ -0,0 +1,232 @@
1
+ import { chmodSync, copyFileSync, existsSync, mkdtempSync, rmSync, statSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { ReleaseProcessError, runReleaseProcess, } from './deploy-process.js';
5
+ import { resolveRecordedArtifact } from './native-deploy-submission.js';
6
+ import { readNativeReleaseStatus, recordCrashSymbolUpload, sha256File, } from './release-state.js';
7
+ // Uploads the dSYMs recorded with an iOS build to Firebase Crashlytics with
8
+ // Firebase's own `upload-symbols` (the copy recorded from the build's package
9
+ // checkout) and the `GoogleService-Info.plist` the archived app bundles:
10
+ //
11
+ // upload-symbols -gsp GoogleService-Info.plist -p ios dSYMs.zip
12
+ //
13
+ // Firebase documents this command for uploading dSYMs outside the Xcode run
14
+ // script (https://firebase.google.com/docs/crashlytics/ios/get-deobfuscated-reports);
15
+ // the tool's usage text accepts "zip archives containing dSYMs". The outcome is
16
+ // recorded next to the immutable build record, so an upload is done once and a
17
+ // failed one can be retried with `mpgd deploy symbols`. It never touches a store
18
+ // submission.
19
+ const uploadTimeoutMs = 10 * 60_000;
20
+ const productionPorts = {
21
+ readStatus: readNativeReleaseStatus,
22
+ recordUpload: recordCrashSymbolUpload,
23
+ run: runReleaseProcess,
24
+ platform: process.platform,
25
+ };
26
+ /** Upload (or, with `dryRun`, plan) the Crashlytics dSYMs of one recorded iOS build. */
27
+ export async function uploadRecordedCrashSymbols(input, ports = productionPorts) {
28
+ const stateInput = {
29
+ gameRoot: input.gameRoot,
30
+ gameId: input.gameId,
31
+ releaseKey: input.releaseKey,
32
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
33
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
34
+ };
35
+ const status = await ports.readStatus(stateInput);
36
+ const build = status.builds.ios;
37
+ if (build === undefined) {
38
+ throw new Error('This release has no recorded iOS build.');
39
+ }
40
+ const symbols = build.crashlyticsSymbols;
41
+ if (symbols === undefined) {
42
+ return {
43
+ status: 'not-applicable',
44
+ reason: 'The iOS build recorded no Crashlytics dSYMs: crash reporting is off, or the build '
45
+ + 'was recorded before deploy run recorded dSYMs (upload those by hand, see '
46
+ + 'docs/CRASH_REPORTING.md).',
47
+ };
48
+ }
49
+ const previous = status.symbolUploads?.ios;
50
+ if (previous?.status === 'uploaded' && previous.buildRunId === build.buildRunId
51
+ && previous.dsymArchiveSha256 === symbols.dsymArchive.sha256) {
52
+ return { status: 'already-uploaded', upload: previous };
53
+ }
54
+ const plan = {
55
+ releaseKey: input.releaseKey,
56
+ buildRunId: build.buildRunId,
57
+ dsymArchiveSha256: symbols.dsymArchive.sha256,
58
+ dsyms: symbols.dsyms,
59
+ command: [
60
+ symbols.uploadTool.location,
61
+ '-gsp',
62
+ symbols.googleServiceInfo.location,
63
+ '-p',
64
+ 'ios',
65
+ symbols.dsymArchive.location,
66
+ ],
67
+ };
68
+ // The recorded files must be the build's bytes, also for a dry run.
69
+ const tool = await verifiedRecordedFile(input.gameRoot, symbols.uploadTool, 'upload-symbols');
70
+ const plist = await verifiedRecordedFile(input.gameRoot, symbols.googleServiceInfo, 'GoogleService-Info.plist');
71
+ const archive = await verifiedRecordedFile(input.gameRoot, symbols.dsymArchive, 'dSYM archive');
72
+ if (input.dryRun === true) {
73
+ return { status: 'planned', plan };
74
+ }
75
+ if (ports.platform !== 'darwin') {
76
+ throw new Error('Firebase upload-symbols is a macOS tool; upload the dSYMs from a Mac.');
77
+ }
78
+ const record = async (outcome, failure) => ports.recordUpload({
79
+ ...stateInput,
80
+ buildRunId: build.buildRunId,
81
+ dsymArchiveSha256: symbols.dsymArchive.sha256,
82
+ status: outcome,
83
+ ...(failure === undefined ? {} : { failure }),
84
+ });
85
+ // Run a private executable copy: a restored workflow artifact loses its mode
86
+ // bits, and the recorded file itself stays untouched.
87
+ const workDirectory = mkdtempSync(path.join(tmpdir(), 'mpgd-crash-symbols-'));
88
+ let output = '';
89
+ try {
90
+ const executable = path.join(workDirectory, 'upload-symbols');
91
+ copyFileSync(tool, executable);
92
+ chmodSync(executable, 0o700);
93
+ if (await sha256File(executable) !== symbols.uploadTool.sha256) {
94
+ throw new Error('The copied upload-symbols differs from the recorded tool.');
95
+ }
96
+ try {
97
+ const result = await ports.run({
98
+ command: executable,
99
+ args: ['-gsp', plist, '-p', 'ios', archive],
100
+ cwd: workDirectory,
101
+ environment: uploadEnvironment(input.environment ?? process.env, workDirectory),
102
+ timeoutMs: uploadTimeoutMs,
103
+ signal: input.signal,
104
+ });
105
+ output = result.output;
106
+ }
107
+ catch (error) {
108
+ output = error instanceof ReleaseProcessError ? error.output : '';
109
+ const reason = describeFailure(error);
110
+ let upload;
111
+ try {
112
+ upload = await record('failed', reason);
113
+ }
114
+ catch {
115
+ upload = undefined;
116
+ }
117
+ return {
118
+ status: 'failed',
119
+ plan,
120
+ error: reason,
121
+ output,
122
+ ...(upload === undefined ? {} : { upload }),
123
+ };
124
+ }
125
+ }
126
+ finally {
127
+ rmSync(workDirectory, { recursive: true, force: true });
128
+ }
129
+ try {
130
+ return { status: 'uploaded', plan, upload: await record('uploaded'), output };
131
+ }
132
+ catch (error) {
133
+ // The dSYMs reached Crashlytics; only the release state is behind.
134
+ return {
135
+ status: 'uploaded',
136
+ plan,
137
+ recordError: error instanceof Error ? error.message : String(error),
138
+ output,
139
+ };
140
+ }
141
+ }
142
+ /** Only what the tool needs (and network proxy settings): no store, signing or Git credentials. */
143
+ function uploadEnvironment(source, temporary) {
144
+ const environment = { TMPDIR: temporary };
145
+ for (const name of [
146
+ 'PATH',
147
+ 'HOME',
148
+ 'USER',
149
+ 'LANG',
150
+ 'LC_ALL',
151
+ 'HTTPS_PROXY',
152
+ 'https_proxy',
153
+ 'HTTP_PROXY',
154
+ 'http_proxy',
155
+ 'NO_PROXY',
156
+ 'no_proxy',
157
+ 'SSL_CERT_FILE',
158
+ ]) {
159
+ const value = source[name];
160
+ if (value !== undefined) {
161
+ environment[name] = value;
162
+ }
163
+ }
164
+ return environment;
165
+ }
166
+ function describeFailure(error) {
167
+ if (error instanceof ReleaseProcessError) {
168
+ if (error.reason === 'exit') {
169
+ return `upload-symbols exited with code ${String(error.exitCode ?? 'unknown')}`;
170
+ }
171
+ return error.reason === 'timeout'
172
+ ? 'upload-symbols timed out'
173
+ : `upload-symbols ${error.reason === 'spawn' ? 'could not start' : 'was aborted'}`;
174
+ }
175
+ return error instanceof Error ? error.message : String(error);
176
+ }
177
+ async function verifiedRecordedFile(gameRoot, recorded, label) {
178
+ const candidate = path.resolve(gameRoot, recorded.location);
179
+ if (!existsSync(candidate) || !statSync(candidate).isFile()) {
180
+ throw new Error(`The recorded ${label} is missing: ${recorded.location}. Restore the release bundle `
181
+ + '(.mpgd/releases/<release>) the build wrote.');
182
+ }
183
+ let file;
184
+ try {
185
+ file = resolveRecordedArtifact(gameRoot, recorded.location);
186
+ }
187
+ catch {
188
+ throw new Error(`The recorded ${label} resolves outside the game directory.`);
189
+ }
190
+ if (await sha256File(file) !== recorded.sha256) {
191
+ throw new Error(`The recorded ${label} differs from the immutable build record.`);
192
+ }
193
+ return file;
194
+ }
195
+ /** The command that retries a failed upload for one release, for the game directory used. */
196
+ export function crashSymbolsRetryCommand(input) {
197
+ return `mpgd deploy symbols --game ${shellWord(input.gameRoot)} --game-id ${shellWord(input.gameId)} --release ${shellWord(input.releaseKey)}`;
198
+ }
199
+ function shellWord(value) {
200
+ return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
201
+ }
202
+ /**
203
+ * The `mpgd deploy run` step: upload the recorded dSYMs of the release's iOS
204
+ * build once. A failure (or an error reading the files or the state) is
205
+ * reported with the retry command and never thrown, so it cannot fail or
206
+ * block the store submission.
207
+ */
208
+ export async function uploadCrashSymbolsForDeployRun(input, report = (message) => console.warn(message), upload = uploadRecordedCrashSymbols) {
209
+ const retry = crashSymbolsRetryCommand(input);
210
+ try {
211
+ const result = await upload(input);
212
+ if (result.status === 'uploaded' && result.recordError !== undefined) {
213
+ report(`warning: Crashlytics: uploaded the iOS dSYMs, but the release state could not record `
214
+ + `it (${result.recordError}); "mpgd deploy status" still shows it as not uploaded, and `
215
+ + `${retry} would upload them again.`);
216
+ }
217
+ else if (result.status === 'uploaded') {
218
+ report(`Crashlytics: uploaded the iOS dSYMs (${result.plan.dsyms
219
+ .map((entry) => entry.bundle).join(', ')}).`);
220
+ }
221
+ else if (result.status === 'failed') {
222
+ report(`warning: Crashlytics dSYM upload failed (${result.error}); the store submission `
223
+ + `is not affected. Retry with: ${retry}${result.output === '' ? '' : `\n${result.output}`}`);
224
+ }
225
+ return result;
226
+ }
227
+ catch (error) {
228
+ report(`warning: Crashlytics dSYM upload did not run (${error instanceof Error ? error.message : String(error)}); the store submission is not `
229
+ + `affected. Retry with: ${retry}`);
230
+ return undefined;
231
+ }
232
+ }
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ import { createPlayReleaseRequest, parsePlayUserFraction, readPlayReleaseNotesFi
38
38
  import { promotePlayRelease } from './play-release-promote.js';
39
39
  import { changePlayRollout } from './play-release-rollout.js';
40
40
  import { readNativeReleaseStatus as readNativeReleaseStatusForCli } from './release-state.js';
41
+ import { uploadRecordedCrashSymbols } from './crash-symbols-upload.js';
41
42
  export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, maximumGameplayE2EReportBytes, } from './game-acceptance.js';
42
43
  export { maximumPerformanceEvidenceBytes, preparePerformanceAcceptance, readPerformanceAcceptanceEvidence, validatePerformanceBudget, } from './performance-acceptance.js';
43
44
  export { applyCapacitorShellStarter, materializeCapacitorShellStarter, planCapacitorShellStarter, } from './capacitor-shell-starter.js';
@@ -439,6 +440,42 @@ const deployCommand = defineI18n({
439
440
  console.info(JSON.stringify(status, null, 2));
440
441
  },
441
442
  }),
443
+ symbols: defineI18n({
444
+ name: 'symbols',
445
+ description: "Upload the Crashlytics dSYMs recorded with a release's iOS build.",
446
+ resource: commandResource({
447
+ en: "Upload the Crashlytics dSYMs recorded with a release's iOS build.",
448
+ ko: '릴리스의 iOS 빌드에 기록된 Crashlytics dSYM을 업로드합니다.',
449
+ }),
450
+ args: {
451
+ game: { type: 'string', required: false, description: 'Game project directory holding .mpgd/releases.' },
452
+ 'game-id': { type: 'string', required: true, description: 'Explicit release ledger game ID.' },
453
+ release: { type: 'string', required: true, description: 'Explicit release ID.' },
454
+ 'dry-run': {
455
+ type: 'boolean', required: false,
456
+ description: 'Verify the recorded files and print the planned upload-symbols command; run nothing.',
457
+ },
458
+ },
459
+ run: async (ctx) => {
460
+ const result = await uploadRecordedCrashSymbols({
461
+ gameRoot: path.resolve(readOptionalString(ctx.values.game) ?? '.'),
462
+ gameId: readRequiredCliOption(ctx.values['game-id'], '--game-id'),
463
+ releaseKey: readRequiredCliOption(ctx.values.release, '--release'),
464
+ dryRun: ctx.values['dry-run'] === true,
465
+ });
466
+ const { output, ...summary } = 'output' in result ? result : { ...result, output: '' };
467
+ if (output !== '') {
468
+ console.info(output);
469
+ }
470
+ console.info(JSON.stringify(summary, null, 2));
471
+ if (result.status === 'failed') {
472
+ throw new Error(`Crashlytics dSYM upload failed: ${result.error}`);
473
+ }
474
+ if (result.status === 'uploaded' && result.recordError !== undefined) {
475
+ throw new Error(`The dSYMs were uploaded, but the release state could not record it: ${result.recordError}`);
476
+ }
477
+ },
478
+ }),
442
479
  submit: defineI18n({
443
480
  name: 'submit',
444
481
  description: 'Resume store submission of one already verified native build.',
@@ -1891,7 +1928,17 @@ const targetCommand = defineI18n({
1891
1928
  }),
1892
1929
  });
1893
1930
  if (!dryRun) {
1894
- materializeCapacitorShellStarter(plan);
1931
+ try {
1932
+ materializeCapacitorShellStarter(plan);
1933
+ }
1934
+ catch (error) {
1935
+ // A failed install may be the one the warnings explain (for example
1936
+ // the allowBuilds entries an unsupported pnpm-workspace.yaml needs).
1937
+ for (const warning of plan.warnings) {
1938
+ console.warn(`Warning: ${warning}`);
1939
+ }
1940
+ throw error;
1941
+ }
1895
1942
  }
1896
1943
  console.info(`${dryRun ? 'Would update' : 'Updated'} game-owned Capacitor shell: ${plan.changedFiles.length} file(s).`);
1897
1944
  for (const file of plan.changedFiles) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "c36007876f2cd9109fcdf519f91756c4453f18d4",
2
+ "kitGitSha": "ea82dfadb6cdcd6b842b753f4ce10b52ad1a1fb4",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.61.1"
4
+ "packageVersion": "0.62.0"
5
5
  }
@@ -0,0 +1,95 @@
1
+ /** Game-relative paths, under the build's `release-output/native/<target>/` root. */
2
+ export interface NativeBuildSymbols {
3
+ /** Android: R8 `mapping.txt` of this exact bundle. */
4
+ readonly r8Mapping?: string;
5
+ /** iOS with Firebase Crashlytics. */
6
+ readonly crashlytics?: NativeBuildCrashlyticsSymbols;
7
+ }
8
+ export interface NativeBuildCrashlyticsSymbols {
9
+ /** Zip of the archive's `dSYMs` folder. */
10
+ readonly dsymArchive: string;
11
+ /** Each dSYM bundle in the archive and the Mach-O UUIDs of its DWARF files. */
12
+ readonly dsyms: readonly NativeDsymIdentity[];
13
+ /** Firebase's `upload-symbols` from the shell's package checkout. */
14
+ readonly uploadTool: string;
15
+ /** The `GoogleService-Info.plist` the archived app bundles. */
16
+ readonly googleServiceInfo: string;
17
+ }
18
+ export interface NativeDsymIdentity {
19
+ readonly bundle: string;
20
+ readonly uuids: readonly string[];
21
+ }
22
+ /**
23
+ * Validate a symbols record read from a build status file. `isPath` checks
24
+ * each recorded path (game-relative, inside the build's output root).
25
+ */
26
+ export declare function assertNativeBuildSymbols(value: unknown, isPath: (file: string) => boolean): asserts value is NativeBuildSymbols;
27
+ /** A non-empty, sorted list of dSYM bundles with their UUIDs. */
28
+ export declare function isDsymIdentityList(value: unknown): value is readonly NativeDsymIdentity[];
29
+ /** A dSYM bundle file name: no path separator or control character, not hidden. */
30
+ export declare function isDsymBundleName(name: string): boolean;
31
+ /**
32
+ * A game-relative path under the target's native build output root
33
+ * (`release-output/native/<target>/`), without empty or `..` segments.
34
+ */
35
+ export declare function isNativeOutputPath(file: string, target: string): boolean;
36
+ /** Where AGP writes the release variant's R8 mapping file. */
37
+ export declare const androidReleaseMappingPath = "app/build/outputs/mapping/release/mapping.txt";
38
+ /** File names inside the build's `symbols/` directory. */
39
+ export declare const nativeSymbolFileNames: Readonly<{
40
+ r8Mapping: "mapping.txt";
41
+ dsymArchive: "dSYMs.zip";
42
+ uploadTool: "upload-symbols";
43
+ googleServiceInfo: "GoogleService-Info.plist";
44
+ }>;
45
+ /**
46
+ * Where a Crashlytics-enabled iOS archive puts its Swift Package checkouts
47
+ * (`xcodebuild -clonedSourcePackagesDirPath`), so `upload-symbols` is found
48
+ * without searching DerivedData.
49
+ */
50
+ export declare const iosCrashlyticsSourcePackagesDirectory = ".mpgd-source-packages";
51
+ /**
52
+ * `upload-symbols` ships next to the Crashlytics `run` script: in the Swift
53
+ * Package checkout (`SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/`,
54
+ * the path Firebase's run script uses) or in CocoaPods
55
+ * (`Pods/FirebaseCrashlytics/`, the path Firebase's manual-upload command uses).
56
+ */
57
+ export declare function crashlyticsUploadToolCandidates(input: {
58
+ readonly sourcePackages: string;
59
+ readonly iosProject: string;
60
+ }): readonly string[];
61
+ export interface CollectIosCrashlyticsSymbolsInput {
62
+ /** Absolute path of the `.xcarchive` this build produced. */
63
+ readonly archive: string;
64
+ /** Absolute path of the staged shell's `ios` directory. */
65
+ readonly iosProject: string;
66
+ /** Absolute `-clonedSourcePackagesDirPath` of the archive build. */
67
+ readonly sourcePackages: string;
68
+ /** Absolute output directory (`<artifact root>/symbols`). */
69
+ readonly outputDirectory: string;
70
+ readonly copyFile: (source: string, destination: string) => void;
71
+ /** Zips `source` (a directory) into `destination` keeping the folder name. */
72
+ readonly zipDirectory: (source: string, destination: string) => void;
73
+ }
74
+ export interface CollectedIosCrashlyticsSymbols {
75
+ readonly dsymArchive: string;
76
+ readonly dsyms: readonly NativeDsymIdentity[];
77
+ readonly uploadTool: string;
78
+ readonly googleServiceInfo: string;
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 declare function collectIosCrashlyticsSymbols(input: CollectIosCrashlyticsSymbolsInput): CollectedIosCrashlyticsSymbols;
87
+ /** Every `*.dSYM` bundle of an archive's `dSYMs` folder, sorted, with its UUIDs. */
88
+ export declare function readArchiveDsyms(dsymFolder: string): NativeDsymIdentity[];
89
+ /** dSYM UUIDs as `dwarfdump --uuid` prints them. */
90
+ export declare function isDsymUuid(value: unknown): value is string;
91
+ /**
92
+ * The `LC_UUID` of each architecture in a thin or universal Mach-O file,
93
+ * read from its headers only (DWARF files can be hundreds of megabytes).
94
+ */
95
+ export declare function readMachOUuids(file: string): string[];