19 Commits
v2.0.3 ... main

Author SHA1 Message Date
dependabot[bot]
bf1269fe21 build(deps): bump glob from 10.4.5 to 10.5.0 (#159)
Some checks failed
Release / test (push) Failing after 1s
Release / Release (push) Has been skipped
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 13:54:26 -08:00
dependabot[bot]
ca53e14488 build(deps): bump actions/checkout from 5.0.0 to 6.0.0 (#158)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...1af3b93b68)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 10:59:33 -08:00
dependabot[bot]
1b9f5eb340 build(deps): bump tar from 7.5.1 to 7.5.2 (#157)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.1 to 7.5.2.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.1...v7.5.2)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 16:07:54 -08:00
Keeley Hammond
53c72d8c47 ci: use npm trusted publishing instead of CFA (#156) 2025-11-13 15:33:47 -08:00
Erick Zhao
b61638598d docs: add API docs and clean up README (#155) 2025-11-12 13:09:18 -08:00
dependabot[bot]
0a0b41d115 build(deps): bump vite from 6.3.6 to 6.4.1 (#154)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 17:51:24 -08:00
dependabot[bot]
ed0459457f build(deps): bump actions/setup-node from 5.0.0 to 6.0.0 (#153)
* build(deps): bump actions/setup-node from 5.0.0 to 6.0.0

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](a0853c2454...2028fbc5c2)

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

Signed-off-by: dependabot[bot] <support@github.com>

* ci: use yarn cache

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2025-11-03 15:24:25 -08:00
Fedor Indutny
0939980564 fix: fully respect singleArchFiles option (#152)
Some checks failed
Publish documentation / docs (push) Failing after 13s
Files listed under `singleArchFiles` are allowed to be unique for
different platforms so `dupedFiles` should not return them.

Fix: #151
2025-10-29 12:52:59 -04:00
dependabot[bot]
2e087ef6c8 build(deps): bump brace-expansion from 1.1.11 to 1.1.12 (#150)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 10:16:37 -07:00
Erick Zhao
355fd7c6e8 build: upgrade to Yarn v4 (#149) 2025-10-17 01:51:49 -07:00
dependabot[bot]
7a73b7793e build(deps): bump azure/cli from 2.1.0 to 2.2.0 (#147)
Bumps [azure/cli](https://github.com/azure/cli) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/azure/cli/releases)
- [Changelog](https://github.com/Azure/cli/blob/master/ReleaseProcess.md)
- [Commits](089eac9d8c...9f7ce6f37c)

---
updated-dependencies:
- dependency-name: azure/cli
  dependency-version: 2.2.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-10-01 14:26:10 -07:00
dependabot[bot]
436b2abeff build(deps): bump actions/setup-node from 4.4.0 to 5.0.0 (#146)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 5.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](49933ea528...a0853c2454)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:25:03 -07:00
dependabot[bot]
01eec61200 build(deps): bump vite from 6.3.5 to 6.3.6 (#145)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 19:53:39 -07:00
dependabot[bot]
9a2c19c940 build(deps): bump amannn/action-semantic-pull-request (#142)
Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 5.5.3 to 6.1.1.
- [Release notes](https://github.com/amannn/action-semantic-pull-request/releases)
- [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md)
- [Commits](0723387faa...48f256284b)

---
updated-dependencies:
- dependency-name: amannn/action-semantic-pull-request
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Samuel Attard <sam@electronjs.org>
2025-09-01 23:55:52 -07:00
dependabot[bot]
4eb37fab7b build(deps): bump actions/checkout from 4.2.2 to 5.0.0 (#143)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](11bd71901b...08c6903cd8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-01 23:41:51 -07:00
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
32 changed files with 4533 additions and 3605 deletions

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

@@ -0,0 +1,36 @@
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn --immutable
- 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@9f7ce6f37c31b777ec6c6b6d1dfe7db79f497956 # v2.2.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

@@ -13,23 +13,25 @@ jobs:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: test needs: test
environment: npm environment: npm-trusted-publisher
permissions: permissions:
id-token: write # for CFA and npm provenance id-token: write # for publishing releases
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version: 20.x node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- name: Install - name: Install
run: yarn install --frozen-lockfile run: yarn install --immutable
- uses: continuousauth/action@4e8a2573eeb706f6d7300d6a9f3ca6322740b72d # v1.0.5 - name: Get GitHub App Token
id: secret-service
uses: electron/secret-service-action@3476425e8b30555aac15b1b7096938e254b0e155 # v1.0.0
- name: Run Semantic Release
uses: electron/semantic-trusted-release@5eceb399ac8de8863205cf6e34109bce473ba566 # v1.0.1
with: with:
project-id: ${{ secrets.CFA_PROJECT_ID }} github-token: ${{ fromJSON(steps.secret-service.outputs.secrets).GITHUB_TOKEN }}
secret: ${{ secrets.CFA_SECRET }}
npm-token: ${{ secrets.NPM_TOKEN }}

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: semantic-pull-request - name: semantic-pull-request
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:

View File

@@ -18,24 +18,18 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: node-version:
- '20.5' - 22.12.x
- '18.17'
- '16.20'
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version: "${{ matrix.node-version }}" node-version: "${{ matrix.node-version }}"
cache: 'yarn' cache: 'yarn'
- name: Install (Node.js v18+) - name: Install
if : ${{ matrix.node-version != '16.20' }} run: yarn install --immutable
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 - name: Build
run: yarn build run: yarn build
- name: Lint - name: Lint

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ test/fixtures/apps
coverage coverage
docs docs
.vscode .vscode
.yarn/install-state.gz

View File

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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
22.12

View File

@@ -2,7 +2,7 @@
"plugins": [ "plugins": [
"@semantic-release/commit-analyzer", "@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
"@continuous-auth/semantic-release-npm", "@semantic-release/npm",
"@semantic-release/github" "@semantic-release/github"
], ],
"branches": [ "main" ] "branches": [ "main" ]

942
.yarn/releases/yarn-4.10.3.cjs vendored Executable file

File diff suppressed because one or more lines are too long

10
.yarnrc.yml Normal file
View File

@@ -0,0 +1,10 @@
enableScripts: false
nodeLinker: node-modules
npmMinimalAgeGate: 10080
npmPreapprovedPackages:
- "@electron/*"
yarnPath: .yarn/releases/yarn-4.10.3.cjs

View File

@@ -4,6 +4,7 @@
[![Test](https://github.com/electron/universal/actions/workflows/test.yml/badge.svg)](https://github.com/electron/universal/actions/workflows/test.yml) [![Test](https://github.com/electron/universal/actions/workflows/test.yml/badge.svg)](https://github.com/electron/universal/actions/workflows/test.yml)
[![NPM package](https://img.shields.io/npm/v/@electron/universal)](https://npm.im/@electron/universal) [![NPM package](https://img.shields.io/npm/v/@electron/universal)](https://npm.im/@electron/universal)
[![API docs](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.npmjs.org%2F%40electron%2Funiversal%2Flatest&query=%24.version&logo=typescript&logoColor=white&label=API%20Docs)](https://packages.electronjs.org/universal)
## Usage ## Usage
@@ -22,6 +23,8 @@ await makeUniversalApp({
}); });
``` ```
For full API usage, see the [API documentation](https://packages.electronjs.org/universal).
## Advanced configuration ## Advanced configuration
The basic usage patterns will work for most apps out of the box. Additional configuration The basic usage patterns will work for most apps out of the box. Additional configuration
@@ -118,8 +121,4 @@ Note that if you are using `mergeASARs`, you may need to add architecture-specif
binary resources to the `singleArchFiles` pattern. binary resources to the `singleArchFiles` pattern.
See [Merging ASARs usage](#merging-asar-archives-to-reduce-app-size) for an example. See [Merging ASARs usage](#merging-asar-archives-to-reduce-app-size) for an example.
#### How do I build my app for Apple silicon in the first place?
Check out the [Electron Apple silicon blog post](https://www.electronjs.org/blog/apple-silicon).
[`minimatch`]: https://github.com/isaacs/minimatch?tab=readme-ov-file#features [`minimatch`]: https://github.com/isaacs/minimatch?tab=readme-ov-file#features

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

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

View File

@@ -1,11 +1,13 @@
import asar from '@electron/asar'; import { execFileSync } from 'node:child_process';
import { execFileSync } from 'child_process'; import crypto from 'node:crypto';
import crypto from 'crypto'; import fs from 'node:fs';
import fs from 'fs-extra'; import os from 'node:os';
import path from 'path'; import path from 'node:path';
import * as asar from '@electron/asar';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import os from 'os';
import { d } from './debug'; import { d } from './debug.js';
const LIPO = 'lipo'; const LIPO = 'lipo';
@@ -40,7 +42,7 @@ export const detectAsarMode = async (appPath: string) => {
d('checking asar mode of', appPath); d('checking asar mode of', appPath);
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar'); const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
if (!(await fs.pathExists(asarPath))) { if (!fs.existsSync(asarPath)) {
d('determined no asar'); d('determined no asar');
return AsarMode.NO_ASAR; return AsarMode.NO_ASAR;
} }
@@ -169,8 +171,8 @@ export const mergeASARs = async ({
// Extract both // Extract both
// //
const x64Dir = await fs.mkdtemp(path.join(os.tmpdir(), 'x64-')); const x64Dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'x64-'));
const arm64Dir = await fs.mkdtemp(path.join(os.tmpdir(), 'arm64-')); const arm64Dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'arm64-'));
try { try {
d(`extracting ${x64AsarPath} to ${x64Dir}`); d(`extracting ${x64AsarPath} to ${x64Dir}`);
@@ -185,18 +187,22 @@ export const mergeASARs = async ({
if (isDirectory(arm64AsarPath, file)) { if (isDirectory(arm64AsarPath, file)) {
d(`creating unique directory: ${file}`); d(`creating unique directory: ${file}`);
await fs.mkdirp(destination); await fs.promises.mkdir(destination, { recursive: true });
continue; continue;
} }
d(`xopying unique file: ${file}`); d(`copying unique file: ${file}`);
await fs.mkdirp(path.dirname(destination)); await fs.promises.mkdir(path.dirname(destination), { recursive: true });
await fs.copy(source, destination); await fs.promises.cp(source, destination, {
force: true,
recursive: true,
verbatimSymlinks: true,
});
} }
for (const binding of commonBindings) { for (const binding of commonBindings) {
const source = await fs.realpath(path.resolve(arm64Dir, binding)); const source = await fs.promises.realpath(path.resolve(arm64Dir, binding));
const destination = await fs.realpath(path.resolve(x64Dir, binding)); const destination = await fs.promises.realpath(path.resolve(x64Dir, binding));
d(`merging binding: ${binding}`); d(`merging binding: ${binding}`);
execFileSync(LIPO, [source, destination, '-create', '-output', destination]); execFileSync(LIPO, [source, destination, '-create', '-output', destination]);
@@ -219,7 +225,10 @@ export const mergeASARs = async ({
d('done merging'); d('done merging');
} finally { } 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 }),
]);
} }
}; };

View File

@@ -1,16 +1,21 @@
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise'; import fs from 'node:fs';
import * as fs from 'fs-extra'; import path from 'node:path';
import * as path from 'path';
import { promises as stream } from 'node:stream'; import { promises as stream } from 'node:stream';
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
import { minimatch } from 'minimatch';
const MACHO_PREFIX = 'Mach-O '; const MACHO_PREFIX = 'Mach-O ';
const UNPACKED_ASAR_PATH = path.join('Contents', 'Resources', 'app.asar.unpacked');
export enum AppFileType { export enum AppFileType {
MACHO, MACHO,
PLAIN, PLAIN,
INFO_PLIST, INFO_PLIST,
SNAPSHOT, SNAPSHOT,
APP_CODE, APP_CODE,
SINGLE_ARCH,
} }
export type AppFile = { export type AppFile = {
@@ -18,22 +23,50 @@ export type AppFile = {
type: AppFileType; 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 * @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 files: AppFile[] = [];
const visited = new Set<string>(); const visited = new Set<string>();
const traverse = async (p: string) => { const traverse = async (p: string) => {
p = await fs.realpath(p); p = await fs.promises.realpath(p);
if (visited.has(p)) return; if (visited.has(p)) return;
visited.add(p); visited.add(p);
const info = await fs.stat(p); const info = await fs.promises.stat(p);
if (info.isSymbolicLink()) return; if (info.isSymbolicLink()) return;
if (info.isFile()) { if (info.isFile()) {
const relativePath = path.relative(appPath, p);
let fileType = AppFileType.PLAIN; let fileType = AppFileType.PLAIN;
var fileOutput = ''; var fileOutput = '';
@@ -48,6 +81,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
} }
if (p.endsWith('.asar')) { if (p.endsWith('.asar')) {
fileType = AppFileType.APP_CODE; fileType = AppFileType.APP_CODE;
} else if (isSingleArchFile(relativePath, opts)) {
fileType = AppFileType.SINGLE_ARCH;
} else if (fileOutput.startsWith(MACHO_PREFIX)) { } else if (fileOutput.startsWith(MACHO_PREFIX)) {
fileType = AppFileType.MACHO; fileType = AppFileType.MACHO;
} else if (p.endsWith('.bin')) { } else if (p.endsWith('.bin')) {
@@ -57,13 +92,13 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
} }
files.push({ files.push({
relativePath: path.relative(appPath, p), relativePath,
type: fileType, type: fileType,
}); });
} }
if (info.isDirectory()) { 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)); await traverse(path.resolve(p, child));
} }
} }
@@ -83,3 +118,21 @@ export const readMachOHeader = async (path: string) => {
}); });
return Buffer.concat(chunks); 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 fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import * as asar from '@electron/asar'; import * as asar from '@electron/asar';
import { spawn } from '@malept/cross-spawn-promise'; import { spawn } from '@malept/cross-spawn-promise';
import * as dircompare from 'dir-compare'; import * as dircompare from 'dir-compare';
import * as fs from 'fs-extra';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import * as os from 'os'; import plist from 'plist';
import * as path from 'path';
import * as plist from 'plist';
import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils'; import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils.js';
import { AppFile, AppFileType, getAllAppFiles, readMachOHeader } from './file-utils'; import { AppFile, AppFileType, fsMove, getAllAppFiles, readMachOHeader } from './file-utils.js';
import { sha } from './sha'; import { sha } from './sha.js';
import { d } from './debug'; import { d } from './debug.js';
import { computeIntegrityData } from './integrity'; import { computeIntegrityData } from './integrity.js';
/** /**
* Options to pass into the {@link makeUniversalApp} function. * Options to pass into the {@link makeUniversalApp} function.
@@ -74,7 +75,12 @@ export type MakeUniversalOpts = {
}; };
const dupedFiles = (files: AppFile[]) => 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> => { export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
d('making a universal app with options', opts); d('making a universal app with options', opts);
@@ -88,7 +94,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (!opts.outAppPath || !path.isAbsolute(opts.outAppPath)) if (!opts.outAppPath || !path.isAbsolute(opts.outAppPath))
throw new Error('Expected opts.outAppPath to be an absolute path but it was not'); 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'); d('output path exists already');
if (!opts.force) { if (!opts.force) {
throw new Error( throw new Error(
@@ -96,7 +102,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
); );
} else { } else {
d('overwriting existing application because force == true'); d('overwriting existing application because force == true');
await fs.remove(opts.outAppPath); await fs.promises.rm(opts.outAppPath, { recursive: true, force: true });
} }
} }
@@ -110,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)', '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); d('building universal app in', tmpDir);
try { try {
@@ -120,8 +126,8 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const uniqueToX64: string[] = []; const uniqueToX64: string[] = [];
const uniqueToArm64: string[] = []; const uniqueToArm64: string[] = [];
const x64Files = await getAllAppFiles(await fs.realpath(tmpApp)); const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp), opts);
const arm64Files = await getAllAppFiles(await fs.realpath(opts.arm64AppPath)); const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath), opts);
for (const file of dupedFiles(x64Files)) { for (const file of dupedFiles(x64Files)) {
if (!arm64Files.some((f) => f.relativePath === file.relativePath)) if (!arm64Files.some((f) => f.relativePath === file.relativePath))
@@ -142,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 x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath)); const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
if (x64Sha !== arm64Sha) { if (x64Sha !== arm64Sha) {
@@ -158,9 +166,11 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
} }
} }
const knownMergedMachOFiles = new Set(); const knownMergedMachOFiles = new Set();
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) { for (const machOFile of multiArchFiles.filter((f) => f.type === AppFileType.MACHO)) {
const first = await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)); const first = await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath));
const second = await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath)); const second = await fs.promises.realpath(
path.resolve(opts.arm64AppPath, machOFile.relativePath),
);
if ( if (
isUniversalMachO(await readMachOHeader(first)) && isUniversalMachO(await readMachOHeader(first)) &&
@@ -201,7 +211,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
second, second,
'-create', '-create',
'-output', '-output',
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)), await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath)),
]); ]);
knownMergedMachOFiles.add(machOFile.relativePath); knownMergedMachOFiles.add(machOFile.relativePath);
} }
@@ -232,26 +242,34 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (nonMergedDifferences.length > 0) { if (nonMergedDifferences.length > 0) {
d('x64 and arm64 app folders are different, creating dynamic entry ASAR'); 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'),
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64'),
); );
await fs.copy( await fs.promises.cp(
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'), path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'),
{ force: true, recursive: true, verbatimSymlinks: true },
); );
const entryAsar = path.resolve(tmpDir, 'entry-asar'); const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.mkdir(entryAsar); await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.copy( await fs.promises.cp(
path.resolve(__dirname, '..', '..', 'entry-asar', 'no-asar.js'), path.resolve(import.meta.dirname, '..', 'entry-asar', 'no-asar.js'),
path.resolve(entryAsar, 'index.js'), path.resolve(entryAsar, 'index.js'),
); );
let pj = await fs.readJson( let pj = JSON.parse(
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'), await fs.promises.readFile(
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
'utf8',
),
); );
pj.main = '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( await asar.createPackage(
entryAsar, entryAsar,
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
@@ -288,19 +306,20 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (x64AsarSha !== arm64AsarSha) { if (x64AsarSha !== arm64AsarSha) {
d('x64 and arm64 asars are different'); d('x64 and arm64 asars are different');
const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar'); 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'); const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked');
if (await fs.pathExists(x64Unpacked)) { if (fs.existsSync(x64Unpacked)) {
await fs.move( await fsMove(
x64Unpacked, x64Unpacked,
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'),
); );
} }
const arm64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar'); 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'), path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'),
arm64AsarPath, arm64AsarPath,
{ force: true, recursive: true, verbatimSymlinks: true },
); );
const arm64Unpacked = path.resolve( const arm64Unpacked = path.resolve(
opts.arm64AppPath, opts.arm64AppPath,
@@ -308,17 +327,18 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
'Resources', 'Resources',
'app.asar.unpacked', 'app.asar.unpacked',
); );
if (await fs.pathExists(arm64Unpacked)) { if (fs.existsSync(arm64Unpacked)) {
await fs.copy( await fs.promises.cp(
arm64Unpacked, arm64Unpacked,
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'),
{ force: true, recursive: true, verbatimSymlinks: true },
); );
} }
const entryAsar = path.resolve(tmpDir, 'entry-asar'); const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.mkdir(entryAsar); await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.copy( await fs.promises.cp(
path.resolve(__dirname, '..', '..', 'entry-asar', 'has-asar.js'), path.resolve(import.meta.dirname, '..', 'entry-asar', 'has-asar.js'),
path.resolve(entryAsar, 'index.js'), path.resolve(entryAsar, 'index.js'),
); );
let pj = JSON.parse( let pj = JSON.parse(
@@ -330,7 +350,11 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
).toString('utf8'), ).toString('utf8'),
); );
pj.main = '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'); const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar');
await asar.createPackage(entryAsar, asarPath); await asar.createPackage(entryAsar, asarPath);
} else { } else {
@@ -338,18 +362,18 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
} }
} }
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents')); const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'), opts);
const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST); const plistFiles = multiArchFiles.filter((f) => f.type === AppFileType.INFO_PLIST);
for (const plistFile of plistFiles) { for (const plistFile of plistFiles) {
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath); const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath); const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath);
const { ElectronAsarIntegrity: x64Integrity, ...x64Plist } = plist.parse( const { ElectronAsarIntegrity: x64Integrity, ...x64Plist } = plist.parse(
await fs.readFile(x64PlistPath, 'utf8'), await fs.promises.readFile(x64PlistPath, 'utf8'),
) as any; ) as any;
const { ElectronAsarIntegrity: arm64Integrity, ...arm64Plist } = plist.parse( const { ElectronAsarIntegrity: arm64Integrity, ...arm64Plist } = plist.parse(
await fs.readFile(arm64PlistPath, 'utf8'), await fs.promises.readFile(arm64PlistPath, 'utf8'),
) as any; ) as any;
if (JSON.stringify(x64Plist) !== JSON.stringify(arm64Plist)) { if (JSON.stringify(x64Plist) !== JSON.stringify(arm64Plist)) {
throw new Error( throw new Error(
@@ -364,23 +388,26 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
? { ...x64Plist, ElectronAsarIntegrity: generatedIntegrity } ? { ...x64Plist, ElectronAsarIntegrity: generatedIntegrity }
: { ...x64Plist }; : { ...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)) { for (const snapshotsFile of arm64Files.filter((f) => f.type === AppFileType.SNAPSHOT)) {
d('copying snapshot file', snapshotsFile.relativePath, 'to target application'); d('copying snapshot file', snapshotsFile.relativePath, 'to target application');
await fs.copy( await fs.promises.cp(
path.resolve(opts.arm64AppPath, snapshotsFile.relativePath), path.resolve(opts.arm64AppPath, snapshotsFile.relativePath),
path.resolve(tmpApp, snapshotsFile.relativePath), path.resolve(tmpApp, snapshotsFile.relativePath),
); );
} }
d('moving final universal app to target destination'); 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]); await spawn('mv', [tmpApp, opts.outAppPath]);
} catch (err) { } catch (err) {
throw err; throw err;
} finally { } 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 fs from 'node:fs';
import path from 'path'; import path from 'node:path';
import { AppFileType, getAllAppFiles } from './file-utils';
import { sha } from './sha'; import { AppFileType, getAllAppFiles } from './file-utils.js';
import { generateAsarIntegrity } from './asar-utils'; import { generateAsarIntegrity } from './asar-utils.js';
type IntegrityMap = { type IntegrityMap = {
[filepath: string]: string; [filepath: string]: string;
@@ -17,13 +17,20 @@ export interface AsarIntegrity {
[key: string]: HeaderHash; [key: string]: HeaderHash;
} }
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> { export type ComputeIntegrityDataOpts = {
const root = await fs.realpath(contentsPath); singleArchFiles?: string;
};
export async function computeIntegrityData(
contentsPath: string,
opts: ComputeIntegrityDataOpts,
): Promise<AsarIntegrity> {
const root = await fs.promises.realpath(contentsPath);
const resourcesRelativePath = 'Resources'; const resourcesRelativePath = 'Resources';
const resourcesPath = path.resolve(root, resourcesRelativePath); const resourcesPath = path.resolve(root, resourcesRelativePath);
const resources = await getAllAppFiles(resourcesPath); const resources = await getAllAppFiles(resourcesPath, opts);
const resourceAsars = resources const resourceAsars = resources
.filter((file) => file.type === AppFileType.APP_CODE) .filter((file) => file.type === AppFileType.APP_CODE)
.reduce<IntegrityMap>( .reduce<IntegrityMap>(

View File

@@ -1,8 +1,8 @@
import * as fs from 'fs-extra'; import fs from 'node:fs';
import * as crypto from 'crypto'; import crypto from 'node:crypto';
import { pipeline } from 'stream/promises'; import { pipeline } from 'node:stream/promises';
import { d } from './debug'; import { d } from './debug.js';
export const sha = async (filePath: string) => { export const sha = async (filePath: string) => {
d('hashing', filePath); 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": { "files": {
"index.js": { "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": { "Contents/Info.plist": {
"Resources/app.asar": { "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": { "files": {
"extra-file.txt": { "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": { "files": {
"index.js": { "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": { "files": {
"index.js": { "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": { "Contents/Info.plist": {
"Resources/app-arm64.asar": { "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": { "files": {
"index.js": { "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": { "files": {
"index.js": { "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": { "Contents/Info.plist": {
"Resources/app.asar": { "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": { "files": {
"extra-file.txt": { "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": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -339,7 +339,87 @@ 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 merge two different asars with native files when \`mergeASARs\` is enabled 1`] = `
{
"files": {
"hello-world-arm64": "<stripped>",
"hello-world-x64": "<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 > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 2`] = `[]`;
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 3`] = `
[
"hello-world-arm64",
"hello-world-x64",
]
`;
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 4`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "<stripped>",
},
},
}
`;
exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -397,190 +477,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/Info.plist": undefined,
"Contents/Resources/SubApp-1.app/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`] = ` exports[`makeUniversalApp > force > packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 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`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -609,7 +513,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": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -620,7 +524,7 @@ exports[`makeUniversalApp force packages successfully if \`out\` bundle already
} }
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 1`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -649,13 +553,13 @@ exports[`makeUniversalApp no asar mode different app dirs with different macho f
} }
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 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", "private/var/i-aint-got-no-rhythm.bin",
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 3`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 3`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -674,7 +578,7 @@ exports[`makeUniversalApp no asar mode different app dirs with different macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 4`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 4`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -693,7 +597,7 @@ exports[`makeUniversalApp no asar mode different app dirs with different macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 5`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 5`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -704,7 +608,7 @@ exports[`makeUniversalApp no asar mode different app dirs with different macho f
} }
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 1`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -733,13 +637,13 @@ exports[`makeUniversalApp no asar mode different app dirs with universal macho f
} }
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 2`] = ` 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", "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`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 3`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -758,7 +662,7 @@ exports[`makeUniversalApp no asar mode different app dirs with universal macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 4`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 4`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -777,7 +681,7 @@ exports[`makeUniversalApp no asar mode different app dirs with universal macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 5`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 5`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -788,7 +692,7 @@ exports[`makeUniversalApp no asar mode different app dirs with universal macho f
} }
`; `;
exports[`makeUniversalApp no asar mode identical app dirs with different macho files (e.g. do not shim, but still lipo) 1`] = ` exports[`makeUniversalApp > no asar mode > identical app dirs with different macho files (e.g. do not shim, but still lipo) 1`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -806,13 +710,13 @@ exports[`makeUniversalApp no asar mode identical app dirs with different macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode identical app dirs with different macho files (e.g. do not shim, but still lipo) 2`] = ` exports[`makeUniversalApp > no asar mode > identical app dirs with different macho files (e.g. do not shim, but still lipo) 2`] = `
{ {
"Contents/Info.plist": {}, "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`] = ` exports[`makeUniversalApp > no asar mode > identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 1`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -830,13 +734,13 @@ exports[`makeUniversalApp no asar mode identical app dirs with universal macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 2`] = ` 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": {}, "Contents/Info.plist": {},
} }
`; `;
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 1`] = ` exports[`makeUniversalApp > no asar mode > should correctly merge two identical app folders 1`] = `
[ [
"index.js", "index.js",
{ {
@@ -849,13 +753,13 @@ exports[`makeUniversalApp no asar mode should correctly merge two identical app
] ]
`; `;
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 2`] = ` exports[`makeUniversalApp > no asar mode > should correctly merge two identical app folders 2`] = `
{ {
"Contents/Info.plist": {}, "Contents/Info.plist": {},
} }
`; `;
exports[`makeUniversalApp no asar mode should shim two different app folders 1`] = ` exports[`makeUniversalApp > no asar mode > should shim two different app folders 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -884,13 +788,13 @@ 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 > should shim two different app folders 2`] = `
[ [
"private/var/i-aint-got-no-rhythm.bin", "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 > should shim two different app folders 3`] = `
[ [
"index.js", "index.js",
{ {
@@ -908,7 +812,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", "index.js",
{ {
@@ -926,7 +830,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": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -937,7 +841,7 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 5`]
} }
`; `;
exports[`makeUniversalApp works for lipo binary resources 1`] = ` exports[`makeUniversalApp > works for lipo binary resources 1`] = `
{ {
"files": { "files": {
"hello-world": "<stripped>", "hello-world": "<stripped>",
@@ -996,15 +900,15 @@ exports[`makeUniversalApp works for lipo binary resources 1`] = `
} }
`; `;
exports[`makeUniversalApp works for lipo binary resources 2`] = `[]`; exports[`makeUniversalApp > works for lipo binary resources 2`] = `[]`;
exports[`makeUniversalApp works for lipo binary resources 3`] = ` exports[`makeUniversalApp > works for lipo binary resources 3`] = `
[ [
"hello-world", "hello-world",
] ]
`; `;
exports[`makeUniversalApp works for lipo binary resources 4`] = ` exports[`makeUniversalApp > works for lipo binary resources 4`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {

View File

@@ -1,10 +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';
import { describe, expect, it } from '@jest/globals';
const asarsPath = path.resolve(__dirname, 'fixtures', 'asars'); import { AsarMode, detectAsarMode, generateAsarIntegrity } from '../src/asar-utils.js';
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
const asarsPath = path.resolve(import.meta.dirname, 'fixtures', 'asars');
const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
describe('asar-utils', () => { describe('asar-utils', () => {
describe('detectAsarMode', () => { describe('detectAsarMode', () => {

View File

@@ -1,9 +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';
import { beforeAll, describe, expect, it } from '@jest/globals';
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('file-utils', () => {
describe('getAllAppFiles', () => { describe('getAllAppFiles', () => {
@@ -11,8 +12,8 @@ describe('file-utils', () => {
let noAsarFiles: AppFile[]; let noAsarFiles: AppFile[];
beforeAll(async () => { beforeAll(async () => {
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app')); asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'), {});
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app')); noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'), {});
}); });
it('should correctly identify plist files', async () => { it('should correctly identify plist files', async () => {

View File

@@ -1,7 +1,8 @@
import { execFileSync } from 'child_process'; import { execFileSync } from 'node:child_process';
import * as fs from 'fs-extra'; import fs from 'node:fs';
import * as path from 'path'; import path from 'node:path';
import { appsDir, asarsDir, fixtureDir, templateApp } from './test/util';
import { appsDir, asarsDir, fixtureDir, templateApp } from './util.js';
// generates binaries from hello-world.c // generates binaries from hello-world.c
// hello-world-universal, hello-world-x86_64, hello-world-arm64 // hello-world-universal, hello-world-x86_64, hello-world-arm64
@@ -23,53 +24,59 @@ const generateMachO = () => {
}; };
export default async () => { export default async () => {
await fs.remove(appsDir); await fs.promises.rm(appsDir, { recursive: true, force: true });
await fs.mkdirp(appsDir); await fs.promises.mkdir(appsDir, { recursive: true });
// generate mach-o binaries to be leveraged in lipo tests // generate mach-o binaries to be leveraged in lipo tests
generateMachO(); generateMachO();
await templateApp('Arm64Asar.app', 'arm64', async (appPath) => { await templateApp('Arm64Asar.app', 'arm64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app.asar'), path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
// contains `extra-file.txt` // contains `extra-file.txt`
await templateApp('Arm64AsarExtraFile.app', 'arm64', async (appPath) => { await templateApp('Arm64AsarExtraFile.app', 'arm64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app2.asar'), path.resolve(asarsDir, 'app2.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
await templateApp('X64Asar.app', 'x64', async (appPath) => { await templateApp('X64Asar.app', 'x64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app.asar'), path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
await templateApp('Arm64NoAsar.app', 'arm64', async (appPath) => { await templateApp('Arm64NoAsar.app', 'arm64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app'), path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'), path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
// contains `extra-file.txt` // contains `extra-file.txt`
await templateApp('Arm64NoAsarExtraFile.app', 'arm64', async (appPath) => { await templateApp('Arm64NoAsarExtraFile.app', 'arm64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app2'), path.resolve(asarsDir, 'app2'),
path.resolve(appPath, 'Contents', 'Resources', 'app'), path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
await templateApp('X64NoAsar.app', 'x64', async (appPath) => { await templateApp('X64NoAsar.app', 'x64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app'), path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'), path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
}; };

View File

@@ -1,24 +1,27 @@
import * as fs from 'fs-extra'; import fs from 'node:fs';
import * as path from 'path'; import path from 'node:path';
import { makeUniversalApp } from '../dist/cjs/index'; import { afterEach, describe, expect, it } from 'vitest';
import { makeUniversalApp } from '../dist/index.js';
import { fsMove } from '../src/file-utils.js';
import { import {
createStagingAppDir, createStagingAppDir,
generateNativeApp, generateNativeApp,
templateApp, templateApp,
VERIFY_APP_TIMEOUT, VERIFY_APP_TIMEOUT,
verifyApp, verifyApp,
} from './util'; } from './util.js';
import { createPackage, createPackageWithOptions } from '@electron/asar'; import { createPackage, createPackageWithOptions } from '@electron/asar';
import { afterEach, describe, expect, it } from '@jest/globals';
const appsPath = path.resolve(__dirname, 'fixtures', 'apps'); const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
const appsOutPath = path.resolve(__dirname, 'fixtures', 'apps', 'out'); 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', () => { describe('makeUniversalApp', () => {
afterEach(async () => { 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 () => { it('throws an error if asar is only detected in one arch', async () => {
@@ -34,31 +37,27 @@ describe('makeUniversalApp', () => {
); );
}); });
it( it('works for lipo binary resources', { timeout: VERIFY_APP_TIMEOUT }, async () => {
'works for lipo binary resources', const x64AppPath = await generateNativeApp({
async () => { appNameWithExtension: 'LipoX64.app',
const x64AppPath = await generateNativeApp({ arch: 'x64',
appNameWithExtension: 'LipoX64.app', createAsar: true,
arch: 'x64', });
createAsar: true, const arm64AppPath = await generateNativeApp({
}); appNameWithExtension: 'LipoArm64.app',
const arm64AppPath = await generateNativeApp({ arch: 'arm64',
appNameWithExtension: 'LipoArm64.app', createAsar: true,
arch: 'arm64', });
createAsar: true,
});
const out = path.resolve(appsOutPath, 'Lipo.app'); const out = path.resolve(appsOutPath, 'Lipo.app');
await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out, mergeASARs: true }); await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out, mergeASARs: true });
await verifyApp(out, true); await verifyApp(out, true);
}, });
VERIFY_APP_TIMEOUT,
);
describe('force', () => { describe('force', () => {
it('throws an error if `out` bundle already exists and `force` is `false`', async () => { it('throws an error if `out` bundle already exists and `force` is `false`', async () => {
const out = path.resolve(appsOutPath, 'Error.app'); const out = path.resolve(appsOutPath, 'Error.app');
await fs.mkdirp(out); await fs.promises.mkdir(out, { recursive: true });
await expect( await expect(
makeUniversalApp({ makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'), x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
@@ -70,9 +69,10 @@ describe('makeUniversalApp', () => {
it( it(
'packages successfully if `out` bundle already exists and `force` is `true`', 'packages successfully if `out` bundle already exists and `force` is `true`',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'NoError.app'); const out = path.resolve(appsOutPath, 'NoError.app');
await fs.mkdirp(out); await fs.promises.mkdir(out, { recursive: true });
await makeUniversalApp({ await makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'), x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'), arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
@@ -81,27 +81,23 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out); await verifyApp(out);
}, },
VERIFY_APP_TIMEOUT,
); );
}); });
describe('asar mode', () => { describe('asar mode', () => {
it( it('should correctly merge two identical asars', { timeout: VERIFY_APP_TIMEOUT }, async () => {
'should correctly merge two identical asars', const out = path.resolve(appsOutPath, 'MergedAsar.app');
async () => { await makeUniversalApp({
const out = path.resolve(appsOutPath, 'MergedAsar.app'); x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
await makeUniversalApp({ arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
x64AppPath: path.resolve(appsPath, 'X64Asar.app'), outAppPath: out,
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'), });
outAppPath: out, await verifyApp(out);
}); });
await verifyApp(out);
},
VERIFY_APP_TIMEOUT,
);
it( it(
'should create a shim if asars are different between architectures', 'should create a shim if asars are different between architectures',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'ShimmedAsar.app'); const out = path.resolve(appsOutPath, 'ShimmedAsar.app');
await makeUniversalApp({ await makeUniversalApp({
@@ -111,11 +107,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out); await verifyApp(out);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'should merge two different asars when `mergeASARs` is enabled', 'should merge two different asars when `mergeASARs` is enabled',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'MergedAsar.app'); const out = path.resolve(appsOutPath, 'MergedAsar.app');
await makeUniversalApp({ await makeUniversalApp({
@@ -127,11 +123,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out); await verifyApp(out);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique file', 'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique file',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'Error.app'); const out = path.resolve(appsOutPath, 'Error.app');
await expect( await expect(
@@ -144,17 +140,76 @@ describe('makeUniversalApp', () => {
}), }),
).rejects.toThrow(/Detected unique file "extra-file\.txt"/); ).rejects.toThrow(/Detected unique file "extra-file\.txt"/);
}, },
VERIFY_APP_TIMEOUT, );
it(
'should merge two different asars with native files when `mergeASARs` is enabled',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'SingleArchFiles-x64.app',
arch: 'x64',
createAsar: true,
singleArchBindings: true,
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'SingleArchFiles-arm64.app',
arch: 'arm64',
createAsar: true,
singleArchBindings: true,
});
const out = path.resolve(appsOutPath, 'SingleArchFiles.app');
await makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath: out,
mergeASARs: true,
singleArchFiles: 'hello-world-*',
});
await verifyApp(out, true);
},
);
it(
'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique native file',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'SingleArchFiles-2-x64.app',
arch: 'x64',
createAsar: true,
singleArchBindings: true,
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'SingleArchFiles-2-arm64.app',
arch: 'arm64',
createAsar: true,
singleArchBindings: true,
});
const out = path.resolve(appsOutPath, 'SingleArchFiles-2.app');
await expect(
makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath: out,
mergeASARs: true,
singleArchFiles: 'bad-rule',
}),
).rejects.toThrow(
/the number of mach-o files is not the same between the arm64 and x64 builds/,
);
},
); );
it( it(
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`', 'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const arm64AppPath = await templateApp('Arm64-1.app', 'arm64', async (appPath) => { const arm64AppPath = await templateApp('Arm64-1.app', 'arm64', async (appPath) => {
const { testPath } = await createStagingAppDir('Arm64-1'); const { testPath } = await createStagingAppDir('Arm64-1');
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar')); await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
await templateApp('SubApp-1.app', 'arm64', async (subArm64AppPath) => { await templateApp('SubApp-1.app', 'arm64', async (subArm64AppPath) => {
await fs.move( await fsMove(
subArm64AppPath, subArm64AppPath,
path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)), path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
); );
@@ -164,7 +219,7 @@ describe('makeUniversalApp', () => {
const { testPath } = await createStagingAppDir('X64-1'); const { testPath } = await createStagingAppDir('X64-1');
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar')); await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
await templateApp('SubApp-1.app', 'x64', async (subArm64AppPath) => { await templateApp('SubApp-1.app', 'x64', async (subArm64AppPath) => {
await fs.move( await fsMove(
subArm64AppPath, subArm64AppPath,
path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)), path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
); );
@@ -180,7 +235,6 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath); await verifyApp(outAppPath);
}, },
VERIFY_APP_TIMEOUT,
); );
// TODO: Investigate if this should even be allowed. // TODO: Investigate if this should even be allowed.
@@ -188,6 +242,7 @@ describe('makeUniversalApp', () => {
// https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49 // https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49
it.skip( it.skip(
'should shim asars with different unpacked dirs', 'should shim asars with different unpacked dirs',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => { const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
const { testPath } = await createStagingAppDir('UnpackedAppArm64'); const { testPath } = await createStagingAppDir('UnpackedAppArm64');
@@ -218,32 +273,32 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath); await verifyApp(outAppPath);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'should generate AsarIntegrity for all asars in the application', 'should generate AsarIntegrity for all asars in the application',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const { testPath } = await createStagingAppDir('app-2'); const { testPath } = await createStagingAppDir('app-2');
const testAsarPath = path.resolve(appsOutPath, 'app-2.asar'); const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
await createPackage(testPath, testAsarPath); await createPackage(testPath, testAsarPath);
const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => { const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => {
await fs.copyFile( await fs.promises.copyFile(
testAsarPath, testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
); );
await fs.copyFile( await fs.promises.copyFile(
testAsarPath, testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'), path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'),
); );
}); });
const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => { const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => {
await fs.copyFile( await fs.promises.copyFile(
testAsarPath, testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
); );
await fs.copyFile( await fs.promises.copyFile(
testAsarPath, testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'), path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'),
); );
@@ -257,13 +312,13 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath); await verifyApp(outAppPath);
}, },
VERIFY_APP_TIMEOUT,
); );
}); });
describe('no asar mode', () => { describe('no asar mode', () => {
it( it(
'should correctly merge two identical app folders', 'should correctly merge two identical app folders',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'MergedNoAsar.app'); const out = path.resolve(appsOutPath, 'MergedNoAsar.app');
await makeUniversalApp({ await makeUniversalApp({
@@ -273,39 +328,41 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out); await verifyApp(out);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it('should shim two different app folders', { timeout: VERIFY_APP_TIMEOUT }, async () => {
'should shim two different app folders', const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => {
async () => { const { testPath } = await createStagingAppDir('shimArm64', {
const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => { 'i-aint-got-no-rhythm.bin': 'boomshakalaka',
const { testPath } = await createStagingAppDir('shimArm64', {
'i-aint-got-no-rhythm.bin': 'boomshakalaka',
});
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'));
}); });
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 x64AppPath = await templateApp('ShimX64.app', 'x64', async (appPath) => {
const { testPath } = await createStagingAppDir('shimX64', { const { testPath } = await createStagingAppDir('shimX64', {
'hello-world.bin': 'Hello World', 'hello-world.bin': 'Hello World',
});
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'));
}); });
await fs.promises.cp(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'), {
recursive: true,
verbatimSymlinks: true,
});
});
const outAppPath = path.resolve(appsOutPath, 'ShimNoAsar.app'); const outAppPath = path.resolve(appsOutPath, 'ShimNoAsar.app');
await makeUniversalApp({ await makeUniversalApp({
x64AppPath, x64AppPath,
arm64AppPath, arm64AppPath,
outAppPath, outAppPath,
}); });
await verifyApp(outAppPath); await verifyApp(outAppPath);
}, });
VERIFY_APP_TIMEOUT,
);
it( it(
'different app dirs with different macho files (shim and lipo)', 'different app dirs with different macho files (shim and lipo)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentMachoAppX64-1.app', appNameWithExtension: 'DifferentMachoAppX64-1.app',
@@ -332,11 +389,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath, true); await verifyApp(outAppPath, true);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
"different app dirs with universal macho files (shim but don't lipo)", "different app dirs with universal macho files (shim but don't lipo)",
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentButUniversalMachoAppX64-2.app', appNameWithExtension: 'DifferentButUniversalMachoAppX64-2.app',
@@ -365,11 +422,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath, true); await verifyApp(outAppPath, true);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'identical app dirs with different macho files (e.g. do not shim, but still lipo)', 'identical app dirs with different macho files (e.g. do not shim, but still lipo)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentMachoAppX64-2.app', appNameWithExtension: 'DifferentMachoAppX64-2.app',
@@ -390,11 +447,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out, true); await verifyApp(out, true);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir)', 'identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'UniversalMachoAppX64.app', appNameWithExtension: 'UniversalMachoAppX64.app',
@@ -413,7 +470,6 @@ describe('makeUniversalApp', () => {
await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out }); await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out });
await verifyApp(out, true); await verifyApp(out, true);
}, },
VERIFY_APP_TIMEOUT,
); );
}); });
}); });

View File

@@ -1,11 +1,12 @@
import * as path from 'path'; import path from 'node:path';
import { sha } from '../src/sha'; import { describe, expect, it } from 'vitest';
import { describe, expect, it } from '@jest/globals';
import { sha } from '../src/sha.js';
describe('sha', () => { describe('sha', () => {
it('should correctly hash a file', async () => { 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', '12998c017066eb0d2a70b94e6ed3192985855ce390f321bbdb832022888bd251',
); );
}); });

View File

@@ -1,29 +1,31 @@
import fs from 'node:fs';
import path from 'node:path';
import { createPackageWithOptions, getRawHeader } from '@electron/asar';
import { downloadArtifact } from '@electron/get'; import { downloadArtifact } from '@electron/get';
import { spawn } from '@malept/cross-spawn-promise'; import { spawn } from '@malept/cross-spawn-promise';
import * as zip from 'cross-zip'; import * as zip from 'cross-zip';
import * as fs from 'fs-extra';
import * as path from 'path';
import plist from 'plist'; import plist from 'plist';
import * as fileUtils from '../dist/cjs/file-utils';
import { createPackageWithOptions, getRawHeader } from '@electron/asar';
declare const expect: typeof import('@jest/globals').expect; import * as fileUtils from '../dist/file-utils.js';
// We do a LOT of verifications in `verifyApp` 😅 // We do a LOT of verifications in `verifyApp` 😅
// exec universal binary -> verify ALL asars -> verify ALL app dirs -> verify ALL asar integrity entries // exec universal binary -> verify ALL asars -> verify ALL app dirs -> verify ALL asar integrity entries
// plus some tests create fixtures at runtime // plus some tests create fixtures at runtime
export const VERIFY_APP_TIMEOUT = 80 * 1000; export const VERIFY_APP_TIMEOUT = 80 * 1000;
export const fixtureDir = path.resolve(__dirname, 'fixtures'); export const fixtureDir = path.resolve(import.meta.dirname, 'fixtures');
export const asarsDir = path.resolve(fixtureDir, 'asars'); export const asarsDir = path.resolve(fixtureDir, 'asars');
export const appsDir = path.resolve(fixtureDir, 'apps'); export const appsDir = path.resolve(fixtureDir, 'apps');
export const appsOutPath = path.resolve(appsDir, 'out'); export const appsOutPath = path.resolve(appsDir, 'out');
export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho = false) => { export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho = false) => {
const { expect } = await import('vitest');
await ensureUniversal(appPath); await ensureUniversal(appPath);
const resourcesDir = path.resolve(appPath, 'Contents', 'Resources'); const resourcesDir = path.resolve(appPath, 'Contents', 'Resources');
const resourcesDirContents = await fs.readdir(resourcesDir); const resourcesDirContents = await fs.promises.readdir(resourcesDir);
// sort for consistent result // sort for consistent result
const asars = resourcesDirContents.filter((p) => p.endsWith('.asar')).sort(); const asars = resourcesDirContents.filter((p) => p.endsWith('.asar')).sort();
@@ -31,7 +33,12 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
// verify header // verify header
const asarFs = getRawHeader(path.resolve(resourcesDir, asar)); const asarFs = getRawHeader(path.resolve(resourcesDir, asar));
expect( expect(
removeUnstableProperties(asarFs.header, containsRuntimeGeneratedMacho ? ['hello-world'] : []), removeUnstableProperties(
asarFs.header,
containsRuntimeGeneratedMacho
? ['hello-world', 'hello-world-arm64', 'hello-world-x64']
: [],
),
).toMatchSnapshot(); ).toMatchSnapshot();
} }
@@ -51,7 +58,7 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
await verifyFileTree(path.resolve(resourcesDir, dir)); await verifyFileTree(path.resolve(resourcesDir, dir));
} }
const allFiles = await fileUtils.getAllAppFiles(appPath); const allFiles = await fileUtils.getAllAppFiles(appPath, {});
const infoPlists = allFiles const infoPlists = allFiles
.filter( .filter(
(appFile) => (appFile) =>
@@ -79,13 +86,15 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
const extractAsarIntegrity = async (infoPlist: string) => { const extractAsarIntegrity = async (infoPlist: string) => {
const { ElectronAsarIntegrity: integrity, ...otherData } = plist.parse( const { ElectronAsarIntegrity: integrity, ...otherData } = plist.parse(
await fs.readFile(infoPlist, 'utf-8'), await fs.promises.readFile(infoPlist, 'utf-8'),
) as any; ) as any;
return integrity; return integrity;
}; };
export const verifyFileTree = async (dirPath: string) => { export const verifyFileTree = async (dirPath: string) => {
const dirFiles = await fileUtils.getAllAppFiles(dirPath); const { expect } = await import('vitest');
const dirFiles = await fileUtils.getAllAppFiles(dirPath, {});
const files = dirFiles.map((file) => { const files = dirFiles.map((file) => {
const it = path.join(dirPath, file.relativePath); const it = path.join(dirPath, file.relativePath);
const name = toSystemIndependentPath(file.relativePath); const name = toSystemIndependentPath(file.relativePath);
@@ -98,6 +107,8 @@ export const verifyFileTree = async (dirPath: string) => {
}; };
export const ensureUniversal = async (app: string) => { export const ensureUniversal = async (app: string) => {
const { expect } = await import('vitest');
const exe = path.resolve(app, 'Contents', 'MacOS', 'Electron'); const exe = path.resolve(app, 'Contents', 'MacOS', 'Electron');
const result = await spawn(exe); const result = await spawn(exe);
expect(result).toContain('arm64'); expect(result).toContain('arm64');
@@ -162,15 +173,18 @@ export const createStagingAppDir = async (
) => { ) => {
const outDir = (testName || 'app') + Math.floor(Math.random() * 100); // tests run in parallel, randomize dir suffix to prevent naming collisions 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); 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 privateVarPath = path.join(testPath, 'private', 'var');
const varPath = path.join(testPath, 'var'); const varPath = path.join(testPath, 'var');
await fs.mkdir(privateVarPath, { recursive: true }); await fs.promises.mkdir(privateVarPath, { recursive: true });
await fs.symlink(path.relative(testPath, privateVarPath), varPath); await fs.promises.symlink(path.relative(testPath, privateVarPath), varPath);
const files = { const files = {
'file.txt': 'hello world', 'file.txt': 'hello world',
@@ -178,11 +192,11 @@ export const createStagingAppDir = async (
}; };
for await (const [filename, fileData] of Object.entries(files)) { for await (const [filename, fileData] of Object.entries(files)) {
const originFilePath = path.join(varPath, filename); const originFilePath = path.join(varPath, filename);
await fs.writeFile(originFilePath, fileData); await fs.promises.writeFile(originFilePath, fileData);
} }
const appPath = path.join(varPath, 'app'); const appPath = path.join(varPath, 'app');
await fs.mkdirp(appPath); await fs.promises.mkdir(appPath, { recursive: true });
await fs.symlink('../file.txt', path.join(appPath, 'file.txt')); await fs.promises.symlink('../file.txt', path.join(appPath, 'file.txt'));
return { return {
testPath, testPath,
@@ -204,8 +218,11 @@ export const templateApp = async (
}); });
const appPath = path.resolve(appsDir, name); const appPath = path.resolve(appsDir, name);
zip.unzipSync(electronZip, appsDir); zip.unzipSync(electronZip, appsDir);
await fs.rename(path.resolve(appsDir, 'Electron.app'), appPath); await fs.promises.rename(path.resolve(appsDir, 'Electron.app'), appPath);
await fs.remove(path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar')); await fs.promises.rm(path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar'), {
recursive: true,
force: true,
});
await modify(appPath); await modify(appPath);
return appPath; return appPath;
@@ -217,6 +234,7 @@ export const generateNativeApp = async (options: {
createAsar: boolean; createAsar: boolean;
nativeModuleArch?: string; nativeModuleArch?: string;
additionalFiles?: Record<string, string>; additionalFiles?: Record<string, string>;
singleArchBindings?: boolean;
}) => { }) => {
const { const {
appNameWithExtension, appNameWithExtension,
@@ -224,27 +242,34 @@ export const generateNativeApp = async (options: {
createAsar, createAsar,
nativeModuleArch = arch, nativeModuleArch = arch,
additionalFiles, additionalFiles,
singleArchBindings,
} = options; } = options;
const appPath = await templateApp(appNameWithExtension, arch, async (appPath) => { const appPath = await templateApp(appNameWithExtension, arch, async (appPath) => {
const resources = path.join(appPath, 'Contents', 'Resources'); const resources = path.join(appPath, 'Contents', 'Resources');
const resourcesApp = path.resolve(resources, 'app'); const resourcesApp = path.resolve(resources, 'app');
if (!fs.existsSync(resourcesApp)) { if (!fs.existsSync(resourcesApp)) {
await fs.mkdir(resourcesApp); await fs.promises.mkdir(resourcesApp, { recursive: true });
} }
const { testPath } = await createStagingAppDir( const { testPath } = await createStagingAppDir(
path.basename(appNameWithExtension, '.app'), path.basename(appNameWithExtension, '.app'),
additionalFiles, additionalFiles,
); );
await fs.copy( let targetBinding: string;
path.join(appsDir, `hello-world-${nativeModuleArch}`), if (singleArchBindings) {
path.join(testPath, 'hello-world'), targetBinding = path.join(testPath, `hello-world-${nativeModuleArch}`);
); } else {
targetBinding = path.join(testPath, 'hello-world');
}
await fs.promises.cp(path.join(appsDir, `hello-world-${nativeModuleArch}`), targetBinding, {
recursive: true,
verbatimSymlinks: true,
});
if (createAsar) { if (createAsar) {
await createPackageWithOptions(testPath, path.resolve(resources, 'app.asar'), { await createPackageWithOptions(testPath, path.resolve(resources, 'app.asar'), {
unpack: '**/hello-world', unpack: '**/hello-world*',
}); });
} else { } else {
await fs.copy(testPath, resourcesApp); await fs.promises.cp(testPath, resourcesApp, { recursive: true, verbatimSymlinks: true });
} }
}); });
return appPath; return appPath;

View File

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

View File

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

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',
},
});

6013
yarn.lock

File diff suppressed because it is too large Load Diff