fix: Skip lipo if native module is already universal. Add native module fixtures for lipo tests (#126)
* fix: when native modules are already universal, don't lipo. adds `node-mac-permissions` fixture from https://github.com/codebytere/node-mac-permissions and resolves 3 `it.todo` test cases * add test `different app dirs with different macho files (shim and lipo)` * add additional test * PR feedback * gotta close `fd` * use `stream` to read first 4 bytes. copy native fixture before packing into asar to leverage `unpack: "**/*.node"` properly. * convert params to object * rename `createTestApp` to `createStagingAppDir` and add jsdoc to the function * compiler error from merge conflict * update snapshots * update snapshots * only check x64Content since it's the tmp app * compile macho binaries at runtime using hellow-world.c for fixtures in lipo tests * Update jest.setup.ts Co-authored-by: Erik Moura <erikian@erikian.dev> * Update jest.setup.ts Co-authored-by: Erik Moura <erikian@erikian.dev> * remove unstable properties for specific keys * force redo * update snapshots * stripping only hello-world from snapshot and only hash from macho-specific asar integrity * optimize logic :) --------- Co-authored-by: Erik Moura <erikian@erikian.dev>
This commit is contained in:
@@ -1,10 +1,34 @@
|
||||
import { execFileSync } from 'child_process';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { appsDir, asarsDir, templateApp } from './test/util';
|
||||
import { appsDir, asarsDir, fixtureDir, templateApp } from './test/util';
|
||||
|
||||
// generates binaries from hello-world.c
|
||||
// hello-world-universal, hello-world-x86_64, hello-world-arm64
|
||||
const generateMachO = () => {
|
||||
const src = path.resolve(fixtureDir, 'hello-world.c');
|
||||
|
||||
const outputFiles = ['x86_64', 'arm64'].map((arch) => {
|
||||
const machO = path.resolve(appsDir, `hello-world-${arch === 'x86_64' ? 'x64' : arch}`);
|
||||
execFileSync('clang', ['-arch', arch, '-o', machO, src]);
|
||||
return machO;
|
||||
});
|
||||
|
||||
execFileSync('lipo', [
|
||||
...outputFiles,
|
||||
'-create',
|
||||
'-output',
|
||||
path.resolve(appsDir, 'hello-world-universal'),
|
||||
]);
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
await fs.remove(appsDir);
|
||||
await fs.mkdirp(appsDir);
|
||||
|
||||
// generate mach-o binaries to be leveraged in lipo tests
|
||||
generateMachO();
|
||||
|
||||
await templateApp('Arm64Asar.app', 'arm64', async (appPath) => {
|
||||
await fs.copy(
|
||||
path.resolve(asarsDir, 'app.asar'),
|
||||
|
||||
@@ -148,14 +148,13 @@ export const mergeASARs = async ({
|
||||
const x64Content = asar.extractFile(x64AsarPath, file);
|
||||
const arm64Content = asar.extractFile(arm64AsarPath, file);
|
||||
|
||||
// Skip file if the same content
|
||||
if (x64Content.compare(arm64Content) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
MACHO_UNIVERSAL_MAGIC.has(x64Content.readUInt32LE(0)) &&
|
||||
MACHO_UNIVERSAL_MAGIC.has(arm64Content.readUInt32LE(0))
|
||||
) {
|
||||
// Skip universal Mach-O files.
|
||||
if (isUniversalMachO(x64Content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -223,3 +222,7 @@ export const mergeASARs = async ({
|
||||
await Promise.all([fs.remove(x64Dir), fs.remove(arm64Dir)]);
|
||||
}
|
||||
};
|
||||
|
||||
export const isUniversalMachO = (fileContent: Buffer) => {
|
||||
return MACHO_UNIVERSAL_MAGIC.has(fileContent.readUInt32LE(0));
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { promises as stream } from 'node:stream';
|
||||
|
||||
const MACHO_PREFIX = 'Mach-O ';
|
||||
|
||||
@@ -71,3 +72,14 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
export const readMachOHeader = async (path: string) => {
|
||||
const chunks: Buffer[] = [];
|
||||
// no need to read the entire file, we only need the first 4 bytes of the file to determine the header
|
||||
await stream.pipeline(fs.createReadStream(path, { start: 0, end: 3 }), async function* (source) {
|
||||
for await (const chunk of source) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
});
|
||||
return Buffer.concat(chunks);
|
||||
};
|
||||
|
||||
17
src/index.ts
17
src/index.ts
@@ -1,14 +1,14 @@
|
||||
import { spawn } from '@malept/cross-spawn-promise';
|
||||
import * as asar from '@electron/asar';
|
||||
import { spawn } from '@malept/cross-spawn-promise';
|
||||
import * as dircompare from 'dir-compare';
|
||||
import * as fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as plist from 'plist';
|
||||
import * as dircompare from 'dir-compare';
|
||||
|
||||
import { AppFile, AppFileType, getAllAppFiles } from './file-utils';
|
||||
import { AsarMode, detectAsarMode, mergeASARs } from './asar-utils';
|
||||
import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils';
|
||||
import { AppFile, AppFileType, getAllAppFiles, readMachOHeader } from './file-utils';
|
||||
import { sha } from './sha';
|
||||
import { d } from './debug';
|
||||
import { computeIntegrityData } from './integrity';
|
||||
@@ -162,6 +162,15 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
const first = await fs.realpath(path.resolve(tmpApp, machOFile.relativePath));
|
||||
const second = await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath));
|
||||
|
||||
if (
|
||||
isUniversalMachO(await readMachOHeader(first)) &&
|
||||
isUniversalMachO(await readMachOHeader(second))
|
||||
) {
|
||||
d(machOFile.relativePath, `is already universal across builds, skipping lipo`);
|
||||
knownMergedMachOFiles.add(machOFile.relativePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const x64Sha = await sha(path.resolve(opts.x64AppPath, machOFile.relativePath));
|
||||
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, machOFile.relativePath));
|
||||
if (x64Sha === arm64Sha) {
|
||||
|
||||
@@ -620,6 +620,222 @@ exports[`makeUniversalApp force packages successfully if \`out\` bundle already
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
|
||||
],
|
||||
"hash": "f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
|
||||
},
|
||||
"size": 1063,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
|
||||
],
|
||||
"hash": "2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
|
||||
},
|
||||
"size": 33,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 2`] = `
|
||||
[
|
||||
"private/var/i-aint-got-no-rhythm.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 3`] = `
|
||||
[
|
||||
"hello-world",
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
"private/var/i-aint-got-no-rhythm.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 4`] = `
|
||||
[
|
||||
"hello-world",
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
"private/var/hello-world.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 5`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "<stripped>",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
|
||||
],
|
||||
"hash": "f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
|
||||
},
|
||||
"size": 1063,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
|
||||
],
|
||||
"hash": "2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
|
||||
},
|
||||
"size": 33,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 2`] = `
|
||||
[
|
||||
"private/var/i-aint-got-no-rhythm.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 3`] = `
|
||||
[
|
||||
"hello-world",
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
"private/var/i-aint-got-no-rhythm.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 4`] = `
|
||||
[
|
||||
"hello-world",
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
"private/var/hello-world.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 5`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "<stripped>",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode identical app dirs with different macho files (e.g. do not shim, but still lipo) 1`] = `
|
||||
[
|
||||
"hello-world",
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode identical app dirs with different macho files (e.g. do not shim, but still lipo) 2`] = `
|
||||
{
|
||||
"Contents/Info.plist": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 1`] = `
|
||||
[
|
||||
"hello-world",
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 2`] = `
|
||||
{
|
||||
"Contents/Info.plist": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 1`] = `
|
||||
[
|
||||
"index.js",
|
||||
@@ -720,3 +936,81 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 5`]
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp works for lipo binary resources 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"hello-world": "<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 works for lipo binary resources 2`] = `[]`;
|
||||
|
||||
exports[`makeUniversalApp works for lipo binary resources 3`] = `
|
||||
[
|
||||
"hello-world",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp works for lipo binary resources 4`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "<stripped>",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
6
test/fixtures/hello-world.c
vendored
Normal file
6
test/fixtures/hello-world.c
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
printf("Hello, World!\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -2,7 +2,13 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { makeUniversalApp } from '../dist/cjs/index';
|
||||
import { createTestApp, templateApp, VERIFY_APP_TIMEOUT, verifyApp } from './util';
|
||||
import {
|
||||
createStagingAppDir,
|
||||
generateNativeApp,
|
||||
templateApp,
|
||||
VERIFY_APP_TIMEOUT,
|
||||
verifyApp,
|
||||
} from './util';
|
||||
import { createPackage, createPackageWithOptions } from '@electron/asar';
|
||||
import { afterEach, describe, expect, it } from '@jest/globals';
|
||||
|
||||
@@ -28,7 +34,26 @@ describe('makeUniversalApp', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.todo('works for lipo binary resources');
|
||||
it(
|
||||
'works for lipo binary resources',
|
||||
async () => {
|
||||
const x64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'LipoX64.app',
|
||||
arch: 'x64',
|
||||
createAsar: true,
|
||||
});
|
||||
const arm64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'LipoArm64.app',
|
||||
arch: 'arm64',
|
||||
createAsar: true,
|
||||
});
|
||||
|
||||
const out = path.resolve(appsOutPath, 'Lipo.app');
|
||||
await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out, mergeASARs: true });
|
||||
await verifyApp(out, true);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
describe('force', () => {
|
||||
it('throws an error if `out` bundle already exists and `force` is `false`', async () => {
|
||||
@@ -46,7 +71,7 @@ describe('makeUniversalApp', () => {
|
||||
it(
|
||||
'packages successfully if `out` bundle already exists and `force` is `true`',
|
||||
async () => {
|
||||
const out = path.resolve(appsOutPath, 'Error.app');
|
||||
const out = path.resolve(appsOutPath, 'NoError.app');
|
||||
await fs.mkdirp(out);
|
||||
await makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
|
||||
@@ -126,7 +151,7 @@ describe('makeUniversalApp', () => {
|
||||
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
|
||||
async () => {
|
||||
const arm64AppPath = await templateApp('Arm64-1.app', 'arm64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('Arm64-1');
|
||||
const { testPath } = await createStagingAppDir('Arm64-1');
|
||||
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
|
||||
await templateApp('SubApp-1.app', 'arm64', async (subArm64AppPath) => {
|
||||
await fs.move(
|
||||
@@ -136,7 +161,7 @@ describe('makeUniversalApp', () => {
|
||||
});
|
||||
});
|
||||
const x64AppPath = await templateApp('X64-1.app', 'x64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('X64-1');
|
||||
const { testPath } = await createStagingAppDir('X64-1');
|
||||
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
|
||||
await templateApp('SubApp-1.app', 'x64', async (subArm64AppPath) => {
|
||||
await fs.move(
|
||||
@@ -165,7 +190,7 @@ describe('makeUniversalApp', () => {
|
||||
'should shim asars with different unpacked dirs',
|
||||
async () => {
|
||||
const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('UnpackedAppArm64');
|
||||
const { testPath } = await createStagingAppDir('UnpackedAppArm64');
|
||||
await createPackageWithOptions(
|
||||
testPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
@@ -177,7 +202,7 @@ describe('makeUniversalApp', () => {
|
||||
});
|
||||
|
||||
const x64AppPath = await templateApp('UnpackedX64.app', 'x64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('UnpackedAppX64');
|
||||
const { testPath } = await createStagingAppDir('UnpackedAppX64');
|
||||
await createPackageWithOptions(
|
||||
testPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
@@ -199,7 +224,7 @@ describe('makeUniversalApp', () => {
|
||||
it(
|
||||
'should generate AsarIntegrity for all asars in the application',
|
||||
async () => {
|
||||
const { testPath } = await createTestApp('app-2');
|
||||
const { testPath } = await createStagingAppDir('app-2');
|
||||
const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
|
||||
await createPackage(testPath, testAsarPath);
|
||||
|
||||
@@ -255,14 +280,16 @@ describe('makeUniversalApp', () => {
|
||||
'should shim two different app folders',
|
||||
async () => {
|
||||
const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('shimArm64', {
|
||||
const { testPath } = await createStagingAppDir('shimArm64', {
|
||||
'i-aint-got-no-rhythm.bin': 'boomshakalaka',
|
||||
});
|
||||
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'));
|
||||
});
|
||||
|
||||
const x64AppPath = await templateApp('ShimX64.app', 'x64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('shimX64', { 'hello-world.bin': 'Hello World' });
|
||||
const { testPath } = await createStagingAppDir('shimX64', {
|
||||
'hello-world.bin': 'Hello World',
|
||||
});
|
||||
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'));
|
||||
});
|
||||
|
||||
@@ -276,9 +303,117 @@ describe('makeUniversalApp', () => {
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'different app dirs with different macho files (shim and lipo)',
|
||||
async () => {
|
||||
const x64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'DifferentMachoAppX64-1.app',
|
||||
arch: 'x64',
|
||||
createAsar: false,
|
||||
additionalFiles: {
|
||||
'hello-world.bin': 'Hello World',
|
||||
},
|
||||
});
|
||||
const arm64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'DifferentMachoAppArm64-1.app',
|
||||
arch: 'arm64',
|
||||
createAsar: false,
|
||||
additionalFiles: {
|
||||
'i-aint-got-no-rhythm.bin': 'boomshakalaka',
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Add tests for
|
||||
// * different app dirs with different macho files
|
||||
// * identical app dirs with universal macho files
|
||||
const outAppPath = path.resolve(appsOutPath, 'DifferentMachoApp1.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath,
|
||||
});
|
||||
await verifyApp(outAppPath, true);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"different app dirs with universal macho files (shim but don't lipo)",
|
||||
async () => {
|
||||
const x64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'DifferentButUniversalMachoAppX64-2.app',
|
||||
arch: 'x64',
|
||||
createAsar: false,
|
||||
nativeModuleArch: 'universal',
|
||||
additionalFiles: {
|
||||
'hello-world.bin': 'Hello World',
|
||||
},
|
||||
});
|
||||
const arm64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'DifferentButUniversalMachoAppArm64-2.app',
|
||||
arch: 'arm64',
|
||||
createAsar: false,
|
||||
nativeModuleArch: 'universal',
|
||||
additionalFiles: {
|
||||
'i-aint-got-no-rhythm.bin': 'boomshakalaka',
|
||||
},
|
||||
});
|
||||
|
||||
const outAppPath = path.resolve(appsOutPath, 'DifferentButUniversalMachoApp.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath,
|
||||
});
|
||||
await verifyApp(outAppPath, true);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'identical app dirs with different macho files (e.g. do not shim, but still lipo)',
|
||||
async () => {
|
||||
const x64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'DifferentMachoAppX64-2.app',
|
||||
arch: 'x64',
|
||||
createAsar: false,
|
||||
});
|
||||
const arm64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'DifferentMachoAppArm64-2.app',
|
||||
arch: 'arm64',
|
||||
createAsar: false,
|
||||
});
|
||||
|
||||
const out = path.resolve(appsOutPath, 'DifferentMachoApp2.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath: out,
|
||||
});
|
||||
await verifyApp(out, true);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir)',
|
||||
async () => {
|
||||
const x64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'UniversalMachoAppX64.app',
|
||||
arch: 'x64',
|
||||
createAsar: false,
|
||||
nativeModuleArch: 'universal',
|
||||
});
|
||||
const arm64AppPath = await generateNativeApp({
|
||||
appNameWithExtension: 'UniversalMachoAppArm64.app',
|
||||
arch: 'arm64',
|
||||
createAsar: false,
|
||||
nativeModuleArch: 'universal',
|
||||
});
|
||||
|
||||
const out = path.resolve(appsOutPath, 'UniversalMachoApp.app');
|
||||
await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out });
|
||||
await verifyApp(out, true);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
88
test/util.ts
88
test/util.ts
@@ -5,7 +5,7 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import plist from 'plist';
|
||||
import * as fileUtils from '../dist/cjs/file-utils';
|
||||
import { getRawHeader } from '@electron/asar';
|
||||
import { createPackageWithOptions, getRawHeader } from '@electron/asar';
|
||||
|
||||
declare const expect: typeof import('@jest/globals').expect;
|
||||
|
||||
@@ -14,11 +14,12 @@ declare const expect: typeof import('@jest/globals').expect;
|
||||
// plus some tests create fixtures at runtime
|
||||
export const VERIFY_APP_TIMEOUT = 80 * 1000;
|
||||
|
||||
export const asarsDir = path.resolve(__dirname, 'fixtures', 'asars');
|
||||
export const appsDir = path.resolve(__dirname, 'fixtures', 'apps');
|
||||
export const fixtureDir = path.resolve(__dirname, 'fixtures');
|
||||
export const asarsDir = path.resolve(fixtureDir, 'asars');
|
||||
export const appsDir = path.resolve(fixtureDir, 'apps');
|
||||
export const appsOutPath = path.resolve(appsDir, 'out');
|
||||
|
||||
export const verifyApp = async (appPath: string) => {
|
||||
export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho = false) => {
|
||||
await ensureUniversal(appPath);
|
||||
|
||||
const resourcesDir = path.resolve(appPath, 'Contents', 'Resources');
|
||||
@@ -29,7 +30,9 @@ export const verifyApp = async (appPath: string) => {
|
||||
for await (const asar of asars) {
|
||||
// verify header
|
||||
const asarFs = getRawHeader(path.resolve(resourcesDir, asar));
|
||||
expect(removeUnstableProperties(asarFs.header)).toMatchSnapshot();
|
||||
expect(
|
||||
removeUnstableProperties(asarFs.header, containsRuntimeGeneratedMacho ? ['hello-world'] : []),
|
||||
).toMatchSnapshot();
|
||||
}
|
||||
|
||||
// check all app and unpacked dirs (incl. shimmed)
|
||||
@@ -66,12 +69,14 @@ export const verifyApp = async (appPath: string) => {
|
||||
for (let i = 0; i < integrity.length; i++) {
|
||||
const relativePath = infoPlists[i];
|
||||
const asarIntegrity = integrity[i];
|
||||
integrityMap[relativePath] = asarIntegrity;
|
||||
// note: `infoPlistsToIgnore` will not have integrity in sub-app plists
|
||||
integrityMap[relativePath] = asarIntegrity
|
||||
? removeUnstableProperties(asarIntegrity, containsRuntimeGeneratedMacho ? ['hash'] : [])
|
||||
: undefined;
|
||||
}
|
||||
expect(integrityMap).toMatchSnapshot();
|
||||
};
|
||||
|
||||
// note: `infoPlistsToIgnore` will not have integrity in sub-app plists
|
||||
const extractAsarIntegrity = async (infoPlist: string) => {
|
||||
const { ElectronAsarIntegrity: integrity, ...otherData } = plist.parse(
|
||||
await fs.readFile(infoPlist, 'utf-8'),
|
||||
@@ -104,9 +109,29 @@ export const toSystemIndependentPath = (s: string): string => {
|
||||
return path.sep === '/' ? s : s.replace(/\\/g, '/');
|
||||
};
|
||||
|
||||
export const removeUnstableProperties = (data: any) => {
|
||||
export const removeUnstableProperties = (data: any, stripKeys: string[]) => {
|
||||
const removeKeysRecursively: (obj: any, keysToRemove: string[]) => any = (obj, keysToRemove) => {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
// if the value is an array, map over it
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item: any) => removeKeysRecursively(item, keysToRemove));
|
||||
}
|
||||
return Object.keys(obj).reduce<any>((acc, key) => {
|
||||
// if the value of the current key is another object, make a recursive call to remove the key from the nested object
|
||||
if (!keysToRemove.includes(key)) {
|
||||
acc[key] = removeKeysRecursively(obj[key], keysToRemove);
|
||||
} else {
|
||||
acc[key] = '<stripped>';
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const filteredData = removeKeysRecursively(data, stripKeys);
|
||||
return JSON.parse(
|
||||
JSON.stringify(data, (name, value) => {
|
||||
JSON.stringify(filteredData, (name, value) => {
|
||||
if (name === 'offset') {
|
||||
return undefined;
|
||||
}
|
||||
@@ -116,6 +141,10 @@ export const removeUnstableProperties = (data: any) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an app directory at runtime for usage:
|
||||
* - `testPath` can be used with `asar.createPackage`. Just set the output `.asar` path to `Test.app/Contents/Resources/<asar_name>.asar`
|
||||
* - `testPath` can be utilized for logic paths involving `AsarMode.NO_ASAR` and copied directly to `Test.app/Contents/Resources`
|
||||
*
|
||||
* Directory structure:
|
||||
* testName
|
||||
* ├── private
|
||||
@@ -127,7 +156,7 @@ export const removeUnstableProperties = (data: any) => {
|
||||
* ├── index.js
|
||||
* ├── package.json
|
||||
*/
|
||||
export const createTestApp = async (
|
||||
export const createStagingAppDir = async (
|
||||
testName: string | undefined,
|
||||
additionalFiles: Record<string, string> = {},
|
||||
) => {
|
||||
@@ -181,3 +210,42 @@ export const templateApp = async (
|
||||
|
||||
return appPath;
|
||||
};
|
||||
|
||||
export const generateNativeApp = async (options: {
|
||||
appNameWithExtension: string;
|
||||
arch: string;
|
||||
createAsar: boolean;
|
||||
nativeModuleArch?: string;
|
||||
additionalFiles?: Record<string, string>;
|
||||
}) => {
|
||||
const {
|
||||
appNameWithExtension,
|
||||
arch,
|
||||
createAsar,
|
||||
nativeModuleArch = arch,
|
||||
additionalFiles,
|
||||
} = options;
|
||||
const appPath = await templateApp(appNameWithExtension, arch, async (appPath) => {
|
||||
const resources = path.join(appPath, 'Contents', 'Resources');
|
||||
const resourcesApp = path.resolve(resources, 'app');
|
||||
if (!fs.existsSync(resourcesApp)) {
|
||||
await fs.mkdir(resourcesApp);
|
||||
}
|
||||
const { testPath } = await createStagingAppDir(
|
||||
path.basename(appNameWithExtension, '.app'),
|
||||
additionalFiles,
|
||||
);
|
||||
await fs.copy(
|
||||
path.join(appsDir, `hello-world-${nativeModuleArch}`),
|
||||
path.join(testPath, 'hello-world'),
|
||||
);
|
||||
if (createAsar) {
|
||||
await createPackageWithOptions(testPath, path.resolve(resources, 'app.asar'), {
|
||||
unpack: '**/hello-world',
|
||||
});
|
||||
} else {
|
||||
await fs.copy(testPath, resourcesApp);
|
||||
}
|
||||
});
|
||||
return appPath;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user