fix: fully respect singleArchFiles option (#152)
Some checks failed
Publish documentation / docs (push) Failing after 13s
Some checks failed
Publish documentation / docs (push) Failing after 13s
Files listed under `singleArchFiles` are allowed to be unique for different platforms so `dupedFiles` should not return them. Fix: #151
This commit is contained in:
@@ -3,15 +3,19 @@ import path from 'node:path';
|
||||
import { promises as stream } from 'node:stream';
|
||||
|
||||
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
|
||||
import { minimatch } from 'minimatch';
|
||||
|
||||
const MACHO_PREFIX = 'Mach-O ';
|
||||
|
||||
const UNPACKED_ASAR_PATH = path.join('Contents', 'Resources', 'app.asar.unpacked');
|
||||
|
||||
export enum AppFileType {
|
||||
MACHO,
|
||||
PLAIN,
|
||||
INFO_PLIST,
|
||||
SNAPSHOT,
|
||||
APP_CODE,
|
||||
SINGLE_ARCH,
|
||||
}
|
||||
|
||||
export type AppFile = {
|
||||
@@ -19,11 +23,37 @@ export type AppFile = {
|
||||
type: AppFileType;
|
||||
};
|
||||
|
||||
export type GetAllAppFilesOpts = {
|
||||
singleArchFiles?: string;
|
||||
};
|
||||
|
||||
const isSingleArchFile = (relativePath: string, opts: GetAllAppFilesOpts): boolean => {
|
||||
if (opts.singleArchFiles === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unpackedPath = path.relative(UNPACKED_ASAR_PATH, relativePath);
|
||||
|
||||
// Outside of app.asar.unpacked
|
||||
if (unpackedPath.startsWith('..')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return minimatch(unpackedPath, opts.singleArchFiles, {
|
||||
matchBase: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param appPath Path to the application
|
||||
*/
|
||||
export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
export const getAllAppFiles = async (
|
||||
appPath: string,
|
||||
opts: GetAllAppFilesOpts,
|
||||
): Promise<AppFile[]> => {
|
||||
const unpackedPath = path.join('Contents', 'Resources', 'app.asar.unpacked');
|
||||
|
||||
const files: AppFile[] = [];
|
||||
|
||||
const visited = new Set<string>();
|
||||
@@ -35,6 +65,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
const info = await fs.promises.stat(p);
|
||||
if (info.isSymbolicLink()) return;
|
||||
if (info.isFile()) {
|
||||
const relativePath = path.relative(appPath, p);
|
||||
|
||||
let fileType = AppFileType.PLAIN;
|
||||
|
||||
var fileOutput = '';
|
||||
@@ -49,6 +81,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
}
|
||||
if (p.endsWith('.asar')) {
|
||||
fileType = AppFileType.APP_CODE;
|
||||
} else if (isSingleArchFile(relativePath, opts)) {
|
||||
fileType = AppFileType.SINGLE_ARCH;
|
||||
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
|
||||
fileType = AppFileType.MACHO;
|
||||
} else if (p.endsWith('.bin')) {
|
||||
@@ -58,7 +92,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
}
|
||||
|
||||
files.push({
|
||||
relativePath: path.relative(appPath, p),
|
||||
relativePath,
|
||||
type: fileType,
|
||||
});
|
||||
}
|
||||
|
||||
21
src/index.ts
21
src/index.ts
@@ -75,7 +75,12 @@ export type MakeUniversalOpts = {
|
||||
};
|
||||
|
||||
const dupedFiles = (files: AppFile[]) =>
|
||||
files.filter((f) => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
|
||||
files.filter(
|
||||
(f) =>
|
||||
f.type !== AppFileType.SNAPSHOT &&
|
||||
f.type !== AppFileType.APP_CODE &&
|
||||
f.type !== AppFileType.SINGLE_ARCH,
|
||||
);
|
||||
|
||||
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
|
||||
d('making a universal app with options', opts);
|
||||
@@ -121,8 +126,8 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
|
||||
const uniqueToX64: string[] = [];
|
||||
const uniqueToArm64: string[] = [];
|
||||
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp));
|
||||
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath));
|
||||
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp), opts);
|
||||
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath), opts);
|
||||
|
||||
for (const file of dupedFiles(x64Files)) {
|
||||
if (!arm64Files.some((f) => f.relativePath === file.relativePath))
|
||||
@@ -143,7 +148,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
);
|
||||
}
|
||||
|
||||
for (const file of x64Files.filter((f) => f.type === AppFileType.PLAIN)) {
|
||||
// Single Arch files are copied as is without processing.
|
||||
const multiArchFiles = x64Files.filter((f) => f.type !== AppFileType.SINGLE_ARCH);
|
||||
for (const file of multiArchFiles.filter((f) => f.type === AppFileType.PLAIN)) {
|
||||
const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
|
||||
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
|
||||
if (x64Sha !== arm64Sha) {
|
||||
@@ -159,7 +166,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
}
|
||||
}
|
||||
const knownMergedMachOFiles = new Set();
|
||||
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) {
|
||||
for (const machOFile of multiArchFiles.filter((f) => f.type === AppFileType.MACHO)) {
|
||||
const first = await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath));
|
||||
const second = await fs.promises.realpath(
|
||||
path.resolve(opts.arm64AppPath, machOFile.relativePath),
|
||||
@@ -355,9 +362,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
}
|
||||
}
|
||||
|
||||
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'));
|
||||
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'), opts);
|
||||
|
||||
const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST);
|
||||
const plistFiles = multiArchFiles.filter((f) => f.type === AppFileType.INFO_PLIST);
|
||||
for (const plistFile of plistFiles) {
|
||||
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
|
||||
const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath);
|
||||
|
||||
@@ -17,13 +17,20 @@ export interface AsarIntegrity {
|
||||
[key: string]: HeaderHash;
|
||||
}
|
||||
|
||||
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
|
||||
export type ComputeIntegrityDataOpts = {
|
||||
singleArchFiles?: string;
|
||||
};
|
||||
|
||||
export async function computeIntegrityData(
|
||||
contentsPath: string,
|
||||
opts: ComputeIntegrityDataOpts,
|
||||
): Promise<AsarIntegrity> {
|
||||
const root = await fs.promises.realpath(contentsPath);
|
||||
|
||||
const resourcesRelativePath = 'Resources';
|
||||
const resourcesPath = path.resolve(root, resourcesRelativePath);
|
||||
|
||||
const resources = await getAllAppFiles(resourcesPath);
|
||||
const resources = await getAllAppFiles(resourcesPath, opts);
|
||||
const resourceAsars = resources
|
||||
.filter((file) => file.type === AppFileType.APP_CODE)
|
||||
.reduce<IntegrityMap>(
|
||||
|
||||
@@ -339,6 +339,86 @@ exports[`makeUniversalApp > asar mode > should merge two different asars when \`
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"hello-world-arm64": "<stripped>",
|
||||
"hello-world-x64": "<stripped>",
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
|
||||
],
|
||||
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
|
||||
},
|
||||
"size": 66,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
],
|
||||
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
},
|
||||
"size": 41,
|
||||
},
|
||||
"private": {
|
||||
"files": {
|
||||
"var": {
|
||||
"files": {
|
||||
"app": {
|
||||
"files": {
|
||||
"file.txt": {
|
||||
"link": "private/var/file.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
"file.txt": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
],
|
||||
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
},
|
||||
"size": 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"var": {
|
||||
"link": "private/var",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 2`] = `[]`;
|
||||
|
||||
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 3`] = `
|
||||
[
|
||||
"hello-world-arm64",
|
||||
"hello-world-x64",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 4`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "<stripped>",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = `
|
||||
{
|
||||
"files": {
|
||||
|
||||
@@ -12,8 +12,8 @@ describe('file-utils', () => {
|
||||
let noAsarFiles: AppFile[];
|
||||
|
||||
beforeAll(async () => {
|
||||
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'));
|
||||
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'));
|
||||
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'), {});
|
||||
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'), {});
|
||||
});
|
||||
|
||||
it('should correctly identify plist files', async () => {
|
||||
|
||||
@@ -142,6 +142,65 @@ describe('makeUniversalApp', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'should merge two different asars with native files when `mergeASARs` is enabled',
|
||||
{ timeout: VERIFY_APP_TIMEOUT },
|
||||
async () => {
|
||||
const x64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'SingleArchFiles-x64.app',
|
||||
arch: 'x64',
|
||||
createAsar: true,
|
||||
singleArchBindings: true,
|
||||
});
|
||||
const arm64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'SingleArchFiles-arm64.app',
|
||||
arch: 'arm64',
|
||||
createAsar: true,
|
||||
singleArchBindings: true,
|
||||
});
|
||||
const out = path.resolve(appsOutPath, 'SingleArchFiles.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath: out,
|
||||
mergeASARs: true,
|
||||
singleArchFiles: 'hello-world-*',
|
||||
});
|
||||
await verifyApp(out, true);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique native file',
|
||||
{ timeout: VERIFY_APP_TIMEOUT },
|
||||
async () => {
|
||||
const x64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'SingleArchFiles-2-x64.app',
|
||||
arch: 'x64',
|
||||
createAsar: true,
|
||||
singleArchBindings: true,
|
||||
});
|
||||
const arm64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'SingleArchFiles-2-arm64.app',
|
||||
arch: 'arm64',
|
||||
createAsar: true,
|
||||
singleArchBindings: true,
|
||||
});
|
||||
const out = path.resolve(appsOutPath, 'SingleArchFiles-2.app');
|
||||
await expect(
|
||||
makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath: out,
|
||||
mergeASARs: true,
|
||||
singleArchFiles: 'bad-rule',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/the number of mach-o files is not the same between the arm64 and x64 builds/,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
|
||||
{ timeout: VERIFY_APP_TIMEOUT },
|
||||
|
||||
30
test/util.ts
30
test/util.ts
@@ -33,7 +33,12 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
|
||||
// verify header
|
||||
const asarFs = getRawHeader(path.resolve(resourcesDir, asar));
|
||||
expect(
|
||||
removeUnstableProperties(asarFs.header, containsRuntimeGeneratedMacho ? ['hello-world'] : []),
|
||||
removeUnstableProperties(
|
||||
asarFs.header,
|
||||
containsRuntimeGeneratedMacho
|
||||
? ['hello-world', 'hello-world-arm64', 'hello-world-x64']
|
||||
: [],
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
}
|
||||
|
||||
@@ -53,7 +58,7 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
|
||||
await verifyFileTree(path.resolve(resourcesDir, dir));
|
||||
}
|
||||
|
||||
const allFiles = await fileUtils.getAllAppFiles(appPath);
|
||||
const allFiles = await fileUtils.getAllAppFiles(appPath, {});
|
||||
const infoPlists = allFiles
|
||||
.filter(
|
||||
(appFile) =>
|
||||
@@ -89,7 +94,7 @@ const extractAsarIntegrity = async (infoPlist: string) => {
|
||||
export const verifyFileTree = async (dirPath: string) => {
|
||||
const { expect } = await import('vitest');
|
||||
|
||||
const dirFiles = await fileUtils.getAllAppFiles(dirPath);
|
||||
const dirFiles = await fileUtils.getAllAppFiles(dirPath, {});
|
||||
const files = dirFiles.map((file) => {
|
||||
const it = path.join(dirPath, file.relativePath);
|
||||
const name = toSystemIndependentPath(file.relativePath);
|
||||
@@ -229,6 +234,7 @@ export const generateNativeApp = async (options: {
|
||||
createAsar: boolean;
|
||||
nativeModuleArch?: string;
|
||||
additionalFiles?: Record<string, string>;
|
||||
singleArchBindings?: boolean;
|
||||
}) => {
|
||||
const {
|
||||
appNameWithExtension,
|
||||
@@ -236,6 +242,7 @@ export const generateNativeApp = async (options: {
|
||||
createAsar,
|
||||
nativeModuleArch = arch,
|
||||
additionalFiles,
|
||||
singleArchBindings,
|
||||
} = options;
|
||||
const appPath = await templateApp(appNameWithExtension, arch, async (appPath) => {
|
||||
const resources = path.join(appPath, 'Contents', 'Resources');
|
||||
@@ -247,14 +254,19 @@ export const generateNativeApp = async (options: {
|
||||
path.basename(appNameWithExtension, '.app'),
|
||||
additionalFiles,
|
||||
);
|
||||
await fs.promises.cp(
|
||||
path.join(appsDir, `hello-world-${nativeModuleArch}`),
|
||||
path.join(testPath, 'hello-world'),
|
||||
{ recursive: true, verbatimSymlinks: true },
|
||||
);
|
||||
let targetBinding: string;
|
||||
if (singleArchBindings) {
|
||||
targetBinding = path.join(testPath, `hello-world-${nativeModuleArch}`);
|
||||
} else {
|
||||
targetBinding = path.join(testPath, 'hello-world');
|
||||
}
|
||||
await fs.promises.cp(path.join(appsDir, `hello-world-${nativeModuleArch}`), targetBinding, {
|
||||
recursive: true,
|
||||
verbatimSymlinks: true,
|
||||
});
|
||||
if (createAsar) {
|
||||
await createPackageWithOptions(testPath, path.resolve(resources, 'app.asar'), {
|
||||
unpack: '**/hello-world',
|
||||
unpack: '**/hello-world*',
|
||||
});
|
||||
} else {
|
||||
await fs.promises.cp(testPath, resourcesApp, { recursive: true, verbatimSymlinks: true });
|
||||
|
||||
Reference in New Issue
Block a user