9 Commits

Author SHA1 Message Date
David Sanders
b8379c01ed fix: don't star import from plist package (#141)
Some checks failed
Publish documentation / docs (push) Failing after 1m9s
2025-07-31 11:12:10 -07:00
David Sanders
421713cf80 feat!: bump engines to Node.js >=22.12.0 (#139)
Some checks failed
Publish documentation / docs (push) Failing after 1m9s
BREAKING CHANGE: Requires Node.js v22.12.0 LTS or higher. ESM-only.
2025-07-03 15:30:07 -07:00
David Sanders
175672e430 test: update snapshot to remove skipped test (#138) 2025-05-28 15:40:37 -07:00
David Sanders
1695dc9eac ci: timeout release job after 1 hour (#136) 2025-05-09 16:28:07 -07:00
Mike Maietta
64be29d2f7 fix: Skip lipo if native module is already universal. Add native module fixtures for lipo tests (#126)
* fix: when native modules are already universal, don't lipo. adds `node-mac-permissions` fixture from https://github.com/codebytere/node-mac-permissions and resolves 3 `it.todo` test cases

* add test `different app dirs with different macho files (shim and lipo)`

* add additional test

* PR feedback

* gotta close `fd`

* use `stream` to read first 4 bytes. copy native fixture before packing into asar to leverage `unpack: "**/*.node"` properly.

* convert params to object

* rename `createTestApp` to `createStagingAppDir` and add jsdoc to the function

* compiler error from merge conflict

* update snapshots

* update snapshots

* only check x64Content since it's the tmp app

* compile macho binaries at runtime using hellow-world.c for fixtures in lipo tests

* Update jest.setup.ts

Co-authored-by: Erik Moura <erikian@erikian.dev>

* Update jest.setup.ts

Co-authored-by: Erik Moura <erikian@erikian.dev>

* remove unstable properties for specific keys

* force redo

* update snapshots

* stripping only hello-world from snapshot and only hash from macho-specific asar integrity

* optimize logic :)

---------

Co-authored-by: Erik Moura <erikian@erikian.dev>
2025-05-02 13:10:36 -03:00
dependabot[bot]
ec7c971959 build(deps): bump actions/setup-node from 4.3.0 to 4.4.0 (#134)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](cdca7365b2...49933ea528)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 10:07:01 -07:00
dependabot[bot]
5b957e6858 build(deps): bump dsanders11/project-actions from 1.5.1 to 1.7.0 (#132)
Bumps [dsanders11/project-actions](https://github.com/dsanders11/project-actions) from 1.5.1 to 1.7.0.
- [Release notes](https://github.com/dsanders11/project-actions/releases)
- [Changelog](https://github.com/dsanders11/project-actions/blob/main/.releaserc.json)
- [Commits](9c80cd31f5...2134fe7cc7)

---
updated-dependencies:
- dependency-name: dsanders11/project-actions
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 13:27:42 -05:00
dependabot[bot]
4276c7cf38 build(deps): bump actions/setup-node from 4.2.0 to 4.3.0 (#131)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](1d0ff469b7...cdca7365b2)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 11:41:59 -05:00
Erik Moura
977baa4d42 test: add explicit imports for jest functions (#128)
* test: add explicit imports for `jest` functions

* add workaround for non-`.spec.ts` file
2025-03-31 23:17:05 -07:00
29 changed files with 1819 additions and 2769 deletions

View File

@@ -21,7 +21,7 @@ jobs:
creds: ${{ secrets.ECOSYSTEM_ISSUE_TRIAGE_GH_APP_CREDS }}
org: electron
- name: Add to Project
uses: dsanders11/project-actions/add-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
uses: dsanders11/project-actions/add-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
with:
field: Opened
field-value: ${{ github.event.pull_request.created_at || github.event.issue.created_at }}

35
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Publish documentation
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
permissions:
id-token: write
contents: read
jobs:
docs:
runs-on: ubuntu-latest
environment: docs-publish
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Build API documentation
run: yarn build:docs
- name: Azure login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_OIDC_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_OIDC_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_OIDC_SUBSCRIPTION_ID }}
- name: Upload to Azure Blob Storage
uses: azure/cli@089eac9d8cc39f5d003e94f8b65efc51076c9cbd # v2.1.0
with:
inlineScript: |
az storage blob upload-batch --account-name ${{ secrets.AZURE_ECOSYSTEM_PACKAGES_STORAGE_ACCOUNT_NAME }} -d '$web/${{ github.event.repository.name }}/${{ github.ref_name }}' -s ./docs --overwrite --auth-mode login

View File

@@ -22,13 +22,14 @@ jobs:
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20.x
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install
run: yarn install --frozen-lockfile
- uses: continuousauth/action@4e8a2573eeb706f6d7300d6a9f3ca6322740b72d # v1.0.5
timeout-minutes: 60
with:
project-id: ${{ secrets.CFA_PROJECT_ID }}
secret: ${{ secrets.CFA_SECRET }}

View File

@@ -18,24 +18,18 @@ jobs:
fail-fast: false
matrix:
node-version:
- '20.5'
- '18.17'
- '16.20'
- 22.12.x
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "${{ matrix.node-version }}"
cache: 'yarn'
- name: Install (Node.js v18+)
if : ${{ matrix.node-version != '16.20' }}
- name: Install
run: yarn install --frozen-lockfile
- name: Install (Node.js < v18)
if : ${{ matrix.node-version == '16.20' }}
run: yarn install --frozen-lockfile --ignore-engines
- name: Build
run: yarn build
- name: Lint

View File

@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
22.12

View File

@@ -1,16 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.ts?$': [
'ts-jest',
{
tsconfig: 'tsconfig.jest.json',
},
],
},
testMatch: ['<rootDir>/test/**/*.spec.ts'],
globalSetup: './jest.setup.ts',
testTimeout: 10000,
};

View File

@@ -1,51 +0,0 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { appsDir, asarsDir, templateApp } from './test/util';
export default async () => {
await fs.remove(appsDir);
await fs.mkdirp(appsDir);
await templateApp('Arm64Asar.app', 'arm64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
});
// contains `extra-file.txt`
await templateApp('Arm64AsarExtraFile.app', 'arm64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app2.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('Arm64NoAsar.app', 'arm64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'),
);
});
// contains `extra-file.txt`
await templateApp('Arm64NoAsarExtraFile.app', 'arm64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app2'),
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'),
);
});
};

View File

@@ -2,8 +2,8 @@
"name": "@electron/universal",
"version": "0.0.0-development",
"description": "Utility for creating Universal macOS applications from two x64 and arm64 Electron applications",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"type": "module",
"exports": "./dist/index.js",
"license": "MIT",
"keywords": [
"electron",
@@ -15,7 +15,7 @@
"url": "git+https://github.com/electron/universal.git"
},
"engines": {
"node": ">=16.4"
"node": ">=22.12.0"
},
"files": [
"dist/*",
@@ -28,38 +28,36 @@
"provenance": true
},
"scripts": {
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.entry-asar.json",
"build": "tsc -p tsconfig.json && tsc -p tsconfig.entry-asar.json",
"build:docs": "npx typedoc",
"lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
"prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
"prepublishOnly": "npm run build",
"test": "jest",
"prepare": "husky install"
"pretest": "npm run build",
"test": "vitest run",
"prepare": "husky"
},
"devDependencies": {
"@electron/get": "^3.0.0",
"@electron/get": "^4.0.0",
"@tsconfig/node22": "^22.0.1",
"@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/node": "~22.10.7",
"@types/plist": "^3.0.4",
"cross-zip": "^4.0.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"lint-staged": "^15.2.10",
"prettier": "^3.0.3",
"ts-jest": "^29.1.1",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"prettier": "^3.5.3",
"typedoc": "~0.25.13",
"typescript": "^5.2.2"
"typescript": "^5.8.3",
"vitest": "^3.1.3"
},
"dependencies": {
"@electron/asar": "^3.3.1",
"@electron/asar": "^4.0.0",
"@malept/cross-spawn-promise": "^2.0.0",
"debug": "^4.3.1",
"dir-compare": "^4.2.0",
"fs-extra": "^11.1.1",
"minimatch": "^9.0.3",
"plist": "^3.1.0"
},
@@ -67,8 +65,5 @@
"*.ts": [
"prettier --write"
]
},
"resolutions": {
"jackspeak": "2.1.1"
}
}

View File

@@ -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;
}
@@ -148,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;
}
@@ -170,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}`);
@@ -186,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]);
@@ -220,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));
};

View File

@@ -1,6 +1,8 @@
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';
const MACHO_PREFIX = 'Mach-O ';
@@ -26,11 +28,11 @@ export const getAllAppFiles = async (appPath: string): Promise<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()) {
let fileType = AppFileType.PLAIN;
@@ -62,7 +64,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
}
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 +73,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;
}
}
};

View File

@@ -1,17 +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, mergeASARs } from './asar-utils';
import { sha } from './sha';
import { d } from './debug';
import { computeIntegrityData } from './integrity';
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.
@@ -88,7 +89,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(
@@ -96,7 +97,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 });
}
}
@@ -110,7 +111,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 {
@@ -120,8 +121,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));
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath));
for (const file of dupedFiles(x64Files)) {
if (!arm64Files.some((f) => f.relativePath === file.relativePath))
@@ -159,8 +160,19 @@ 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));
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));
@@ -192,7 +204,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);
}
@@ -223,26 +235,34 @@ 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.copy(
path.resolve(__dirname, '..', '..', 'entry-asar', 'no-asar.js'),
await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'no-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
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',
),
);
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'),
@@ -279,19 +299,20 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (x64AsarSha !== arm64AsarSha) {
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,
@@ -299,17 +320,18 @@ 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.copy(
path.resolve(__dirname, '..', '..', 'entry-asar', 'has-asar.js'),
await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'has-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
let pj = JSON.parse(
@@ -321,7 +343,11 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
).toString('utf8'),
);
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);
} else {
@@ -337,10 +363,10 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
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(
@@ -355,23 +381,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 });
}
};

View File

@@ -1,8 +1,8 @@
import * as fs from 'fs-extra';
import path from 'path';
import { AppFileType, getAllAppFiles } from './file-utils';
import { sha } from './sha';
import { generateAsarIntegrity } from './asar-utils';
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;
@@ -18,7 +18,7 @@ export interface AsarIntegrity {
}
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
const root = await fs.realpath(contentsPath);
const root = await fs.promises.realpath(contentsPath);
const resourcesRelativePath = 'Resources';
const resourcesPath = path.resolve(root, resourcesRelativePath);

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`makeUniversalApp asar mode should correctly merge two identical asars 1`] = `
exports[`makeUniversalApp > asar mode > should correctly merge two identical asars 1`] = `
{
"files": {
"index.js": {
@@ -29,7 +29,7 @@ exports[`makeUniversalApp asar mode should correctly merge two identical asars 1
}
`;
exports[`makeUniversalApp asar mode should correctly merge two identical asars 2`] = `
exports[`makeUniversalApp > asar mode > should correctly merge two identical asars 2`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
@@ -40,7 +40,7 @@ exports[`makeUniversalApp asar mode should correctly merge two identical asars 2
}
`;
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 1`] = `
exports[`makeUniversalApp > asar mode > should create a shim if asars are different between architectures 1`] = `
{
"files": {
"extra-file.txt": {
@@ -80,7 +80,7 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
}
`;
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 2`] = `
exports[`makeUniversalApp > asar mode > should create a shim if asars are different between architectures 2`] = `
{
"files": {
"index.js": {
@@ -109,7 +109,7 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
}
`;
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 3`] = `
exports[`makeUniversalApp > asar mode > should create a shim if asars are different between architectures 3`] = `
{
"files": {
"index.js": {
@@ -138,7 +138,7 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
}
`;
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 4`] = `
exports[`makeUniversalApp > asar mode > should create a shim if asars are different between architectures 4`] = `
{
"Contents/Info.plist": {
"Resources/app-arm64.asar": {
@@ -157,7 +157,7 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
}
`;
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 1`] = `
exports[`makeUniversalApp > asar mode > should generate AsarIntegrity for all asars in the application 1`] = `
{
"files": {
"index.js": {
@@ -215,7 +215,7 @@ exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars
}
`;
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 2`] = `
exports[`makeUniversalApp > asar mode > should generate AsarIntegrity for all asars in the application 2`] = `
{
"files": {
"index.js": {
@@ -273,7 +273,7 @@ exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars
}
`;
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 3`] = `
exports[`makeUniversalApp > asar mode > should generate AsarIntegrity for all asars in the application 3`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
@@ -288,7 +288,7 @@ exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars
}
`;
exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 1`] = `
exports[`makeUniversalApp > asar mode > should merge two different asars when \`mergeASARs\` is enabled 1`] = `
{
"files": {
"extra-file.txt": {
@@ -328,7 +328,7 @@ exports[`makeUniversalApp asar mode should merge two different asars when \`merg
}
`;
exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 2`] = `
exports[`makeUniversalApp > asar mode > should merge two different asars when \`mergeASARs\` is enabled 2`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
@@ -339,7 +339,7 @@ exports[`makeUniversalApp asar mode should merge two different asars when \`merg
}
`;
exports[`makeUniversalApp asar mode should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = `
exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = `
{
"files": {
"index.js": {
@@ -397,190 +397,14 @@ exports[`makeUniversalApp asar mode should not inject ElectronAsarIntegrity into
}
`;
exports[`makeUniversalApp asar mode should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 2`] = `
exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 2`] = `
{
"Contents/Info.plist": undefined,
"Contents/Resources/SubApp-1.app/Contents/Info.plist": undefined,
}
`;
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 1`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
],
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
},
"size": 66,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
],
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
},
"size": 41,
},
"private": {
"files": {
"var": {
"files": {
"app": {
"files": {
"file.txt": {
"link": "private/var/file.txt",
"unpacked": true,
},
},
},
"file.txt": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
],
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
"size": 11,
"unpacked": true,
},
},
},
},
},
"var": {
"link": "private/var",
"unpacked": true,
},
},
}
`;
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 2`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
],
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
},
"size": 66,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
],
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
},
"size": 41,
},
"private": {
"files": {
"var": {
"files": {
"app": {
"files": {
"file.txt": {
"link": "private/var/file.txt",
},
},
},
"file.txt": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
],
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
"size": 11,
},
},
},
},
},
"var": {
"link": "private/var",
},
},
}
`;
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 3`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b7e5f58d3c0fddc1a57d1279a7f19a34a01784f4036920d4b60a1e33f6d1635b",
],
"hash": "b7e5f58d3c0fddc1a57d1279a7f19a34a01784f4036920d4b60a1e33f6d1635b",
},
"size": 1068,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
],
"hash": "2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
},
"size": 33,
},
},
}
`;
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 4`] = `
[
{
"content": "hello world",
"name": "private/var/file.txt",
},
]
`;
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 5`] = `
{
"Contents/Info.plist": {
"Resources/app-arm64.asar": {
"algorithm": "SHA256",
"hash": "d06a628e759f54def7ff8785a077b3a3d756882cb84ee99e9725966226e1f195",
},
"Resources/app-x64.asar": {
"algorithm": "SHA256",
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
},
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "b62aaaed07ff72dc33da1720d900e0443c060285ef374ce1bdaef1d4f28b5fe4",
},
},
}
`;
exports[`makeUniversalApp force packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 1`] = `
exports[`makeUniversalApp > force > packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 1`] = `
{
"files": {
"index.js": {
@@ -609,7 +433,7 @@ exports[`makeUniversalApp force packages successfully if \`out\` bundle already
}
`;
exports[`makeUniversalApp force packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 2`] = `
exports[`makeUniversalApp > force > packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 2`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
@@ -620,26 +444,7 @@ exports[`makeUniversalApp force packages successfully if \`out\` bundle already
}
`;
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 1`] = `
[
"index.js",
{
"content": "{
"name": "app",
"main": "index.js"
}",
"name": "package.json",
},
]
`;
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 2`] = `
{
"Contents/Info.plist": {},
}
`;
exports[`makeUniversalApp no asar mode should shim two different app folders 1`] = `
exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 1`] = `
{
"files": {
"index.js": {
@@ -668,13 +473,248 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 1`]
}
`;
exports[`makeUniversalApp no asar mode should shim two different app folders 2`] = `
exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 2`] = `
[
"private/var/i-aint-got-no-rhythm.bin",
]
`;
exports[`makeUniversalApp no asar mode should shim two different app folders 3`] = `
exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 3`] = `
[
"hello-world",
"index.js",
{
"content": "{
"name": "app",
"main": "index.js"
}",
"name": "package.json",
},
{
"content": "hello world",
"name": "private/var/file.txt",
},
"private/var/i-aint-got-no-rhythm.bin",
]
`;
exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 4`] = `
[
"hello-world",
"index.js",
{
"content": "{
"name": "app",
"main": "index.js"
}",
"name": "package.json",
},
{
"content": "hello world",
"name": "private/var/file.txt",
},
"private/var/hello-world.bin",
]
`;
exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 5`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "<stripped>",
},
},
}
`;
exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 1`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
],
"hash": "f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
},
"size": 1063,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
],
"hash": "2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
},
"size": 33,
},
},
}
`;
exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 2`] = `
[
"private/var/i-aint-got-no-rhythm.bin",
]
`;
exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 3`] = `
[
"hello-world",
"index.js",
{
"content": "{
"name": "app",
"main": "index.js"
}",
"name": "package.json",
},
{
"content": "hello world",
"name": "private/var/file.txt",
},
"private/var/i-aint-got-no-rhythm.bin",
]
`;
exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 4`] = `
[
"hello-world",
"index.js",
{
"content": "{
"name": "app",
"main": "index.js"
}",
"name": "package.json",
},
{
"content": "hello world",
"name": "private/var/file.txt",
},
"private/var/hello-world.bin",
]
`;
exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 5`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "<stripped>",
},
},
}
`;
exports[`makeUniversalApp > no asar mode > identical app dirs with different macho files (e.g. do not shim, but still lipo) 1`] = `
[
"hello-world",
"index.js",
{
"content": "{
"name": "app",
"main": "index.js"
}",
"name": "package.json",
},
{
"content": "hello world",
"name": "private/var/file.txt",
},
]
`;
exports[`makeUniversalApp > no asar mode > identical app dirs with different macho files (e.g. do not shim, but still lipo) 2`] = `
{
"Contents/Info.plist": {},
}
`;
exports[`makeUniversalApp > no asar mode > identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 1`] = `
[
"hello-world",
"index.js",
{
"content": "{
"name": "app",
"main": "index.js"
}",
"name": "package.json",
},
{
"content": "hello world",
"name": "private/var/file.txt",
},
]
`;
exports[`makeUniversalApp > no asar mode > identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 2`] = `
{
"Contents/Info.plist": {},
}
`;
exports[`makeUniversalApp > no asar mode > should correctly merge two identical app folders 1`] = `
[
"index.js",
{
"content": "{
"name": "app",
"main": "index.js"
}",
"name": "package.json",
},
]
`;
exports[`makeUniversalApp > no asar mode > should correctly merge two identical app folders 2`] = `
{
"Contents/Info.plist": {},
}
`;
exports[`makeUniversalApp > no asar mode > should shim two different app folders 1`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
],
"hash": "f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
},
"size": 1063,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
],
"hash": "2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
},
"size": 33,
},
},
}
`;
exports[`makeUniversalApp > no asar mode > should shim two different app folders 2`] = `
[
"private/var/i-aint-got-no-rhythm.bin",
]
`;
exports[`makeUniversalApp > no asar mode > should shim two different app folders 3`] = `
[
"index.js",
{
@@ -692,7 +732,7 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 3`]
]
`;
exports[`makeUniversalApp no asar mode should shim two different app folders 4`] = `
exports[`makeUniversalApp > no asar mode > should shim two different app folders 4`] = `
[
"index.js",
{
@@ -710,7 +750,7 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 4`]
]
`;
exports[`makeUniversalApp no asar mode should shim two different app folders 5`] = `
exports[`makeUniversalApp > no asar mode > should shim two different app folders 5`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
@@ -720,3 +760,81 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 5`]
},
}
`;
exports[`makeUniversalApp > works for lipo binary resources 1`] = `
{
"files": {
"hello-world": "<stripped>",
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
],
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
},
"size": 66,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
],
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
},
"size": 41,
},
"private": {
"files": {
"var": {
"files": {
"app": {
"files": {
"file.txt": {
"link": "private/var/file.txt",
},
},
},
"file.txt": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
],
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
"size": 11,
},
},
},
},
},
"var": {
"link": "private/var",
},
},
}
`;
exports[`makeUniversalApp > works for lipo binary resources 2`] = `[]`;
exports[`makeUniversalApp > works for lipo binary resources 3`] = `
[
"hello-world",
]
`;
exports[`makeUniversalApp > works for lipo binary resources 4`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "<stripped>",
},
},
}
`;

View File

@@ -1,9 +1,11 @@
import * as path from 'path';
import * as path from 'node:path';
import { AsarMode, detectAsarMode, generateAsarIntegrity } from '../src/asar-utils';
import { describe, expect, it } from 'vitest';
const asarsPath = path.resolve(__dirname, 'fixtures', 'asars');
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
import { AsarMode, detectAsarMode, generateAsarIntegrity } from '../src/asar-utils.js';
const asarsPath = path.resolve(import.meta.dirname, 'fixtures', 'asars');
const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
describe('asar-utils', () => {
describe('detectAsarMode', () => {

View File

@@ -1,8 +1,10 @@
import * as path from 'path';
import * as path from 'node:path';
import { AppFile, AppFileType, getAllAppFiles } from '../src/file-utils';
import { beforeAll, describe, expect, it } from 'vitest';
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
import { AppFile, AppFileType, getAllAppFiles } from '../src/file-utils.js';
const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
describe('file-utils', () => {
describe('getAllAppFiles', () => {

6
test/fixtures/hello-world.c vendored Normal file
View File

@@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}

82
test/globalSetup.ts Normal file
View File

@@ -0,0 +1,82 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { appsDir, asarsDir, fixtureDir, templateApp } from './util.js';
// generates binaries from hello-world.c
// hello-world-universal, hello-world-x86_64, hello-world-arm64
const generateMachO = () => {
const src = path.resolve(fixtureDir, 'hello-world.c');
const outputFiles = ['x86_64', 'arm64'].map((arch) => {
const machO = path.resolve(appsDir, `hello-world-${arch === 'x86_64' ? 'x64' : arch}`);
execFileSync('clang', ['-arch', arch, '-o', machO, src]);
return machO;
});
execFileSync('lipo', [
...outputFiles,
'-create',
'-output',
path.resolve(appsDir, 'hello-world-universal'),
]);
};
export default async () => {
await fs.promises.rm(appsDir, { recursive: true, force: true });
await fs.promises.mkdir(appsDir, { recursive: true });
// generate mach-o binaries to be leveraged in lipo tests
generateMachO();
await templateApp('Arm64Asar.app', 'arm64', async (appPath) => {
await fs.promises.cp(
path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
);
});
// contains `extra-file.txt`
await templateApp('Arm64AsarExtraFile.app', 'arm64', async (appPath) => {
await fs.promises.cp(
path.resolve(asarsDir, 'app2.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
);
});
await templateApp('X64Asar.app', 'x64', async (appPath) => {
await fs.promises.cp(
path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
);
});
await templateApp('Arm64NoAsar.app', 'arm64', async (appPath) => {
await fs.promises.cp(
path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
);
});
// contains `extra-file.txt`
await templateApp('Arm64NoAsarExtraFile.app', 'arm64', async (appPath) => {
await fs.promises.cp(
path.resolve(asarsDir, 'app2'),
path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
);
});
await templateApp('X64NoAsar.app', 'x64', async (appPath) => {
await fs.promises.cp(
path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
);
});
};

View File

@@ -1,17 +1,27 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import fs from 'node:fs';
import path from 'node:path';
import { makeUniversalApp } from '../dist/cjs/index';
import { createTestApp, templateApp, VERIFY_APP_TIMEOUT, verifyApp } from './util';
import { afterEach, describe, expect, it } from 'vitest';
import { makeUniversalApp } from '../dist/index.js';
import { fsMove } from '../src/file-utils.js';
import {
createStagingAppDir,
generateNativeApp,
templateApp,
VERIFY_APP_TIMEOUT,
verifyApp,
} from './util.js';
import { createPackage, createPackageWithOptions } from '@electron/asar';
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
const appsOutPath = path.resolve(__dirname, 'fixtures', 'apps', 'out');
const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
const appsOutPath = path.resolve(import.meta.dirname, 'fixtures', 'apps', 'out');
// See `jest.setup.ts` for app fixture setup process
// See `globalSetup.ts` for app fixture setup process
describe('makeUniversalApp', () => {
afterEach(async () => {
await fs.emptyDir(appsOutPath);
await fs.promises.rm(appsOutPath, { force: true, recursive: true });
await fs.promises.mkdir(appsOutPath, { recursive: true });
});
it('throws an error if asar is only detected in one arch', async () => {
@@ -27,12 +37,27 @@ describe('makeUniversalApp', () => {
);
});
it.todo('works for lipo binary resources');
it('works for lipo binary resources', { timeout: VERIFY_APP_TIMEOUT }, async () => {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'LipoX64.app',
arch: 'x64',
createAsar: true,
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'LipoArm64.app',
arch: 'arm64',
createAsar: true,
});
const out = path.resolve(appsOutPath, 'Lipo.app');
await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out, mergeASARs: true });
await verifyApp(out, true);
});
describe('force', () => {
it('throws an error if `out` bundle already exists and `force` is `false`', async () => {
const out = path.resolve(appsOutPath, 'Error.app');
await fs.mkdirp(out);
await fs.promises.mkdir(out, { recursive: true });
await expect(
makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
@@ -44,9 +69,10 @@ describe('makeUniversalApp', () => {
it(
'packages successfully if `out` bundle already exists and `force` is `true`',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const out = path.resolve(appsOutPath, 'Error.app');
await fs.mkdirp(out);
const out = path.resolve(appsOutPath, 'NoError.app');
await fs.promises.mkdir(out, { recursive: true });
await makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
@@ -55,27 +81,23 @@ describe('makeUniversalApp', () => {
});
await verifyApp(out);
},
VERIFY_APP_TIMEOUT,
);
});
describe('asar mode', () => {
it(
'should correctly merge two identical asars',
async () => {
const out = path.resolve(appsOutPath, 'MergedAsar.app');
await makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
outAppPath: out,
});
await verifyApp(out);
},
VERIFY_APP_TIMEOUT,
);
it('should correctly merge two identical asars', { timeout: VERIFY_APP_TIMEOUT }, async () => {
const out = path.resolve(appsOutPath, 'MergedAsar.app');
await makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
outAppPath: out,
});
await verifyApp(out);
});
it(
'should create a shim if asars are different between architectures',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const out = path.resolve(appsOutPath, 'ShimmedAsar.app');
await makeUniversalApp({
@@ -85,11 +107,11 @@ describe('makeUniversalApp', () => {
});
await verifyApp(out);
},
VERIFY_APP_TIMEOUT,
);
it(
'should merge two different asars when `mergeASARs` is enabled',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const out = path.resolve(appsOutPath, 'MergedAsar.app');
await makeUniversalApp({
@@ -101,11 +123,11 @@ describe('makeUniversalApp', () => {
});
await verifyApp(out);
},
VERIFY_APP_TIMEOUT,
);
it(
'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique file',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const out = path.resolve(appsOutPath, 'Error.app');
await expect(
@@ -118,27 +140,27 @@ describe('makeUniversalApp', () => {
}),
).rejects.toThrow(/Detected unique file "extra-file\.txt"/);
},
VERIFY_APP_TIMEOUT,
);
it(
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const arm64AppPath = await templateApp('Arm64-1.app', 'arm64', async (appPath) => {
const { testPath } = await createTestApp('Arm64-1');
const { testPath } = await createStagingAppDir('Arm64-1');
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
await templateApp('SubApp-1.app', 'arm64', async (subArm64AppPath) => {
await fs.move(
await fsMove(
subArm64AppPath,
path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
);
});
});
const x64AppPath = await templateApp('X64-1.app', 'x64', async (appPath) => {
const { testPath } = await createTestApp('X64-1');
const { testPath } = await createStagingAppDir('X64-1');
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
await templateApp('SubApp-1.app', 'x64', async (subArm64AppPath) => {
await fs.move(
await fsMove(
subArm64AppPath,
path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
);
@@ -154,7 +176,6 @@ describe('makeUniversalApp', () => {
});
await verifyApp(outAppPath);
},
VERIFY_APP_TIMEOUT,
);
// TODO: Investigate if this should even be allowed.
@@ -162,9 +183,10 @@ describe('makeUniversalApp', () => {
// https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49
it.skip(
'should shim asars with different unpacked dirs',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
const { testPath } = await createTestApp('UnpackedAppArm64');
const { testPath } = await createStagingAppDir('UnpackedAppArm64');
await createPackageWithOptions(
testPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
@@ -176,7 +198,7 @@ describe('makeUniversalApp', () => {
});
const x64AppPath = await templateApp('UnpackedX64.app', 'x64', async (appPath) => {
const { testPath } = await createTestApp('UnpackedAppX64');
const { testPath } = await createStagingAppDir('UnpackedAppX64');
await createPackageWithOptions(
testPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
@@ -192,32 +214,32 @@ describe('makeUniversalApp', () => {
});
await verifyApp(outAppPath);
},
VERIFY_APP_TIMEOUT,
);
it(
'should generate AsarIntegrity for all asars in the application',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const { testPath } = await createTestApp('app-2');
const { testPath } = await createStagingAppDir('app-2');
const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
await createPackage(testPath, testAsarPath);
const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => {
await fs.copyFile(
await fs.promises.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
await fs.copyFile(
await fs.promises.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'),
);
});
const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => {
await fs.copyFile(
await fs.promises.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
await fs.copyFile(
await fs.promises.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'),
);
@@ -231,13 +253,13 @@ describe('makeUniversalApp', () => {
});
await verifyApp(outAppPath);
},
VERIFY_APP_TIMEOUT,
);
});
describe('no asar mode', () => {
it(
'should correctly merge two identical app folders',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const out = path.resolve(appsOutPath, 'MergedNoAsar.app');
await makeUniversalApp({
@@ -247,37 +269,148 @@ describe('makeUniversalApp', () => {
});
await verifyApp(out);
},
VERIFY_APP_TIMEOUT,
);
it('should shim two different app folders', { timeout: VERIFY_APP_TIMEOUT }, async () => {
const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => {
const { testPath } = await createStagingAppDir('shimArm64', {
'i-aint-got-no-rhythm.bin': 'boomshakalaka',
});
await fs.promises.cp(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'), {
recursive: true,
verbatimSymlinks: true,
});
});
const x64AppPath = await templateApp('ShimX64.app', 'x64', async (appPath) => {
const { testPath } = await createStagingAppDir('shimX64', {
'hello-world.bin': 'Hello World',
});
await fs.promises.cp(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'), {
recursive: true,
verbatimSymlinks: true,
});
});
const outAppPath = path.resolve(appsOutPath, 'ShimNoAsar.app');
await makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath,
});
await verifyApp(outAppPath);
});
it(
'should shim two different app folders',
'different app dirs with different macho files (shim and lipo)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => {
const { testPath } = await createTestApp('shimArm64', {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentMachoAppX64-1.app',
arch: 'x64',
createAsar: false,
additionalFiles: {
'hello-world.bin': 'Hello World',
},
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentMachoAppArm64-1.app',
arch: 'arm64',
createAsar: false,
additionalFiles: {
'i-aint-got-no-rhythm.bin': 'boomshakalaka',
});
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'));
},
});
const x64AppPath = await templateApp('ShimX64.app', 'x64', async (appPath) => {
const { testPath } = await createTestApp('shimX64', { 'hello-world.bin': 'Hello World' });
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'));
});
const outAppPath = path.resolve(appsOutPath, 'ShimNoAsar.app');
const outAppPath = path.resolve(appsOutPath, 'DifferentMachoApp1.app');
await makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath,
});
await verifyApp(outAppPath);
await verifyApp(outAppPath, true);
},
);
it(
"different app dirs with universal macho files (shim but don't lipo)",
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentButUniversalMachoAppX64-2.app',
arch: 'x64',
createAsar: false,
nativeModuleArch: 'universal',
additionalFiles: {
'hello-world.bin': 'Hello World',
},
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentButUniversalMachoAppArm64-2.app',
arch: 'arm64',
createAsar: false,
nativeModuleArch: 'universal',
additionalFiles: {
'i-aint-got-no-rhythm.bin': 'boomshakalaka',
},
});
const outAppPath = path.resolve(appsOutPath, 'DifferentButUniversalMachoApp.app');
await makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath,
});
await verifyApp(outAppPath, true);
},
);
it(
'identical app dirs with different macho files (e.g. do not shim, but still lipo)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentMachoAppX64-2.app',
arch: 'x64',
createAsar: false,
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentMachoAppArm64-2.app',
arch: 'arm64',
createAsar: false,
});
const out = path.resolve(appsOutPath, 'DifferentMachoApp2.app');
await makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath: out,
});
await verifyApp(out, true);
},
);
it(
'identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'UniversalMachoAppX64.app',
arch: 'x64',
createAsar: false,
nativeModuleArch: 'universal',
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'UniversalMachoAppArm64.app',
arch: 'arm64',
createAsar: false,
nativeModuleArch: 'universal',
});
const out = path.resolve(appsOutPath, 'UniversalMachoApp.app');
await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out });
await verifyApp(out, true);
},
VERIFY_APP_TIMEOUT,
);
});
// TODO: Add tests for
// * different app dirs with different macho files
// * identical app dirs with universal macho files
});

View File

@@ -1,10 +1,12 @@
import * as path from 'path';
import path from 'node:path';
import { sha } from '../src/sha';
import { describe, expect, it } from 'vitest';
import { sha } from '../src/sha.js';
describe('sha', () => {
it('should correctly hash a file', async () => {
expect(await sha(path.resolve(__dirname, 'fixtures', 'tohash'))).toEqual(
expect(await sha(path.resolve(import.meta.dirname, 'fixtures', 'tohash'))).toEqual(
'12998c017066eb0d2a70b94e6ed3192985855ce390f321bbdb832022888bd251',
);
});

View File

@@ -1,33 +1,40 @@
import fs from 'node:fs';
import path from 'node:path';
import { createPackageWithOptions, getRawHeader } from '@electron/asar';
import { downloadArtifact } from '@electron/get';
import { spawn } from '@malept/cross-spawn-promise';
import * as zip from 'cross-zip';
import * as fs from 'fs-extra';
import * as path from 'path';
import plist from 'plist';
import * as fileUtils from '../dist/cjs/file-utils';
import { getRawHeader } from '@electron/asar';
import * as fileUtils from '../dist/file-utils.js';
// We do a LOT of verifications in `verifyApp` 😅
// exec universal binary -> verify ALL asars -> verify ALL app dirs -> verify ALL asar integrity entries
// plus some tests create fixtures at runtime
export const VERIFY_APP_TIMEOUT = 80 * 1000;
export const asarsDir = path.resolve(__dirname, 'fixtures', 'asars');
export const appsDir = path.resolve(__dirname, 'fixtures', 'apps');
export const fixtureDir = path.resolve(import.meta.dirname, 'fixtures');
export const asarsDir = path.resolve(fixtureDir, 'asars');
export const appsDir = path.resolve(fixtureDir, 'apps');
export const appsOutPath = path.resolve(appsDir, 'out');
export const verifyApp = async (appPath: string) => {
export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho = false) => {
const { expect } = await import('vitest');
await ensureUniversal(appPath);
const resourcesDir = path.resolve(appPath, 'Contents', 'Resources');
const resourcesDirContents = await fs.readdir(resourcesDir);
const resourcesDirContents = await fs.promises.readdir(resourcesDir);
// sort for consistent result
const asars = resourcesDirContents.filter((p) => p.endsWith('.asar')).sort();
for await (const asar of asars) {
// verify header
const asarFs = getRawHeader(path.resolve(resourcesDir, asar));
expect(removeUnstableProperties(asarFs.header)).toMatchSnapshot();
expect(
removeUnstableProperties(asarFs.header, containsRuntimeGeneratedMacho ? ['hello-world'] : []),
).toMatchSnapshot();
}
// check all app and unpacked dirs (incl. shimmed)
@@ -64,20 +71,24 @@ export const verifyApp = async (appPath: string) => {
for (let i = 0; i < integrity.length; i++) {
const relativePath = infoPlists[i];
const asarIntegrity = integrity[i];
integrityMap[relativePath] = asarIntegrity;
// note: `infoPlistsToIgnore` will not have integrity in sub-app plists
integrityMap[relativePath] = asarIntegrity
? removeUnstableProperties(asarIntegrity, containsRuntimeGeneratedMacho ? ['hash'] : [])
: undefined;
}
expect(integrityMap).toMatchSnapshot();
};
// note: `infoPlistsToIgnore` will not have integrity in sub-app plists
const extractAsarIntegrity = async (infoPlist: string) => {
const { ElectronAsarIntegrity: integrity, ...otherData } = plist.parse(
await fs.readFile(infoPlist, 'utf-8'),
await fs.promises.readFile(infoPlist, 'utf-8'),
) as any;
return integrity;
};
export const verifyFileTree = async (dirPath: string) => {
const { expect } = await import('vitest');
const dirFiles = await fileUtils.getAllAppFiles(dirPath);
const files = dirFiles.map((file) => {
const it = path.join(dirPath, file.relativePath);
@@ -91,6 +102,8 @@ export const verifyFileTree = async (dirPath: string) => {
};
export const ensureUniversal = async (app: string) => {
const { expect } = await import('vitest');
const exe = path.resolve(app, 'Contents', 'MacOS', 'Electron');
const result = await spawn(exe);
expect(result).toContain('arm64');
@@ -102,9 +115,29 @@ export const toSystemIndependentPath = (s: string): string => {
return path.sep === '/' ? s : s.replace(/\\/g, '/');
};
export const removeUnstableProperties = (data: any) => {
export const removeUnstableProperties = (data: any, stripKeys: string[]) => {
const removeKeysRecursively: (obj: any, keysToRemove: string[]) => any = (obj, keysToRemove) => {
if (!obj || typeof obj !== 'object') {
return obj;
}
// if the value is an array, map over it
if (Array.isArray(obj)) {
return obj.map((item: any) => removeKeysRecursively(item, keysToRemove));
}
return Object.keys(obj).reduce<any>((acc, key) => {
// if the value of the current key is another object, make a recursive call to remove the key from the nested object
if (!keysToRemove.includes(key)) {
acc[key] = removeKeysRecursively(obj[key], keysToRemove);
} else {
acc[key] = '<stripped>';
}
return acc;
}, {});
};
const filteredData = removeKeysRecursively(data, stripKeys);
return JSON.parse(
JSON.stringify(data, (name, value) => {
JSON.stringify(filteredData, (name, value) => {
if (name === 'offset') {
return undefined;
}
@@ -114,6 +147,10 @@ export const removeUnstableProperties = (data: any) => {
};
/**
* Creates an app directory at runtime for usage:
* - `testPath` can be used with `asar.createPackage`. Just set the output `.asar` path to `Test.app/Contents/Resources/<asar_name>.asar`
* - `testPath` can be utilized for logic paths involving `AsarMode.NO_ASAR` and copied directly to `Test.app/Contents/Resources`
*
* Directory structure:
* testName
* ├── private
@@ -125,21 +162,24 @@ export const removeUnstableProperties = (data: any) => {
* ├── index.js
* ├── package.json
*/
export const createTestApp = async (
export const createStagingAppDir = async (
testName: string | undefined,
additionalFiles: Record<string, string> = {},
) => {
const outDir = (testName || 'app') + Math.floor(Math.random() * 100); // tests run in parallel, randomize dir suffix to prevent naming collisions
const testPath = path.join(appsDir, outDir);
await fs.remove(testPath);
await fs.promises.rm(testPath, { recursive: true, force: true });
await fs.copy(path.join(asarsDir, 'app'), testPath);
await fs.promises.cp(path.join(asarsDir, 'app'), testPath, {
recursive: true,
verbatimSymlinks: true,
});
const privateVarPath = path.join(testPath, 'private', 'var');
const varPath = path.join(testPath, 'var');
await fs.mkdir(privateVarPath, { recursive: true });
await fs.symlink(path.relative(testPath, privateVarPath), varPath);
await fs.promises.mkdir(privateVarPath, { recursive: true });
await fs.promises.symlink(path.relative(testPath, privateVarPath), varPath);
const files = {
'file.txt': 'hello world',
@@ -147,11 +187,11 @@ export const createTestApp = async (
};
for await (const [filename, fileData] of Object.entries(files)) {
const originFilePath = path.join(varPath, filename);
await fs.writeFile(originFilePath, fileData);
await fs.promises.writeFile(originFilePath, fileData);
}
const appPath = path.join(varPath, 'app');
await fs.mkdirp(appPath);
await fs.symlink('../file.txt', path.join(appPath, 'file.txt'));
await fs.promises.mkdir(appPath, { recursive: true });
await fs.promises.symlink('../file.txt', path.join(appPath, 'file.txt'));
return {
testPath,
@@ -173,9 +213,52 @@ export const templateApp = async (
});
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 fs.promises.rename(path.resolve(appsDir, 'Electron.app'), appPath);
await fs.promises.rm(path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar'), {
recursive: true,
force: true,
});
await modify(appPath);
return appPath;
};
export const generateNativeApp = async (options: {
appNameWithExtension: string;
arch: string;
createAsar: boolean;
nativeModuleArch?: string;
additionalFiles?: Record<string, string>;
}) => {
const {
appNameWithExtension,
arch,
createAsar,
nativeModuleArch = arch,
additionalFiles,
} = options;
const appPath = await templateApp(appNameWithExtension, arch, async (appPath) => {
const resources = path.join(appPath, 'Contents', 'Resources');
const resourcesApp = path.resolve(resources, 'app');
if (!fs.existsSync(resourcesApp)) {
await fs.promises.mkdir(resourcesApp, { recursive: true });
}
const { testPath } = await createStagingAppDir(
path.basename(appNameWithExtension, '.app'),
additionalFiles,
);
await fs.promises.cp(
path.join(appsDir, `hello-world-${nativeModuleArch}`),
path.join(testPath, 'hello-world'),
{ recursive: true, verbatimSymlinks: true },
);
if (createAsar) {
await createPackageWithOptions(testPath, path.resolve(resources, 'app.asar'), {
unpack: '**/hello-world',
});
} else {
await fs.promises.cp(testPath, resourcesApp, { recursive: true, verbatimSymlinks: true });
}
});
return appPath;
};

View File

@@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["src"]
}

View File

@@ -1,7 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"lib": [
"es2017"
],
"sourceMap": true,
"strict": true,
"outDir": "entry-asar",
"types": [
"node",
],
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"esModuleInterop": true,
"declaration": false
},
"include": [
"entry-asar"

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm"
},
"include": ["src"]
}

View File

@@ -1,11 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm",
"types": [
"jest"
]
},
"include": ["src"]
}

View File

@@ -1,23 +1,15 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"lib": [
"es2017"
],
"sourceMap": true,
"strict": true,
"outDir": "dist/cjs",
"outDir": "dist",
"types": [
"node",
],
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"esModuleInterop": true,
"declaration": true
},
"include": [
"src",
"entry-asar"
"src"
]
}

7
vitest.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globalSetup: './test/globalSetup.ts',
},
});

3083
yarn.lock

File diff suppressed because it is too large Load Diff