feat: add support for v8 snapshots and cross-arch app code

This commit is contained in:
Samuel Attard
2020-11-12 16:17:59 -08:00
parent c6136396fd
commit 46ff9c4b38
2 changed files with 98 additions and 31 deletions

7
entry-asar/index.js Normal file
View File

@@ -0,0 +1,7 @@
if (process.arch === 'arm64') {
process._asarPath = require.resolve('../arm64.app.asar');
} else {
process._asarPath = require.resolve('../x64.app.asar');
}
require(process._asarPath);

View File

@@ -1,5 +1,6 @@
import { spawn } from '@malept/cross-spawn-promise'; import { spawn } from '@malept/cross-spawn-promise';
import * as asar from 'asar'; import * as asar from 'asar';
import * as crypto from 'crypto';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
@@ -42,8 +43,7 @@ type MakeUniversalOpts = {
enum AsarMode { enum AsarMode {
NO_ASAR, NO_ASAR,
PURE_ASAR_EMBEDDED_NATIVE_MODULES, HAS_ASAR,
PURE_ASAR_UNPACKED_NATIVE_MODULES,
} }
const detectAsarMode = async (appPath: string) => { const detectAsarMode = async (appPath: string) => {
@@ -51,16 +51,24 @@ const detectAsarMode = async (appPath: string) => {
const asarUnpackedPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar.unpacked'); const asarUnpackedPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar.unpacked');
if (!(await fs.pathExists(asarPath))) return AsarMode.NO_ASAR; if (!(await fs.pathExists(asarPath))) return AsarMode.NO_ASAR;
const nativeContents = asar.listPackage(asarPath).filter((p) => p.endsWith('.node'));
for (const nativeModule of nativeContents) { return AsarMode.HAS_ASAR;
if (!(await fs.pathExists(path.resolve(asarUnpackedPath, nativeModule.substr(1)))))
return AsarMode.PURE_ASAR_EMBEDDED_NATIVE_MODULES;
}
return AsarMode.PURE_ASAR_UNPACKED_NATIVE_MODULES;
}; };
const getAllMachOFiles = async (appPath: string) => { enum AppFileType {
const machoOFiles: string[] = []; MACHO,
PLAIN,
SNAPSHOT,
APP_CODE,
}
type AppFile = {
relativePath: string;
type: AppFileType;
}
const getAllFiles = async (appPath: string): Promise<AppFile[]> => {
const files: AppFile[] = [];
const visited = new Set<string>(); const visited = new Set<string>();
const traverse = async (p: string) => { const traverse = async (p: string) => {
@@ -71,10 +79,21 @@ const getAllMachOFiles = async (appPath: string) => {
const info = await fs.stat(p); const info = await fs.stat(p);
if (info.isSymbolicLink()) return; if (info.isSymbolicLink()) return;
if (info.isFile()) { if (info.isFile()) {
let fileType = AppFileType.PLAIN;
const fileOutput = await spawn('file', ['--brief', '--no-pad', p]); const fileOutput = await spawn('file', ['--brief', '--no-pad', p]);
if (fileOutput.startsWith(MACHO_PREFIX)) { if (p.includes('app.asar')) {
machoOFiles.push(path.relative(appPath, p)); fileType = AppFileType.APP_CODE;
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
fileType = AppFileType.MACHO;
} else if (p.endsWith('.bin')) {
fileType = AppFileType.SNAPSHOT;
} }
files.push({
relativePath: path.relative(appPath, p),
type: fileType,
});
} }
if (info.isDirectory()) { if (info.isDirectory()) {
@@ -85,9 +104,15 @@ const getAllMachOFiles = async (appPath: string) => {
}; };
await traverse(appPath); await traverse(appPath);
return machoOFiles; return files;
}; };
const dupedFiles = (files: AppFile[]) => files.filter(f => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
const sha = async (filePath: string) => {
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => { export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
if (process.platform !== 'darwin') if (process.platform !== 'darwin')
throw new Error('@electron/universal is only supported on darwin platforms'); throw new Error('@electron/universal is only supported on darwin platforms');
@@ -115,14 +140,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
throw new Error( throw new Error(
'Both the x64 and arm64 versions of your application need to have been built with the same asar settings (enabled vs disabled)', 'Both the x64 and arm64 versions of your application need to have been built with the same asar settings (enabled vs disabled)',
); );
if (x64AsarMode === AsarMode.PURE_ASAR_EMBEDDED_NATIVE_MODULES)
throw new Error(
'@electron/universal does not currently support apps that contain native modules in ASAR files. Please use asar.unpacked',
);
if (arm64AsarMode === AsarMode.PURE_ASAR_EMBEDDED_NATIVE_MODULES)
throw new Error(
'@electron/universal does not currently support apps that contain native modules in ASAR files. Please use asar.unpacked',
);
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-')); const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-'));
@@ -132,14 +149,14 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const uniqueToX64: string[] = []; const uniqueToX64: string[] = [];
const uniqueToArm64: string[] = []; const uniqueToArm64: string[] = [];
const x64MachOFiles = await getAllMachOFiles(await fs.realpath(tmpApp)); const x64Files = await getAllFiles(await fs.realpath(tmpApp));
const arm64MachoOFiles = await getAllMachOFiles(opts.arm64AppPath); const arm64Files = await getAllFiles(opts.arm64AppPath);
for (const file of x64MachOFiles) { for (const file of dupedFiles(x64Files)) {
if (!arm64MachoOFiles.includes(file)) uniqueToX64.push(file); if (!arm64Files.some(f => f.relativePath === file.relativePath)) uniqueToX64.push(file.relativePath);
} }
for (const file of arm64MachoOFiles) { for (const file of dupedFiles(arm64Files)) {
if (!x64MachOFiles.includes(file)) uniqueToArm64.push(file); if (!x64Files.some(f => f.relativePath === file.relativePath)) uniqueToArm64.push(file.relativePath);
} }
if (uniqueToX64.length !== 0 || uniqueToArm64.length !== 0) { if (uniqueToX64.length !== 0 || uniqueToArm64.length !== 0) {
console.error({ console.error({
@@ -151,16 +168,59 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
); );
} }
for (const machOFile of x64MachOFiles) { for (const file of x64Files.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) {
console.error(`${x64Sha} !== ${arm64Sha}`);
throw new Error(`Expected all non-binary files to have identical SHAs when creating a universal build but "${file.relativePath}" did not`);
}
}
for (const machOFile of x64Files.filter(f => f.type === AppFileType.MACHO)) {
await spawn('lipo', [ await spawn('lipo', [
await fs.realpath(path.resolve(tmpApp, machOFile)), await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
await fs.realpath(path.resolve(opts.arm64AppPath, machOFile)), await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath)),
'-create', '-create',
'-output', '-output',
await fs.realpath(path.resolve(tmpApp, machOFile)), await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
]); ]);
} }
if (x64AsarMode === AsarMode.NO_ASAR) {
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app'), path.resolve(tmpApp, 'Contents', 'Resources', 'x64.app'));
await fs.copy(path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'), path.resolve(tmpApp, 'Contents', 'Resources', 'arm64.app'));
} else {
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), path.resolve(tmpApp, 'Contents', 'Resources', 'x64.app.asar'));
const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked');
if (await fs.pathExists(x64Unpacked)) {
await fs.move(x64Unpacked, path.resolve(tmpApp, 'Contents', 'Resources', 'x64.app.asar.unpacked'));
}
await fs.copy(path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'), path.resolve(tmpApp, 'Contents', 'Resources', 'arm64.app.asar'));
const arm64Unpacked = path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar.unpacked');
if (await fs.pathExists(arm64Unpacked)) {
await fs.copy(arm64Unpacked, path.resolve(tmpApp, 'Contents', 'Resources', 'arm64.app.asar.unpacked'));
}
}
const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.mkdir(entryAsar);
await fs.copy(path.resolve(__dirname, '..', '..', 'entry-asar', 'index.js'), path.resolve(entryAsar, 'index.js'));
let pj: any;
if (x64AsarMode === AsarMode.NO_ASAR) {
pj = await fs.readJson(path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'));
} else {
pj = JSON.parse((await asar.extractFile(path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app.asar'), 'package.json')).toString('utf8'));
}
pj.main = 'index.js';
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
await asar.createPackage(entryAsar, path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
for (const snapshotsFile of arm64Files.filter(f => f.type === AppFileType.SNAPSHOT)) {
await fs.copy(path.resolve(opts.arm64AppPath, snapshotsFile.relativePath), path.resolve(tmpApp, snapshotsFile.relativePath));
}
await spawn('mv', [tmpApp, opts.outAppPath]); await spawn('mv', [tmpApp, opts.outAppPath]);
} catch (err) { } catch (err) {
throw err; throw err;