Merge branch 'main' into esm-asar-entrypoints
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import asar from '@electron/asar';
|
||||
import { execFileSync } from 'child_process';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import * as asar from '@electron/asar';
|
||||
import { minimatch } from 'minimatch';
|
||||
import os from 'os';
|
||||
import { d } from './debug';
|
||||
|
||||
import { d } from './debug.js';
|
||||
|
||||
const LIPO = 'lipo';
|
||||
|
||||
@@ -40,7 +42,7 @@ export const detectAsarMode = async (appPath: string) => {
|
||||
d('checking asar mode of', appPath);
|
||||
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
|
||||
|
||||
if (!(await fs.pathExists(asarPath))) {
|
||||
if (!fs.existsSync(asarPath)) {
|
||||
d('determined no asar');
|
||||
return AsarMode.NO_ASAR;
|
||||
}
|
||||
@@ -84,8 +86,10 @@ export const mergeASARs = async ({
|
||||
}: MergeASARsOptions): Promise<void> => {
|
||||
d(`merging ${x64AsarPath} and ${arm64AsarPath}`);
|
||||
|
||||
const x64Files = new Set(asar.listPackage(x64AsarPath).map(toRelativePath));
|
||||
const arm64Files = new Set(asar.listPackage(arm64AsarPath).map(toRelativePath));
|
||||
const x64Files = new Set(asar.listPackage(x64AsarPath, { isPack: false }).map(toRelativePath));
|
||||
const arm64Files = new Set(
|
||||
asar.listPackage(arm64AsarPath, { isPack: false }).map(toRelativePath),
|
||||
);
|
||||
|
||||
//
|
||||
// Build set of unpacked directories and files
|
||||
@@ -146,14 +150,13 @@ export const mergeASARs = async ({
|
||||
const x64Content = asar.extractFile(x64AsarPath, file);
|
||||
const arm64Content = asar.extractFile(arm64AsarPath, file);
|
||||
|
||||
// Skip file if the same content
|
||||
if (x64Content.compare(arm64Content) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
MACHO_UNIVERSAL_MAGIC.has(x64Content.readUInt32LE(0)) &&
|
||||
MACHO_UNIVERSAL_MAGIC.has(arm64Content.readUInt32LE(0))
|
||||
) {
|
||||
// Skip universal Mach-O files.
|
||||
if (isUniversalMachO(x64Content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -168,8 +171,8 @@ export const mergeASARs = async ({
|
||||
// Extract both
|
||||
//
|
||||
|
||||
const x64Dir = await fs.mkdtemp(path.join(os.tmpdir(), 'x64-'));
|
||||
const arm64Dir = await fs.mkdtemp(path.join(os.tmpdir(), 'arm64-'));
|
||||
const x64Dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'x64-'));
|
||||
const arm64Dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'arm64-'));
|
||||
|
||||
try {
|
||||
d(`extracting ${x64AsarPath} to ${x64Dir}`);
|
||||
@@ -184,18 +187,22 @@ export const mergeASARs = async ({
|
||||
|
||||
if (isDirectory(arm64AsarPath, file)) {
|
||||
d(`creating unique directory: ${file}`);
|
||||
await fs.mkdirp(destination);
|
||||
await fs.promises.mkdir(destination, { recursive: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
d(`xopying unique file: ${file}`);
|
||||
await fs.mkdirp(path.dirname(destination));
|
||||
await fs.copy(source, destination);
|
||||
d(`copying unique file: ${file}`);
|
||||
await fs.promises.mkdir(path.dirname(destination), { recursive: true });
|
||||
await fs.promises.cp(source, destination, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
verbatimSymlinks: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (const binding of commonBindings) {
|
||||
const source = await fs.realpath(path.resolve(arm64Dir, binding));
|
||||
const destination = await fs.realpath(path.resolve(x64Dir, binding));
|
||||
const source = await fs.promises.realpath(path.resolve(arm64Dir, binding));
|
||||
const destination = await fs.promises.realpath(path.resolve(x64Dir, binding));
|
||||
|
||||
d(`merging binding: ${binding}`);
|
||||
execFileSync(LIPO, [source, destination, '-create', '-output', destination]);
|
||||
@@ -218,6 +225,13 @@ export const mergeASARs = async ({
|
||||
|
||||
d('done merging');
|
||||
} finally {
|
||||
await Promise.all([fs.remove(x64Dir), fs.remove(arm64Dir)]);
|
||||
await Promise.all([
|
||||
fs.promises.rm(x64Dir, { recursive: true, force: true }),
|
||||
fs.promises.rm(arm64Dir, { recursive: true, force: true }),
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
export const isUniversalMachO = (fileContent: Buffer) => {
|
||||
return MACHO_UNIVERSAL_MAGIC.has(fileContent.readUInt32LE(0));
|
||||
};
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { promises as stream } from 'node:stream';
|
||||
|
||||
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { minimatch } from 'minimatch';
|
||||
|
||||
const MACHO_PREFIX = 'Mach-O ';
|
||||
|
||||
const UNPACKED_ASAR_PATH = path.join('Contents', 'Resources', 'app.asar.unpacked');
|
||||
|
||||
export enum AppFileType {
|
||||
MACHO,
|
||||
PLAIN,
|
||||
INFO_PLIST,
|
||||
SNAPSHOT,
|
||||
APP_CODE,
|
||||
SINGLE_ARCH,
|
||||
}
|
||||
|
||||
export type AppFile = {
|
||||
@@ -17,22 +23,50 @@ export type AppFile = {
|
||||
type: AppFileType;
|
||||
};
|
||||
|
||||
export type GetAllAppFilesOpts = {
|
||||
singleArchFiles?: string;
|
||||
};
|
||||
|
||||
const isSingleArchFile = (relativePath: string, opts: GetAllAppFilesOpts): boolean => {
|
||||
if (opts.singleArchFiles === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unpackedPath = path.relative(UNPACKED_ASAR_PATH, relativePath);
|
||||
|
||||
// Outside of app.asar.unpacked
|
||||
if (unpackedPath.startsWith('..')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return minimatch(unpackedPath, opts.singleArchFiles, {
|
||||
matchBase: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param appPath Path to the application
|
||||
*/
|
||||
export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
export const getAllAppFiles = async (
|
||||
appPath: string,
|
||||
opts: GetAllAppFilesOpts,
|
||||
): Promise<AppFile[]> => {
|
||||
const unpackedPath = path.join('Contents', 'Resources', 'app.asar.unpacked');
|
||||
|
||||
const files: AppFile[] = [];
|
||||
|
||||
const visited = new Set<string>();
|
||||
const traverse = async (p: string) => {
|
||||
p = await fs.realpath(p);
|
||||
p = await fs.promises.realpath(p);
|
||||
if (visited.has(p)) return;
|
||||
visited.add(p);
|
||||
|
||||
const info = await fs.stat(p);
|
||||
const info = await fs.promises.stat(p);
|
||||
if (info.isSymbolicLink()) return;
|
||||
if (info.isFile()) {
|
||||
const relativePath = path.relative(appPath, p);
|
||||
|
||||
let fileType = AppFileType.PLAIN;
|
||||
|
||||
var fileOutput = '';
|
||||
@@ -45,8 +79,10 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (p.includes('app.asar')) {
|
||||
if (p.endsWith('.asar')) {
|
||||
fileType = AppFileType.APP_CODE;
|
||||
} else if (isSingleArchFile(relativePath, opts)) {
|
||||
fileType = AppFileType.SINGLE_ARCH;
|
||||
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
|
||||
fileType = AppFileType.MACHO;
|
||||
} else if (p.endsWith('.bin')) {
|
||||
@@ -56,13 +92,13 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
}
|
||||
|
||||
files.push({
|
||||
relativePath: path.relative(appPath, p),
|
||||
relativePath,
|
||||
type: fileType,
|
||||
});
|
||||
}
|
||||
|
||||
if (info.isDirectory()) {
|
||||
for (const child of await fs.readdir(p)) {
|
||||
for (const child of await fs.promises.readdir(p)) {
|
||||
await traverse(path.resolve(p, child));
|
||||
}
|
||||
}
|
||||
@@ -71,3 +107,32 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
export const readMachOHeader = async (path: string) => {
|
||||
const chunks: Buffer[] = [];
|
||||
// no need to read the entire file, we only need the first 4 bytes of the file to determine the header
|
||||
await stream.pipeline(fs.createReadStream(path, { start: 0, end: 3 }), async function* (source) {
|
||||
for await (const chunk of source) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
});
|
||||
return Buffer.concat(chunks);
|
||||
};
|
||||
|
||||
export const fsMove = async (oldPath: string, newPath: string) => {
|
||||
try {
|
||||
await fs.promises.rename(oldPath, newPath);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'EXDEV') {
|
||||
// Cross-device link, fallback to copy and delete
|
||||
await fs.promises.cp(oldPath, newPath, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
verbatimSymlinks: true,
|
||||
});
|
||||
await fs.promises.rm(oldPath, { force: true, recursive: true });
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
156
src/index.ts
156
src/index.ts
@@ -1,16 +1,18 @@
|
||||
import { spawn } from '@malept/cross-spawn-promise';
|
||||
import * as asar from '@electron/asar';
|
||||
import * as fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as plist from 'plist';
|
||||
import * as dircompare from 'dir-compare';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { AppFile, AppFileType, getAllAppFiles } from './file-utils';
|
||||
import { AsarMode, detectAsarMode, generateAsarIntegrity, mergeASARs } from './asar-utils';
|
||||
import { sha } from './sha';
|
||||
import { d } from './debug';
|
||||
import * as asar from '@electron/asar';
|
||||
import { spawn } from '@malept/cross-spawn-promise';
|
||||
import * as dircompare from 'dir-compare';
|
||||
import { minimatch } from 'minimatch';
|
||||
import plist from 'plist';
|
||||
|
||||
import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils.js';
|
||||
import { AppFile, AppFileType, fsMove, getAllAppFiles, readMachOHeader } from './file-utils.js';
|
||||
import { sha } from './sha.js';
|
||||
import { d } from './debug.js';
|
||||
import { computeIntegrityData } from './integrity.js';
|
||||
|
||||
/**
|
||||
* Options to pass into the {@link makeUniversalApp} function.
|
||||
@@ -73,7 +75,12 @@ export type MakeUniversalOpts = {
|
||||
};
|
||||
|
||||
const dupedFiles = (files: AppFile[]) =>
|
||||
files.filter((f) => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
|
||||
files.filter(
|
||||
(f) =>
|
||||
f.type !== AppFileType.SNAPSHOT &&
|
||||
f.type !== AppFileType.APP_CODE &&
|
||||
f.type !== AppFileType.SINGLE_ARCH,
|
||||
);
|
||||
|
||||
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
|
||||
d('making a universal app with options', opts);
|
||||
@@ -87,7 +94,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
if (!opts.outAppPath || !path.isAbsolute(opts.outAppPath))
|
||||
throw new Error('Expected opts.outAppPath to be an absolute path but it was not');
|
||||
|
||||
if (await fs.pathExists(opts.outAppPath)) {
|
||||
if (fs.existsSync(opts.outAppPath)) {
|
||||
d('output path exists already');
|
||||
if (!opts.force) {
|
||||
throw new Error(
|
||||
@@ -95,7 +102,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
);
|
||||
} else {
|
||||
d('overwriting existing application because force == true');
|
||||
await fs.remove(opts.outAppPath);
|
||||
await fs.promises.rm(opts.outAppPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +116,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
'Both the x64 and arm64 versions of your application need to have been built with the same asar settings (enabled vs disabled)',
|
||||
);
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-'));
|
||||
const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-'));
|
||||
d('building universal app in', tmpDir);
|
||||
|
||||
try {
|
||||
@@ -119,8 +126,8 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
|
||||
const uniqueToX64: string[] = [];
|
||||
const uniqueToArm64: string[] = [];
|
||||
const x64Files = await getAllAppFiles(await fs.realpath(tmpApp));
|
||||
const arm64Files = await getAllAppFiles(await fs.realpath(opts.arm64AppPath));
|
||||
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp), opts);
|
||||
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath), opts);
|
||||
|
||||
for (const file of dupedFiles(x64Files)) {
|
||||
if (!arm64Files.some((f) => f.relativePath === file.relativePath))
|
||||
@@ -141,7 +148,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
);
|
||||
}
|
||||
|
||||
for (const file of x64Files.filter((f) => f.type === AppFileType.PLAIN)) {
|
||||
// Single Arch files are copied as is without processing.
|
||||
const multiArchFiles = x64Files.filter((f) => f.type !== AppFileType.SINGLE_ARCH);
|
||||
for (const file of multiArchFiles.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) {
|
||||
@@ -157,9 +166,20 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
}
|
||||
}
|
||||
const knownMergedMachOFiles = new Set();
|
||||
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) {
|
||||
const first = await fs.realpath(path.resolve(tmpApp, machOFile.relativePath));
|
||||
const second = await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath));
|
||||
for (const machOFile of multiArchFiles.filter((f) => f.type === AppFileType.MACHO)) {
|
||||
const first = await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath));
|
||||
const second = await fs.promises.realpath(
|
||||
path.resolve(opts.arm64AppPath, machOFile.relativePath),
|
||||
);
|
||||
|
||||
if (
|
||||
isUniversalMachO(await readMachOHeader(first)) &&
|
||||
isUniversalMachO(await readMachOHeader(second))
|
||||
) {
|
||||
d(machOFile.relativePath, `is already universal across builds, skipping lipo`);
|
||||
knownMergedMachOFiles.add(machOFile.relativePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const x64Sha = await sha(path.resolve(opts.x64AppPath, machOFile.relativePath));
|
||||
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, machOFile.relativePath));
|
||||
@@ -191,7 +211,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
second,
|
||||
'-create',
|
||||
'-output',
|
||||
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
|
||||
await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath)),
|
||||
]);
|
||||
knownMergedMachOFiles.add(machOFile.relativePath);
|
||||
}
|
||||
@@ -222,39 +242,47 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
|
||||
if (nonMergedDifferences.length > 0) {
|
||||
d('x64 and arm64 app folders are different, creating dynamic entry ASAR');
|
||||
await fs.move(
|
||||
await fsMove(
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app'),
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64'),
|
||||
);
|
||||
await fs.copy(
|
||||
await fs.promises.cp(
|
||||
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'),
|
||||
{ force: true, recursive: true, verbatimSymlinks: true },
|
||||
);
|
||||
|
||||
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
||||
await fs.mkdir(entryAsar);
|
||||
await fs.promises.mkdir(entryAsar, { recursive: true });
|
||||
|
||||
let pj = await fs.readJson(
|
||||
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
|
||||
let pj = JSON.parse(
|
||||
await fs.promises.readFile(
|
||||
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
|
||||
// Load a shim that redirects to the correct folder for the architecture.
|
||||
// This needs to be a different file depending on if the app entrypoint is CommonJS or ESM.
|
||||
if (pj.type === 'module' || pj.main.endsWith('.mjs')) {
|
||||
await fs.copy(
|
||||
path.resolve(__dirname, '..', '..', 'entry-asar', 'esm', 'no-asar.mjs'),
|
||||
await fs.promises.cp(
|
||||
path.resolve(import.meta.dirname, '..', '..', 'entry-asar', 'esm', 'no-asar.mjs'),
|
||||
path.resolve(entryAsar, 'index.mjs'),
|
||||
);
|
||||
pj.main = 'index.mjs';
|
||||
} else {
|
||||
await fs.copy(
|
||||
path.resolve(__dirname, '..', '..', 'entry-asar', 'cjs', 'no-asar.js'),
|
||||
await fs.promises.cp(
|
||||
path.resolve(import.meta.dirname, '..', '..', 'entry-asar', 'cjs', 'no-asar.js'),
|
||||
path.resolve(entryAsar, 'index.js'),
|
||||
);
|
||||
pj.main = 'index.js';
|
||||
}
|
||||
|
||||
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
||||
await fs.promises.writeFile(
|
||||
path.resolve(entryAsar, 'package.json'),
|
||||
JSON.stringify(pj) + '\n',
|
||||
'utf8',
|
||||
);
|
||||
await asar.createPackage(
|
||||
entryAsar,
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||
@@ -264,9 +292,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
}
|
||||
}
|
||||
|
||||
const generatedIntegrity: Record<string, { algorithm: 'SHA256'; hash: string }> = {};
|
||||
let didSplitAsar = false;
|
||||
|
||||
/**
|
||||
* If we have an ASAR we just need to check if the two "app.asar" files have the same hash,
|
||||
* if they are, same as above, we can leave one there and call it a day. If they're different
|
||||
@@ -284,8 +309,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
outputAsarPath: output,
|
||||
singleArchFiles: opts.singleArchFiles,
|
||||
});
|
||||
|
||||
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(output);
|
||||
} else if (x64AsarMode === AsarMode.HAS_ASAR) {
|
||||
d('checking if the x64 and arm64 asars are identical');
|
||||
const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
|
||||
@@ -294,22 +317,22 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
);
|
||||
|
||||
if (x64AsarSha !== arm64AsarSha) {
|
||||
didSplitAsar = true;
|
||||
d('x64 and arm64 asars are different');
|
||||
const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar');
|
||||
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath);
|
||||
await fsMove(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath);
|
||||
const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked');
|
||||
if (await fs.pathExists(x64Unpacked)) {
|
||||
await fs.move(
|
||||
if (fs.existsSync(x64Unpacked)) {
|
||||
await fsMove(
|
||||
x64Unpacked,
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'),
|
||||
);
|
||||
}
|
||||
|
||||
const arm64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar');
|
||||
await fs.copy(
|
||||
await fs.promises.cp(
|
||||
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'),
|
||||
arm64AsarPath,
|
||||
{ force: true, recursive: true, verbatimSymlinks: true },
|
||||
);
|
||||
const arm64Unpacked = path.resolve(
|
||||
opts.arm64AppPath,
|
||||
@@ -317,15 +340,16 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
'Resources',
|
||||
'app.asar.unpacked',
|
||||
);
|
||||
if (await fs.pathExists(arm64Unpacked)) {
|
||||
await fs.copy(
|
||||
if (fs.existsSync(arm64Unpacked)) {
|
||||
await fs.promises.cp(
|
||||
arm64Unpacked,
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'),
|
||||
{ force: true, recursive: true, verbatimSymlinks: true },
|
||||
);
|
||||
}
|
||||
|
||||
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
||||
await fs.mkdir(entryAsar);
|
||||
await fs.promises.mkdir(entryAsar, { recursive: true });
|
||||
let pj = JSON.parse(
|
||||
(
|
||||
await asar.extractFile(
|
||||
@@ -338,44 +362,43 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
// Load a shim that redirects to the correct `app.asar` for the architecture.
|
||||
// This needs to be a different file depending on if the app entrypoint is CommonJS or ESM.
|
||||
if (pj.type === 'module' || pj.main.endsWith('.mjs')) {
|
||||
await fs.copy(
|
||||
path.resolve(__dirname, '..', '..', 'entry-asar', 'esm', 'has-asar.mjs'),
|
||||
await fs.promises.cp(
|
||||
path.resolve(import.meta.dirname, '..', '..', 'entry-asar', 'esm', 'has-asar.mjs'),
|
||||
path.resolve(entryAsar, 'index.mjs'),
|
||||
);
|
||||
pj.main = 'index.mjs';
|
||||
} else {
|
||||
await fs.copy(
|
||||
path.resolve(__dirname, '..', '..', 'entry-asar', 'cjs', 'has-asar.js'),
|
||||
await fs.promises.cp(
|
||||
path.resolve(import.meta.dirname, '..', '..', 'entry-asar', 'cjs', 'has-asar.js'),
|
||||
path.resolve(entryAsar, 'index.js'),
|
||||
);
|
||||
pj.main = 'index.js';
|
||||
}
|
||||
|
||||
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
||||
await fs.promises.writeFile(
|
||||
path.resolve(entryAsar, 'package.json'),
|
||||
JSON.stringify(pj) + '\n',
|
||||
'utf8',
|
||||
);
|
||||
const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar');
|
||||
await asar.createPackage(entryAsar, asarPath);
|
||||
|
||||
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(asarPath);
|
||||
generatedIntegrity['Resources/app-x64.asar'] = generateAsarIntegrity(x64AsarPath);
|
||||
generatedIntegrity['Resources/app-arm64.asar'] = generateAsarIntegrity(arm64AsarPath);
|
||||
} else {
|
||||
d('x64 and arm64 asars are the same');
|
||||
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST);
|
||||
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'), opts);
|
||||
|
||||
const plistFiles = multiArchFiles.filter((f) => f.type === AppFileType.INFO_PLIST);
|
||||
for (const plistFile of plistFiles) {
|
||||
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
|
||||
const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath);
|
||||
|
||||
const { ElectronAsarIntegrity: x64Integrity, ...x64Plist } = plist.parse(
|
||||
await fs.readFile(x64PlistPath, 'utf8'),
|
||||
await fs.promises.readFile(x64PlistPath, 'utf8'),
|
||||
) as any;
|
||||
const { ElectronAsarIntegrity: arm64Integrity, ...arm64Plist } = plist.parse(
|
||||
await fs.readFile(arm64PlistPath, 'utf8'),
|
||||
await fs.promises.readFile(arm64PlistPath, 'utf8'),
|
||||
) as any;
|
||||
if (JSON.stringify(x64Plist) !== JSON.stringify(arm64Plist)) {
|
||||
throw new Error(
|
||||
@@ -390,23 +413,26 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
? { ...x64Plist, ElectronAsarIntegrity: generatedIntegrity }
|
||||
: { ...x64Plist };
|
||||
|
||||
await fs.writeFile(path.resolve(tmpApp, plistFile.relativePath), plist.build(mergedPlist));
|
||||
await fs.promises.writeFile(
|
||||
path.resolve(tmpApp, plistFile.relativePath),
|
||||
plist.build(mergedPlist),
|
||||
);
|
||||
}
|
||||
|
||||
for (const snapshotsFile of arm64Files.filter((f) => f.type === AppFileType.SNAPSHOT)) {
|
||||
d('copying snapshot file', snapshotsFile.relativePath, 'to target application');
|
||||
await fs.copy(
|
||||
await fs.promises.cp(
|
||||
path.resolve(opts.arm64AppPath, snapshotsFile.relativePath),
|
||||
path.resolve(tmpApp, snapshotsFile.relativePath),
|
||||
);
|
||||
}
|
||||
|
||||
d('moving final universal app to target destination');
|
||||
await fs.mkdirp(path.dirname(opts.outAppPath));
|
||||
await fs.promises.mkdir(path.dirname(opts.outAppPath), { recursive: true });
|
||||
await spawn('mv', [tmpApp, opts.outAppPath]);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
await fs.remove(tmpDir);
|
||||
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
58
src/integrity.ts
Normal file
58
src/integrity.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { AppFileType, getAllAppFiles } from './file-utils.js';
|
||||
import { generateAsarIntegrity } from './asar-utils.js';
|
||||
|
||||
type IntegrityMap = {
|
||||
[filepath: string]: string;
|
||||
};
|
||||
|
||||
export interface HeaderHash {
|
||||
algorithm: 'SHA256';
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export interface AsarIntegrity {
|
||||
[key: string]: HeaderHash;
|
||||
}
|
||||
|
||||
export type ComputeIntegrityDataOpts = {
|
||||
singleArchFiles?: string;
|
||||
};
|
||||
|
||||
export async function computeIntegrityData(
|
||||
contentsPath: string,
|
||||
opts: ComputeIntegrityDataOpts,
|
||||
): Promise<AsarIntegrity> {
|
||||
const root = await fs.promises.realpath(contentsPath);
|
||||
|
||||
const resourcesRelativePath = 'Resources';
|
||||
const resourcesPath = path.resolve(root, resourcesRelativePath);
|
||||
|
||||
const resources = await getAllAppFiles(resourcesPath, opts);
|
||||
const resourceAsars = resources
|
||||
.filter((file) => file.type === AppFileType.APP_CODE)
|
||||
.reduce<IntegrityMap>(
|
||||
(prev, file) => ({
|
||||
...prev,
|
||||
[path.join(resourcesRelativePath, file.relativePath)]: path.join(
|
||||
resourcesPath,
|
||||
file.relativePath,
|
||||
),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
// sort to produce constant result
|
||||
const allAsars = Object.entries(resourceAsars).sort(([name1], [name2]) =>
|
||||
name1.localeCompare(name2),
|
||||
);
|
||||
const hashes = await Promise.all(allAsars.map(async ([, from]) => generateAsarIntegrity(from)));
|
||||
const asarIntegrity: AsarIntegrity = {};
|
||||
for (let i = 0; i < allAsars.length; i++) {
|
||||
const [asar] = allAsars[i];
|
||||
asarIntegrity[asar] = hashes[i];
|
||||
}
|
||||
return asarIntegrity;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as crypto from 'crypto';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import fs from 'node:fs';
|
||||
import crypto from 'node:crypto';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
import { d } from './debug';
|
||||
import { d } from './debug.js';
|
||||
|
||||
export const sha = async (filePath: string) => {
|
||||
d('hashing', filePath);
|
||||
|
||||
Reference in New Issue
Block a user