Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bump minimum node version to 16 and add tests #86

Merged
merged 4 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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