Skip to content

Commit 656539f

Browse files
committed
Fix Sentry CLI and plist compatibility
1 parent 1b55c10 commit 656539f

4 files changed

Lines changed: 200 additions & 57 deletions

File tree

src/bundle-runner.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,51 @@ export async function copyDebugidForSentry(
557557
fs.removeSync(path.join(outputFolder, `${bundleName}.txt.map`));
558558
}
559559

560+
export function buildSentrySourcemapsUploadArgs(
561+
sentryCliPath: string,
562+
bundleName: string,
563+
outputFolder: string,
564+
version: string,
565+
useStandaloneSourcemapsCommand = true,
566+
): string[] {
567+
const uploadArgs = [
568+
'--strip-prefix',
569+
path.join(process.cwd(), outputFolder),
570+
path.join(outputFolder, bundleName),
571+
path.join(outputFolder, `${bundleName}.map`),
572+
];
573+
574+
if (!useStandaloneSourcemapsCommand) {
575+
return [
576+
sentryCliPath,
577+
'releases',
578+
'files',
579+
version,
580+
'upload-sourcemaps',
581+
...uploadArgs,
582+
];
583+
}
584+
585+
return [
586+
sentryCliPath,
587+
'sourcemaps',
588+
'upload',
589+
'--release',
590+
version,
591+
...uploadArgs,
592+
];
593+
}
594+
595+
function supportsStandaloneSentrySourcemapsUpload(sentryCliPath: string) {
596+
const result = spawnJavaScriptSync(
597+
[sentryCliPath, 'sourcemaps', 'upload', '--help'],
598+
{
599+
stdio: 'ignore',
600+
},
601+
);
602+
return !result.error && result.status === 0;
603+
}
604+
560605
export async function uploadSourcemapForSentry(
561606
bundleName: string,
562607
outputFolder: string,
@@ -595,17 +640,13 @@ export async function uploadSourcemapForSentry(
595640
console.log(t('uploadingSourcemap'));
596641
assertSuccessfulSyncProcess(
597642
spawnJavaScriptSync(
598-
[
643+
buildSentrySourcemapsUploadArgs(
599644
sentryCliPath,
600-
'releases',
601-
'files',
645+
bundleName,
646+
outputFolder,
602647
version,
603-
'upload-sourcemaps',
604-
'--strip-prefix',
605-
path.join(process.cwd(), outputFolder),
606-
path.join(outputFolder, bundleName),
607-
path.join(outputFolder, `${bundleName}.map`),
608-
],
648+
supportsStandaloneSentrySourcemapsUpload(sentryCliPath),
649+
),
609650
{
610651
stdio: 'inherit',
611652
},

src/utils/app-info-parser/ipa.ts

Lines changed: 65 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
const parsePlist = require('plist').parse;
21
const parseBplist = require('bplist-parser').parseBuffer;
32
const cgbiToPng = require('cgbi-to-png');
43

@@ -8,57 +7,77 @@ import { Zip } from './zip';
87
const PlistName = /payload\/[^/]+?.app\/info.plist$/i;
98
const ProvisionName = /payload\/.+?\.app\/embedded.mobileprovision/;
109

11-
export class IpaParser extends Zip {
12-
parse(): Promise<any> {
13-
return new Promise((resolve, reject) => {
14-
this.getEntries([PlistName, ProvisionName])
15-
.then((buffers: any) => {
16-
if (!buffers[PlistName as any]) {
17-
throw new Error("Info.plist can't be found.");
18-
}
19-
const plistInfo = this._parsePlist(buffers[PlistName as any]);
20-
const provisionInfo = this._parseProvision(
21-
buffers[ProvisionName as any],
22-
);
23-
plistInfo.mobileProvision = provisionInfo;
10+
type ParsePlist = (content: string) => Record<string, any>;
11+
12+
let parsePlistPromise: Promise<ParsePlist> | undefined;
2413

25-
const iconRegex = new RegExp(
26-
findIpaIconPath(plistInfo).toLowerCase(),
27-
);
28-
this.getEntry(iconRegex)
29-
.then((iconBuffer: any) => {
30-
try {
31-
plistInfo.icon = iconBuffer
32-
? getBase64FromBuffer(cgbiToPng.revert(iconBuffer))
33-
: null;
34-
} catch (err) {
35-
if (isBrowser()) {
36-
plistInfo.icon = iconBuffer
37-
? getBase64FromBuffer(
38-
window.btoa(String.fromCharCode(...iconBuffer)),
39-
)
40-
: null;
41-
} else {
42-
plistInfo.icon = null;
43-
console.warn('[Warning] failed to parse icon: ', err);
44-
}
45-
}
46-
resolve(plistInfo);
47-
})
48-
.catch((e: any) => {
49-
reject(e);
50-
});
51-
})
52-
.catch((e: any) => {
53-
reject(e);
54-
});
14+
function loadParsePlist(): Promise<ParsePlist> {
15+
if (!parsePlistPromise) {
16+
parsePlistPromise = Promise.resolve().then(async () => {
17+
try {
18+
return require('plist').parse as ParsePlist;
19+
} catch (requireError) {
20+
const importModule = new Function(
21+
'specifier',
22+
'return import(specifier)',
23+
) as (specifier: string) => Promise<{
24+
parse?: ParsePlist;
25+
default?: { parse?: ParsePlist };
26+
}>;
27+
const plistModule = await importModule('plist');
28+
const parsePlist = plistModule.parse ?? plistModule.default?.parse;
29+
if (!parsePlist) {
30+
throw requireError;
31+
}
32+
return parsePlist;
33+
}
5534
});
5635
}
36+
return parsePlistPromise;
37+
}
38+
39+
export class IpaParser extends Zip {
40+
async parse(): Promise<any> {
41+
const parsePlist = await loadParsePlist();
42+
const buffers = await this.getEntries([PlistName, ProvisionName]);
43+
if (!buffers[PlistName as any]) {
44+
throw new Error("Info.plist can't be found.");
45+
}
46+
const plistInfo = this._parsePlist(
47+
buffers[PlistName as any] as Buffer,
48+
parsePlist,
49+
);
50+
const provisionInfo = this._parseProvision(
51+
buffers[ProvisionName as any] as Buffer | undefined,
52+
parsePlist,
53+
);
54+
plistInfo.mobileProvision = provisionInfo;
55+
56+
const iconRegex = new RegExp(findIpaIconPath(plistInfo).toLowerCase());
57+
const iconBuffer = await this.getEntry(iconRegex);
58+
try {
59+
plistInfo.icon = iconBuffer
60+
? getBase64FromBuffer(cgbiToPng.revert(iconBuffer))
61+
: null;
62+
} catch (err) {
63+
if (isBrowser()) {
64+
plistInfo.icon = iconBuffer
65+
? getBase64FromBuffer(
66+
window.btoa(String.fromCharCode(...(iconBuffer as Buffer))),
67+
)
68+
: null;
69+
} else {
70+
plistInfo.icon = null;
71+
console.warn('[Warning] failed to parse icon: ', err);
72+
}
73+
}
74+
return plistInfo;
75+
}
5776
/**
5877
* Parse plist
5978
* @param {Buffer} buffer // plist file's buffer
6079
*/
61-
private _parsePlist(buffer: Buffer) {
80+
private _parsePlist(buffer: Buffer, parsePlist: ParsePlist) {
6281
let result: any;
6382
const bufferType = buffer[0];
6483
if (bufferType === 60 || bufferType === 239) {
@@ -74,7 +93,7 @@ export class IpaParser extends Zip {
7493
* parse provision
7594
* @param {Buffer} buffer // provision file's buffer
7695
*/
77-
private _parseProvision(buffer: Buffer | undefined) {
96+
private _parseProvision(buffer: Buffer | undefined, parsePlist: ParsePlist) {
7897
let info: Record<string, any> = {};
7998
if (buffer) {
8099
let content = buffer.toString('utf-8');

tests/app-info-zip.test.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'fs';
33
import os from 'os';
44
import path from 'path';
55
import { ZipFile as YazlZipFile } from 'yazl';
6+
import { IpaParser } from '../src/utils/app-info-parser/ipa';
67
import { Zip } from '../src/utils/app-info-parser/zip';
78

89
function mkTempDir(prefix: string): string {
@@ -11,12 +12,15 @@ function mkTempDir(prefix: string): string {
1112

1213
async function writeZip(
1314
zipPath: string,
14-
entries: Record<string, string>,
15+
entries: Record<string, string | Buffer>,
1516
): Promise<void> {
1617
await new Promise<void>((resolve, reject) => {
1718
const zipFile = new YazlZipFile();
1819
for (const [name, content] of Object.entries(entries)) {
19-
zipFile.addBuffer(Buffer.from(content), name);
20+
zipFile.addBuffer(
21+
Buffer.isBuffer(content) ? content : Buffer.from(content),
22+
name,
23+
);
2024
}
2125
zipFile.outputStream.on('error', reject);
2226
zipFile.outputStream.pipe(fs.createWriteStream(zipPath)).on('close', () => {
@@ -58,4 +62,35 @@ describe('app-info-parser Zip', () => {
5862
);
5963
expect((buffers['res/icon.png'] as Buffer).toString()).toBe('icon');
6064
});
65+
66+
test('parses ipa plist with current plist package exports', async () => {
67+
const ipaPath = path.join(tempRoot, 'app.ipa');
68+
await writeZip(ipaPath, {
69+
'Payload/Test.app/Info.plist': `<?xml version="1.0" encoding="UTF-8"?>
70+
<plist version="1.0">
71+
<dict>
72+
<key>CFBundleIdentifier</key>
73+
<string>cn.reactnative.test</string>
74+
<key>CFBundleShortVersionString</key>
75+
<string>1.2.3</string>
76+
</dict>
77+
</plist>`,
78+
'Payload/Test.app/embedded.mobileprovision': `prefix
79+
<?xml version="1.0" encoding="UTF-8"?>
80+
<plist version="1.0">
81+
<dict>
82+
<key>TeamName</key>
83+
<string>ReactNativeCN</string>
84+
</dict>
85+
</plist>
86+
suffix`,
87+
});
88+
89+
const info = await new IpaParser(ipaPath).parse();
90+
91+
expect(info.CFBundleIdentifier).toBe('cn.reactnative.test');
92+
expect(info.CFBundleShortVersionString).toBe('1.2.3');
93+
expect(info.mobileProvision.TeamName).toBe('ReactNativeCN');
94+
expect(info.icon).toBe(null);
95+
});
6196
});

tests/bundle-runner.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'fs';
33
import os from 'os';
44
import path from 'path';
55
import {
6+
buildSentrySourcemapsUploadArgs,
67
hasProjectDependency,
78
resolveExpoCli,
89
resolveHermesCommand,
@@ -203,3 +204,50 @@ describe('resolveHermesCommand', () => {
203204
expect(resolveHermesCommand(tempRoot)).toBe(hermesCommand);
204205
});
205206
});
207+
208+
describe('buildSentrySourcemapsUploadArgs', () => {
209+
test('uses the Sentry sourcemaps command supported by current CLI versions', () => {
210+
const args = buildSentrySourcemapsUploadArgs(
211+
'/bin/sentry-cli',
212+
'index.android.bundle',
213+
'build/intermedia',
214+
'1.0.0',
215+
);
216+
217+
expect(args).toEqual([
218+
'/bin/sentry-cli',
219+
'sourcemaps',
220+
'upload',
221+
'--release',
222+
'1.0.0',
223+
'--strip-prefix',
224+
path.join(process.cwd(), 'build/intermedia'),
225+
path.join('build/intermedia', 'index.android.bundle'),
226+
path.join('build/intermedia', 'index.android.bundle.map'),
227+
]);
228+
expect(args).not.toContain('files');
229+
expect(args).not.toContain('upload-sourcemaps');
230+
});
231+
232+
test('keeps the legacy releases files command for old Sentry CLI versions', () => {
233+
const args = buildSentrySourcemapsUploadArgs(
234+
'/bin/sentry-cli',
235+
'index.android.bundle',
236+
'build/intermedia',
237+
'1.0.0',
238+
false,
239+
);
240+
241+
expect(args).toEqual([
242+
'/bin/sentry-cli',
243+
'releases',
244+
'files',
245+
'1.0.0',
246+
'upload-sourcemaps',
247+
'--strip-prefix',
248+
path.join(process.cwd(), 'build/intermedia'),
249+
path.join('build/intermedia', 'index.android.bundle'),
250+
path.join('build/intermedia', 'index.android.bundle.map'),
251+
]);
252+
});
253+
});

0 commit comments

Comments
 (0)