Skip to content

Commit 0939980

Browse files
fix: fully respect singleArchFiles option (#152)
Files listed under `singleArchFiles` are allowed to be unique for different platforms so `dupedFiles` should not return them. Fix: #151
1 parent 2e087ef commit 0939980

7 files changed

Lines changed: 221 additions & 22 deletions

File tree

src/file-utils.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,57 @@ import path from 'node:path';
33
import { promises as stream } from 'node:stream';
44

55
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
6+
import { minimatch } from 'minimatch';
67

78
const MACHO_PREFIX = 'Mach-O ';
89

10+
const UNPACKED_ASAR_PATH = path.join('Contents', 'Resources', 'app.asar.unpacked');
11+
912
export enum AppFileType {
1013
MACHO,
1114
PLAIN,
1215
INFO_PLIST,
1316
SNAPSHOT,
1417
APP_CODE,
18+
SINGLE_ARCH,
1519
}
1620

1721
export type AppFile = {
1822
relativePath: string;
1923
type: AppFileType;
2024
};
2125

26+
export type GetAllAppFilesOpts = {
27+
singleArchFiles?: string;
28+
};
29+
30+
const isSingleArchFile = (relativePath: string, opts: GetAllAppFilesOpts): boolean => {
31+
if (opts.singleArchFiles === undefined) {
32+
return false;
33+
}
34+
35+
const unpackedPath = path.relative(UNPACKED_ASAR_PATH, relativePath);
36+
37+
// Outside of app.asar.unpacked
38+
if (unpackedPath.startsWith('..')) {
39+
return false;
40+
}
41+
42+
return minimatch(unpackedPath, opts.singleArchFiles, {
43+
matchBase: true,
44+
});
45+
};
46+
2247
/**
2348
*
2449
* @param appPath Path to the application
2550
*/
26-
export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
51+
export const getAllAppFiles = async (
52+
appPath: string,
53+
opts: GetAllAppFilesOpts,
54+
): Promise<AppFile[]> => {
55+
const unpackedPath = path.join('Contents', 'Resources', 'app.asar.unpacked');
56+
2757
const files: AppFile[] = [];
2858

2959
const visited = new Set<string>();
@@ -35,6 +65,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
3565
const info = await fs.promises.stat(p);
3666
if (info.isSymbolicLink()) return;
3767
if (info.isFile()) {
68+
const relativePath = path.relative(appPath, p);
69+
3870
let fileType = AppFileType.PLAIN;
3971

4072
var fileOutput = '';
@@ -49,6 +81,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
4981
}
5082
if (p.endsWith('.asar')) {
5183
fileType = AppFileType.APP_CODE;
84+
} else if (isSingleArchFile(relativePath, opts)) {
85+
fileType = AppFileType.SINGLE_ARCH;
5286
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
5387
fileType = AppFileType.MACHO;
5488
} else if (p.endsWith('.bin')) {
@@ -58,7 +92,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
5892
}
5993

6094
files.push({
61-
relativePath: path.relative(appPath, p),
95+
relativePath,
6296
type: fileType,
6397
});
6498
}

src/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ export type MakeUniversalOpts = {
7575
};
7676

7777
const dupedFiles = (files: AppFile[]) =>
78-
files.filter((f) => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
78+
files.filter(
79+
(f) =>
80+
f.type !== AppFileType.SNAPSHOT &&
81+
f.type !== AppFileType.APP_CODE &&
82+
f.type !== AppFileType.SINGLE_ARCH,
83+
);
7984

8085
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
8186
d('making a universal app with options', opts);
@@ -121,8 +126,8 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
121126

122127
const uniqueToX64: string[] = [];
123128
const uniqueToArm64: string[] = [];
124-
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp));
125-
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath));
129+
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp), opts);
130+
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath), opts);
126131

127132
for (const file of dupedFiles(x64Files)) {
128133
if (!arm64Files.some((f) => f.relativePath === file.relativePath))
@@ -143,7 +148,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
143148
);
144149
}
145150

146-
for (const file of x64Files.filter((f) => f.type === AppFileType.PLAIN)) {
151+
// Single Arch files are copied as is without processing.
152+
const multiArchFiles = x64Files.filter((f) => f.type !== AppFileType.SINGLE_ARCH);
153+
for (const file of multiArchFiles.filter((f) => f.type === AppFileType.PLAIN)) {
147154
const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
148155
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
149156
if (x64Sha !== arm64Sha) {
@@ -159,7 +166,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
159166
}
160167
}
161168
const knownMergedMachOFiles = new Set();
162-
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) {
169+
for (const machOFile of multiArchFiles.filter((f) => f.type === AppFileType.MACHO)) {
163170
const first = await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath));
164171
const second = await fs.promises.realpath(
165172
path.resolve(opts.arm64AppPath, machOFile.relativePath),
@@ -355,9 +362,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
355362
}
356363
}
357364

358-
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'));
365+
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'), opts);
359366

360-
const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST);
367+
const plistFiles = multiArchFiles.filter((f) => f.type === AppFileType.INFO_PLIST);
361368
for (const plistFile of plistFiles) {
362369
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
363370
const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath);

src/integrity.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ export interface AsarIntegrity {
1717
[key: string]: HeaderHash;
1818
}
1919

20-
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
20+
export type ComputeIntegrityDataOpts = {
21+
singleArchFiles?: string;
22+
};
23+
24+
export async function computeIntegrityData(
25+
contentsPath: string,
26+
opts: ComputeIntegrityDataOpts,
27+
): Promise<AsarIntegrity> {
2128
const root = await fs.promises.realpath(contentsPath);
2229

2330
const resourcesRelativePath = 'Resources';
2431
const resourcesPath = path.resolve(root, resourcesRelativePath);
2532

26-
const resources = await getAllAppFiles(resourcesPath);
33+
const resources = await getAllAppFiles(resourcesPath, opts);
2734
const resourceAsars = resources
2835
.filter((file) => file.type === AppFileType.APP_CODE)
2936
.reduce<IntegrityMap>(

test/__snapshots__/index.spec.ts.snap

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,86 @@ exports[`makeUniversalApp > asar mode > should merge two different asars when \`
339339
}
340340
`;
341341

342+
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 1`] = `
343+
{
344+
"files": {
345+
"hello-world-arm64": "<stripped>",
346+
"hello-world-x64": "<stripped>",
347+
"index.js": {
348+
"integrity": {
349+
"algorithm": "SHA256",
350+
"blockSize": 4194304,
351+
"blocks": [
352+
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
353+
],
354+
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
355+
},
356+
"size": 66,
357+
},
358+
"package.json": {
359+
"integrity": {
360+
"algorithm": "SHA256",
361+
"blockSize": 4194304,
362+
"blocks": [
363+
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
364+
],
365+
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
366+
},
367+
"size": 41,
368+
},
369+
"private": {
370+
"files": {
371+
"var": {
372+
"files": {
373+
"app": {
374+
"files": {
375+
"file.txt": {
376+
"link": "private/var/file.txt",
377+
},
378+
},
379+
},
380+
"file.txt": {
381+
"integrity": {
382+
"algorithm": "SHA256",
383+
"blockSize": 4194304,
384+
"blocks": [
385+
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
386+
],
387+
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
388+
},
389+
"size": 11,
390+
},
391+
},
392+
},
393+
},
394+
},
395+
"var": {
396+
"link": "private/var",
397+
},
398+
},
399+
}
400+
`;
401+
402+
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 2`] = `[]`;
403+
404+
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 3`] = `
405+
[
406+
"hello-world-arm64",
407+
"hello-world-x64",
408+
]
409+
`;
410+
411+
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 4`] = `
412+
{
413+
"Contents/Info.plist": {
414+
"Resources/app.asar": {
415+
"algorithm": "SHA256",
416+
"hash": "<stripped>",
417+
},
418+
},
419+
}
420+
`;
421+
342422
exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = `
343423
{
344424
"files": {

test/file-utils.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ describe('file-utils', () => {
1212
let noAsarFiles: AppFile[];
1313

1414
beforeAll(async () => {
15-
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'));
16-
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'));
15+
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'), {});
16+
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'), {});
1717
});
1818

1919
it('should correctly identify plist files', async () => {

test/index.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,65 @@ describe('makeUniversalApp', () => {
142142
},
143143
);
144144

145+
it(
146+
'should merge two different asars with native files when `mergeASARs` is enabled',
147+
{ timeout: VERIFY_APP_TIMEOUT },
148+
async () => {
149+
const x64AppPath = await generateNativeApp({
150+
appNameWithExtension: 'SingleArchFiles-x64.app',
151+
arch: 'x64',
152+
createAsar: true,
153+
singleArchBindings: true,
154+
});
155+
const arm64AppPath = await generateNativeApp({
156+
appNameWithExtension: 'SingleArchFiles-arm64.app',
157+
arch: 'arm64',
158+
createAsar: true,
159+
singleArchBindings: true,
160+
});
161+
const out = path.resolve(appsOutPath, 'SingleArchFiles.app');
162+
await makeUniversalApp({
163+
x64AppPath,
164+
arm64AppPath,
165+
outAppPath: out,
166+
mergeASARs: true,
167+
singleArchFiles: 'hello-world-*',
168+
});
169+
await verifyApp(out, true);
170+
},
171+
);
172+
173+
it(
174+
'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique native file',
175+
{ timeout: VERIFY_APP_TIMEOUT },
176+
async () => {
177+
const x64AppPath = await generateNativeApp({
178+
appNameWithExtension: 'SingleArchFiles-2-x64.app',
179+
arch: 'x64',
180+
createAsar: true,
181+
singleArchBindings: true,
182+
});
183+
const arm64AppPath = await generateNativeApp({
184+
appNameWithExtension: 'SingleArchFiles-2-arm64.app',
185+
arch: 'arm64',
186+
createAsar: true,
187+
singleArchBindings: true,
188+
});
189+
const out = path.resolve(appsOutPath, 'SingleArchFiles-2.app');
190+
await expect(
191+
makeUniversalApp({
192+
x64AppPath,
193+
arm64AppPath,
194+
outAppPath: out,
195+
mergeASARs: true,
196+
singleArchFiles: 'bad-rule',
197+
}),
198+
).rejects.toThrow(
199+
/the number of mach-o files is not the same between the arm64 and x64 builds/,
200+
);
201+
},
202+
);
203+
145204
it(
146205
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
147206
{ timeout: VERIFY_APP_TIMEOUT },

0 commit comments

Comments
 (0)