diff --git a/entry-asar/index.js b/entry-asar/index.js new file mode 100644 index 0000000..4cb9dc9 --- /dev/null +++ b/entry-asar/index.js @@ -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); diff --git a/src/index.ts b/src/index.ts index 7d66dcf..720d40e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { spawn } from '@malept/cross-spawn-promise'; import * as asar from 'asar'; +import * as crypto from 'crypto'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; @@ -42,8 +43,7 @@ type MakeUniversalOpts = { enum AsarMode { NO_ASAR, - PURE_ASAR_EMBEDDED_NATIVE_MODULES, - PURE_ASAR_UNPACKED_NATIVE_MODULES, + HAS_ASAR, } const detectAsarMode = async (appPath: string) => { @@ -51,16 +51,24 @@ const detectAsarMode = async (appPath: string) => { const asarUnpackedPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar.unpacked'); if (!(await fs.pathExists(asarPath))) return AsarMode.NO_ASAR; - const nativeContents = asar.listPackage(asarPath).filter((p) => p.endsWith('.node')); - for (const nativeModule of nativeContents) { - if (!(await fs.pathExists(path.resolve(asarUnpackedPath, nativeModule.substr(1))))) - return AsarMode.PURE_ASAR_EMBEDDED_NATIVE_MODULES; - } - return AsarMode.PURE_ASAR_UNPACKED_NATIVE_MODULES; + + return AsarMode.HAS_ASAR; }; -const getAllMachOFiles = async (appPath: string) => { - const machoOFiles: string[] = []; +enum AppFileType { + MACHO, + PLAIN, + SNAPSHOT, + APP_CODE, +} + +type AppFile = { + relativePath: string; + type: AppFileType; +} + +const getAllFiles = async (appPath: string): Promise => { + const files: AppFile[] = []; const visited = new Set(); const traverse = async (p: string) => { @@ -71,10 +79,21 @@ const getAllMachOFiles = async (appPath: string) => { const info = await fs.stat(p); if (info.isSymbolicLink()) return; if (info.isFile()) { + let fileType = AppFileType.PLAIN; + const fileOutput = await spawn('file', ['--brief', '--no-pad', p]); - if (fileOutput.startsWith(MACHO_PREFIX)) { - machoOFiles.push(path.relative(appPath, p)); + if (p.includes('app.asar')) { + 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()) { @@ -85,9 +104,15 @@ const getAllMachOFiles = async (appPath: string) => { }; 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 => { if (process.platform !== 'darwin') throw new Error('@electron/universal is only supported on darwin platforms'); @@ -115,14 +140,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = 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)', ); - 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-')); @@ -132,14 +149,14 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = const uniqueToX64: string[] = []; const uniqueToArm64: string[] = []; - const x64MachOFiles = await getAllMachOFiles(await fs.realpath(tmpApp)); - const arm64MachoOFiles = await getAllMachOFiles(opts.arm64AppPath); + const x64Files = await getAllFiles(await fs.realpath(tmpApp)); + const arm64Files = await getAllFiles(opts.arm64AppPath); - for (const file of x64MachOFiles) { - if (!arm64MachoOFiles.includes(file)) uniqueToX64.push(file); + for (const file of dupedFiles(x64Files)) { + if (!arm64Files.some(f => f.relativePath === file.relativePath)) uniqueToX64.push(file.relativePath); } - for (const file of arm64MachoOFiles) { - if (!x64MachOFiles.includes(file)) uniqueToArm64.push(file); + for (const file of dupedFiles(arm64Files)) { + if (!x64Files.some(f => f.relativePath === file.relativePath)) uniqueToArm64.push(file.relativePath); } if (uniqueToX64.length !== 0 || uniqueToArm64.length !== 0) { console.error({ @@ -151,16 +168,59 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = ); } - 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 fs.realpath(path.resolve(tmpApp, machOFile)), - await fs.realpath(path.resolve(opts.arm64AppPath, machOFile)), + await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)), + await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath)), '-create', '-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]); } catch (err) { throw err;