Skip to content

Commit

Permalink
feat: bump minimum node version to 16 and add tests (#86)
Browse files Browse the repository at this point in the history
* build: add tests

* build: bump deps

* sigh

* build: install rosetta on m1
  • Loading branch information
MarshallOfSound authored Nov 9, 2023
1 parent 02119d5 commit 8e2842b
Show file tree
Hide file tree
Showing 18 changed files with 3,622 additions and 1,463 deletions.
8 changes: 3 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
version: 2.1

orbs:
cfa: continuousauth/npm@1.0.2
node: electronjs/node@1.4.1
cfa: continuousauth/npm@2.0.0
node: electronjs/node@2.1.0

workflows:
test_and_release:
Expand All @@ -13,6 +13,7 @@ workflows:
name: test-mac-<< matrix.node-version >>
override-ci-command: yarn install --frozen-lockfile --ignore-engines
test-steps:
- node/install-rosetta
- run: yarn build
- run: yarn lint
- run: yarn test
Expand All @@ -24,9 +25,6 @@ workflows:
- 20.5.0
- 18.17.0
- 16.20.1
- 14.21.3
- 12.22.12
- 10.24.1
- cfa/release:
requires:
- test
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ dist
entry-asar/*.js*
entry-asar/*.ts
*.app
test/fixtures/apps
coverage
14 changes: 14 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'.': [
'ts-jest',
{
tsconfig: 'tsconfig.jest.json'
}
]
},
globalSetup: './jest.setup.ts'
};
57 changes: 57 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { downloadArtifact } from '@electron/get';
import * as zip from 'cross-zip';
import * as fs from 'fs-extra';
import * as path from 'path';

const asarsDir = path.resolve(__dirname, 'test', 'fixtures', 'asars');
const appsDir = path.resolve(__dirname, 'test', 'fixtures', 'apps');

const templateApp = async (
name: string,
arch: string,
modify: (appPath: string) => Promise<void>,
) => {
const electronZip = await downloadArtifact({
artifactName: 'electron',
version: '27.0.0',
platform: 'darwin',
arch,
});
const appPath = path.resolve(appsDir, name);
zip.unzipSync(electronZip, appsDir);
await fs.rename(path.resolve(appsDir, 'Electron.app'), appPath);
await fs.remove(path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar'));
await modify(appPath);
};

export default async () => {
await fs.remove(appsDir);
await fs.mkdirp(appsDir);
await templateApp('Asar.app', 'arm64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
});

await templateApp('X64Asar.app', 'x64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
});

await templateApp('NoAsar.app', 'arm64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'),
);
});

await templateApp('X64NoAsar.app', 'x64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'),
);
});
};
49 changes: 29 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"url": "https://github.com/electron/universal.git"
},
"engines": {
"node": ">=8.6"
"node": ">=16.4"
},
"files": [
"dist/*",
Expand All @@ -26,36 +26,45 @@
"author": "Samuel Attard",
"scripts": {
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.entry-asar.json",
"lint": "prettier --check \"{src,entry-asar}/**/*.ts\"",
"prettier:write": "prettier --write \"{src,entry-asar}/**/*.ts\"",
"lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
"prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
"prepublishOnly": "npm run build",
"test": "exit 0",
"test": "jest",
"prepare": "husky install"
},
"devDependencies": {
"@continuous-auth/semantic-release-npm": "^3.0.0",
"@types/debug": "^4.1.5",
"@types/fs-extra": "^9.0.4",
"@types/minimatch": "^3.0.5",
"@types/node": "^14.14.7",
"@types/plist": "^3.0.2",
"husky": "^8.0.0",
"lint-staged": "^10.5.1",
"prettier": "^2.1.2",
"typescript": "^4.0.5"
"@continuous-auth/semantic-release-npm": "^4.0.0",
"@electron/get": "^3.0.0",
"@types/cross-zip": "^4.0.1",
"@types/debug": "^4.1.10",
"@types/fs-extra": "^11.0.3",
"@types/jest": "^29.5.7",
"@types/minimatch": "^5.1.2",
"@types/node": "^20.8.10",
"@types/plist": "^3.0.4",
"cross-zip": "^4.0.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"lint-staged": "^15.0.2",
"prettier": "^3.0.3",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2"
},
"dependencies": {
"@electron/asar": "^3.2.1",
"@malept/cross-spawn-promise": "^1.1.0",
"@electron/asar": "^3.2.7",
"@malept/cross-spawn-promise": "^2.0.0",
"debug": "^4.3.1",
"dir-compare": "^3.0.0",
"fs-extra": "^9.0.1",
"minimatch": "^3.0.4",
"plist": "^3.0.4"
"dir-compare": "^4.2.0",
"fs-extra": "^11.1.1",
"minimatch": "^9.0.3",
"plist": "^3.1.0"
},
"lint-staged": {
"*.ts": [
"prettier --write"
]
},
"resolutions": {
"jackspeak": "2.1.1"
}
}
11 changes: 4 additions & 7 deletions src/asar-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { execFileSync } from 'child_process';
import crypto from 'crypto';
import fs from 'fs-extra';
import path from 'path';
import minimatch from 'minimatch';
import { minimatch } from 'minimatch';
import os from 'os';
import { d } from './debug';

Expand All @@ -25,18 +25,15 @@ export type MergeASARsOptions = {
// See: https://github.com/apple-opensource-mirror/llvmCore/blob/0c60489d96c87140db9a6a14c6e82b15f5e5d252/include/llvm/Object/MachOFormat.h#L108-L112
const MACHO_MAGIC = new Set([
// 32-bit Mach-O
0xfeedface,
0xcefaedfe,
0xfeedface, 0xcefaedfe,

// 64-bit Mach-O
0xfeedfacf,
0xcffaedfe,
0xfeedfacf, 0xcffaedfe,
]);

const MACHO_UNIVERSAL_MAGIC = new Set([
// universal
0xcafebabe,
0xbebafeca,
0xcafebabe, 0xbebafeca,
]);

export const detectAsarMode = async (appPath: string) => {
Expand Down
5 changes: 2 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { spawn } from '@malept/cross-spawn-promise';
import * as asar from '@electron/asar';
import * as crypto from 'crypto';
import * as fs from 'fs-extra';
import minimatch from 'minimatch';
import { minimatch } from 'minimatch';
import * as os from 'os';
import * as path from 'path';
import * as plist from 'plist';
Expand Down Expand Up @@ -31,7 +30,7 @@ export type MakeUniversalOpts = {
/**
* Forcefully overwrite any existing files that are in the way of generating the universal application
*/
force: boolean;
force?: boolean;
/**
* Merge x64 and arm64 ASARs into one.
*/
Expand Down
9 changes: 3 additions & 6 deletions src/sha.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import * as fs from 'fs-extra';
import * as crypto from 'crypto';
import { pipeline } from 'stream/promises';

import { d } from './debug';

export const sha = async (filePath: string) => {
d('hashing', filePath);
const hash = crypto.createHash('sha256');
hash.setEncoding('hex');
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(hash);
await new Promise((resolve, reject) => {
fileStream.on('end', () => resolve());
fileStream.on('error', (err) => reject(err));
});
await pipeline(fs.createReadStream(filePath), hash);
return hash.read();
};
26 changes: 26 additions & 0 deletions test/asar-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as path from 'path';

import { AsarMode, detectAsarMode, generateAsarIntegrity } from '../src/asar-utils';

const asarsPath = path.resolve(__dirname, 'fixtures', 'asars');
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');

describe('asar-utils', () => {
describe('detectAsarMode', () => {
it('should correctly detect an asar enabled app', async () => {
expect(await detectAsarMode(path.resolve(appsPath, 'Asar.app'))).toBe(AsarMode.HAS_ASAR);
});

it('should correctly detect an app without an asar', async () => {
expect(await detectAsarMode(path.resolve(appsPath, 'NoAsar.app'))).toBe(AsarMode.NO_ASAR);
});
});

describe('generateAsarIntegrity', () => {
it('should deterministically hash an asar header', async () => {
expect(generateAsarIntegrity(path.resolve(asarsPath, 'app.asar')).hash).toEqual(
'85fff474383bd8df11cd9c5784e8fcd1525af71ff140a8a882e1dc9d5b39fcbf',
);
});
});
});
61 changes: 61 additions & 0 deletions test/file-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as path from 'path';

import { AppFile, AppFileType, getAllAppFiles } from '../src/file-utils';

const appsPath = path.resolve(__dirname, 'fixtures', 'apps');

describe('file-utils', () => {
describe('getAllAppFiles', () => {
let asarFiles: AppFile[];
let noAsarFiles: AppFile[];

beforeAll(async () => {
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Asar.app'));
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'NoAsar.app'));
});

it('should correctly identify plist files', async () => {
expect(asarFiles.find((f) => f.relativePath === 'Contents/Info.plist')?.type).toBe(
AppFileType.INFO_PLIST,
);
});

it('should correctly identify asar files as app code', async () => {
expect(asarFiles.find((f) => f.relativePath === 'Contents/Resources/app.asar')?.type).toBe(
AppFileType.APP_CODE,
);
});

it('should correctly identify non-asar code files as plain text', async () => {
expect(
noAsarFiles.find((f) => f.relativePath === 'Contents/Resources/app/index.js')?.type,
).toBe(AppFileType.PLAIN);
});

it('should correctly identify the Electron binary as Mach-O', async () => {
expect(noAsarFiles.find((f) => f.relativePath === 'Contents/MacOS/Electron')?.type).toBe(
AppFileType.MACHO,
);
});

it('should correctly identify the Electron Framework as Mach-O', async () => {
expect(
noAsarFiles.find(
(f) =>
f.relativePath ===
'Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework',
)?.type,
).toBe(AppFileType.MACHO);
});

it('should correctly identify the v8 context snapshot', async () => {
expect(
noAsarFiles.find(
(f) =>
f.relativePath ===
'Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/v8_context_snapshot.arm64.bin',
)?.type,
).toBe(AppFileType.SNAPSHOT);
});
});
});
Binary file added test/fixtures/asars/app.asar
Binary file not shown.
2 changes: 2 additions & 0 deletions test/fixtures/asars/app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
console.log('I am an app folder', process.arch);
process.exit(0);
4 changes: 4 additions & 0 deletions test/fixtures/asars/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "app",
"main": "index.js"
}
1 change: 1 addition & 0 deletions test/fixtures/tohash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello there
40 changes: 40 additions & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { spawn } from '@malept/cross-spawn-promise';
import * as fs from 'fs-extra';
import * as path from 'path';

import { makeUniversalApp } from '../src/index';

const appsPath = path.resolve(__dirname, 'fixtures', 'apps');

async function ensureUniversal(app: string) {
const exe = path.resolve(app, 'Contents', 'MacOS', 'Electron');
const result = await spawn(exe);
expect(result).toContain('arm64');
const result2 = await spawn('arch', ['-x86_64', exe]);
expect(result2).toContain('x64');
}

describe('makeUniversalApp', () => {
it('should correctly merge two identical asars', async () => {
const out = path.resolve(appsPath, 'MergedAsar.app');
await makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
arm64AppPath: path.resolve(appsPath, 'Asar.app'),
outAppPath: out,
});
await ensureUniversal(out);
// Only a single asar as they were identical
expect(
(await fs.readdir(path.resolve(out, 'Contents', 'Resources'))).filter((p) =>
p.endsWith('asar'),
),
).toEqual(['app.asar']);
}, 60000);

// TODO: Add tests for
// * different asar files
// * identical app dirs
// * different app dirs
// * different app dirs with different macho files
// * identical app dirs with universal macho files
});
Loading

0 comments on commit 8e2842b

Please sign in to comment.