Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb304ce10b |
@@ -1,8 +1,8 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
cfa: continuousauth/npm@2.0.0
|
cfa: continuousauth/npm@1.0.2
|
||||||
node: electronjs/node@2.1.0
|
node: electronjs/node@1.4.1
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
test_and_release:
|
test_and_release:
|
||||||
@@ -13,7 +13,6 @@ workflows:
|
|||||||
name: test-mac-<< matrix.node-version >>
|
name: test-mac-<< matrix.node-version >>
|
||||||
override-ci-command: yarn install --frozen-lockfile --ignore-engines
|
override-ci-command: yarn install --frozen-lockfile --ignore-engines
|
||||||
test-steps:
|
test-steps:
|
||||||
- node/install-rosetta
|
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
@@ -25,6 +24,9 @@ workflows:
|
|||||||
- 20.5.0
|
- 20.5.0
|
||||||
- 18.17.0
|
- 18.17.0
|
||||||
- 16.20.1
|
- 16.20.1
|
||||||
|
- 14.21.3
|
||||||
|
- 12.22.12
|
||||||
|
- 10.24.1
|
||||||
- cfa/release:
|
- cfa/release:
|
||||||
requires:
|
requires:
|
||||||
- test
|
- test
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,5 +3,3 @@ dist
|
|||||||
entry-asar/*.js*
|
entry-asar/*.js*
|
||||||
entry-asar/*.ts
|
entry-asar/*.ts
|
||||||
*.app
|
*.app
|
||||||
test/fixtures/apps
|
|
||||||
coverage
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
||||||
module.exports = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'node',
|
|
||||||
transform: {
|
|
||||||
'.': [
|
|
||||||
'ts-jest',
|
|
||||||
{
|
|
||||||
tsconfig: 'tsconfig.jest.json'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
globalSetup: './jest.setup.ts'
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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
package.json
49
package.json
@@ -15,7 +15,7 @@
|
|||||||
"url": "https://github.com/electron/universal.git"
|
"url": "https://github.com/electron/universal.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.4"
|
"node": ">=8.6"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/*",
|
"dist/*",
|
||||||
@@ -26,45 +26,36 @@
|
|||||||
"author": "Samuel Attard",
|
"author": "Samuel Attard",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.entry-asar.json",
|
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.entry-asar.json",
|
||||||
"lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
|
"lint": "prettier --check \"{src,entry-asar}/**/*.ts\"",
|
||||||
"prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
|
"prettier:write": "prettier --write \"{src,entry-asar}/**/*.ts\"",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"test": "jest",
|
"test": "exit 0",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@continuous-auth/semantic-release-npm": "^4.0.0",
|
"@continuous-auth/semantic-release-npm": "^3.0.0",
|
||||||
"@electron/get": "^3.0.0",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/cross-zip": "^4.0.1",
|
"@types/fs-extra": "^9.0.4",
|
||||||
"@types/debug": "^4.1.10",
|
"@types/minimatch": "^3.0.5",
|
||||||
"@types/fs-extra": "^11.0.3",
|
"@types/node": "^14.14.7",
|
||||||
"@types/jest": "^29.5.7",
|
"@types/plist": "^3.0.2",
|
||||||
"@types/minimatch": "^5.1.2",
|
"husky": "^8.0.0",
|
||||||
"@types/node": "^20.8.10",
|
"lint-staged": "^10.5.1",
|
||||||
"@types/plist": "^3.0.4",
|
"prettier": "^2.1.2",
|
||||||
"cross-zip": "^4.0.0",
|
"typescript": "^4.0.5"
|
||||||
"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": {
|
"dependencies": {
|
||||||
"@electron/asar": "^3.2.7",
|
"@electron/asar": "^3.2.1",
|
||||||
"@malept/cross-spawn-promise": "^2.0.0",
|
"@malept/cross-spawn-promise": "^1.1.0",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"dir-compare": "^4.2.0",
|
"dir-compare": "^3.0.0",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^9.0.1",
|
||||||
"minimatch": "^9.0.3",
|
"minimatch": "^3.0.4",
|
||||||
"plist": "^3.1.0"
|
"plist": "^3.0.4"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.ts": [
|
"*.ts": [
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"jackspeak": "2.1.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { execFileSync } from 'child_process';
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { minimatch } from 'minimatch';
|
import minimatch from 'minimatch';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { d } from './debug';
|
import { d } from './debug';
|
||||||
|
|
||||||
@@ -25,15 +25,18 @@ export type MergeASARsOptions = {
|
|||||||
// See: https://github.com/apple-opensource-mirror/llvmCore/blob/0c60489d96c87140db9a6a14c6e82b15f5e5d252/include/llvm/Object/MachOFormat.h#L108-L112
|
// See: https://github.com/apple-opensource-mirror/llvmCore/blob/0c60489d96c87140db9a6a14c6e82b15f5e5d252/include/llvm/Object/MachOFormat.h#L108-L112
|
||||||
const MACHO_MAGIC = new Set([
|
const MACHO_MAGIC = new Set([
|
||||||
// 32-bit Mach-O
|
// 32-bit Mach-O
|
||||||
0xfeedface, 0xcefaedfe,
|
0xfeedface,
|
||||||
|
0xcefaedfe,
|
||||||
|
|
||||||
// 64-bit Mach-O
|
// 64-bit Mach-O
|
||||||
0xfeedfacf, 0xcffaedfe,
|
0xfeedfacf,
|
||||||
|
0xcffaedfe,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const MACHO_UNIVERSAL_MAGIC = new Set([
|
const MACHO_UNIVERSAL_MAGIC = new Set([
|
||||||
// universal
|
// universal
|
||||||
0xcafebabe, 0xbebafeca,
|
0xcafebabe,
|
||||||
|
0xbebafeca,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const detectAsarMode = async (appPath: string) => {
|
export const detectAsarMode = async (appPath: string) => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { spawn } from '@malept/cross-spawn-promise';
|
import { spawn } from '@malept/cross-spawn-promise';
|
||||||
import * as asar from '@electron/asar';
|
import * as asar from '@electron/asar';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import { minimatch } from 'minimatch';
|
import minimatch from 'minimatch';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as plist from 'plist';
|
import * as plist from 'plist';
|
||||||
@@ -30,7 +31,7 @@ export type MakeUniversalOpts = {
|
|||||||
/**
|
/**
|
||||||
* Forcefully overwrite any existing files that are in the way of generating the universal application
|
* 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.
|
* Merge x64 and arm64 ASARs into one.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { pipeline } from 'stream/promises';
|
|
||||||
|
|
||||||
import { d } from './debug';
|
import { d } from './debug';
|
||||||
|
|
||||||
export const sha = async (filePath: string) => {
|
export const sha = async (filePath: string) => {
|
||||||
d('hashing', filePath);
|
d('hashing', filePath);
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
hash.setEncoding('hex');
|
hash.setEncoding('hex');
|
||||||
await pipeline(fs.createReadStream(filePath), hash);
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
fileStream.pipe(hash);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
fileStream.on('end', () => resolve());
|
||||||
|
fileStream.on('error', (err) => reject(err));
|
||||||
|
});
|
||||||
return hash.read();
|
return hash.read();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
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',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
BIN
test/fixtures/asars/app.asar
vendored
BIN
test/fixtures/asars/app.asar
vendored
Binary file not shown.
2
test/fixtures/asars/app/index.js
vendored
2
test/fixtures/asars/app/index.js
vendored
@@ -1,2 +0,0 @@
|
|||||||
console.log('I am an app folder', process.arch);
|
|
||||||
process.exit(0);
|
|
||||||
4
test/fixtures/asars/app/package.json
vendored
4
test/fixtures/asars/app/package.json
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "app",
|
|
||||||
"main": "index.js"
|
|
||||||
}
|
|
||||||
1
test/fixtures/tohash
vendored
1
test/fixtures/tohash
vendored
@@ -1 +0,0 @@
|
|||||||
hello there
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import { sha } from '../src/sha';
|
|
||||||
|
|
||||||
describe('sha', () => {
|
|
||||||
it('should correctly hash a file', async () => {
|
|
||||||
expect(await sha(path.resolve(__dirname, 'fixtures', 'tohash'))).toEqual(
|
|
||||||
'12998c017066eb0d2a70b94e6ed3192985855ce390f321bbdb832022888bd251',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "esnext",
|
|
||||||
"outDir": "dist/esm",
|
|
||||||
"types": [
|
|
||||||
"jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user