23 Commits

Author SHA1 Message Date
75309d0945 fix build
Some checks failed
Publish documentation / docs (push) Failing after 2s
Check Semantic Commit / Validate PR Title (pull_request) Failing after 2s
Test / Test (22.12.x) (pull_request) Has been cancelled
2025-12-12 17:11:16 +01:00
6f0968cdce Merge branch 'main' into esm-asar-entrypoints 2025-12-12 14:42:02 +01:00
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
Erick Zhao
b3059564b7 Merge branch 'main' into esm-asar-entrypoints 2024-06-21 16:12:39 -07:00
Erick Zhao
f7d15b8d34 Merge remote-tracking branch 'origin' into esm-asar-entrypoints 2024-06-17 15:18:33 -07:00
Erick Zhao
1c55526cdb Update package.json
Co-authored-by: Erik Moura <erikian@erikian.dev>
2024-06-16 21:04:57 -07:00
Erick Zhao
e9a5812213 add no-asar 2024-06-12 20:50:35 -07:00
Erick Zhao
ed1efe60a0 beep boop 2024-06-12 20:34:54 -07:00
26 changed files with 4275 additions and 1745 deletions

View File

@@ -14,12 +14,13 @@ jobs:
runs-on: ubuntu-latest
environment: docs-publish
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- 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 --frozen-lockfile
run: yarn --immutable
- name: Build API documentation
run: yarn build:docs
- name: Azure login
@@ -29,7 +30,7 @@ jobs:
tenant-id: ${{ secrets.AZURE_OIDC_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_OIDC_SUBSCRIPTION_ID }}
- name: Upload to Azure Blob Storage
uses: azure/cli@089eac9d8cc39f5d003e94f8b65efc51076c9cbd # v2.1.0
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,24 +13,25 @@ jobs:
name: Release
runs-on: ubuntu-latest
needs: test
environment: npm
environment: npm-trusted-publisher
permissions:
id-token: write # for CFA and npm provenance
id-token: write # for publishing releases
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install
run: yarn install --frozen-lockfile
- uses: continuousauth/action@4e8a2573eeb706f6d7300d6a9f3ca6322740b72d # v1.0.5
timeout-minutes: 60
run: yarn install --immutable
- 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:
project-id: ${{ secrets.CFA_PROJECT_ID }}
secret: ${{ secrets.CFA_SECRET }}
npm-token: ${{ secrets.NPM_TOKEN }}
github-token: ${{ fromJSON(steps.secret-service.outputs.secrets).GITHUB_TOKEN }}

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -22,14 +22,14 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: "${{ matrix.node-version }}"
cache: 'yarn'
- name: Install
run: yarn install --frozen-lockfile
run: yarn install --immutable
- name: Build
run: yarn build
- name: Lint

9
.gitignore vendored
View File

@@ -1,9 +1,12 @@
node_modules
dist
entry-asar/*.js*
entry-asar/*.ts
entry-asar/cjs/*.js*
entry-asar/cjs/*.d.ts
entry-asar/esm/*.?js*
entry-asar/esm/*.d.?ts
*.app
test/fixtures/apps
coverage
docs
.vscode
.vscode
.yarn/install-state.gz

5
.npmignore Normal file
View File

@@ -0,0 +1,5 @@
# npmignore overrides .gitignore for yarn pack
# Only exclude source files, not built files
entry-asar/**/*.mts
entry-asar/**/*.ts
entry-asar/**/tsconfig.json

View File

@@ -2,7 +2,7 @@
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@continuous-auth/semantic-release-npm",
"@semantic-release/npm",
"@semantic-release/github"
],
"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)
[![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
@@ -22,6 +23,8 @@ await makeUniversalApp({
});
```
For full API usage, see the [API documentation](https://packages.electronjs.org/universal).
## Advanced 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.
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

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": ".",
},
"include": [
".",
"../ambient.d.ts"
],
"exclude": []
}

View File

@@ -0,0 +1,28 @@
import { app } from 'electron';
import { createRequire } from 'node:module';
import path from 'node:path';
if (process.arch === 'arm64') {
await setPaths('arm64');
} else {
await setPaths('x64');
}
async function setPaths(platform: string) {
// This should return the full path, ending in something like
// Notion.app/Contents/Resources/app.asar
const appPath = app.getAppPath();
const asarFile = `app-${platform}.asar`;
// Maybe we'll handle this in Electron one day
if (path.basename(appPath) === 'app.asar') {
const platformAppPath = path.join(path.dirname(appPath), asarFile);
// This is an undocumented API. It exists.
app.setAppPath(platformAppPath);
}
const require = createRequire(import.meta.url);
process._archPath = require.resolve(`../${asarFile}`);
await import(process._archPath);
}

View File

@@ -0,0 +1,29 @@
import { app } from 'electron';
import { createRequire } from 'node:module';
import path from 'node:path';
if (process.arch === 'arm64') {
await setPaths('arm64');
} else {
await setPaths('x64');
}
async function setPaths(platform: string) {
// This should return the full path, ending in something like
// Notion.app/Contents/Resources/app
const appPath = app.getAppPath();
const appFolder = `app-${platform}`;
// Maybe we'll handle this in Electron one day
if (path.basename(appPath) === 'app') {
const platformAppPath = path.join(path.dirname(appPath), appFolder);
// This is an undocumented private API. It exists.
app.setAppPath(platformAppPath);
}
const require = createRequire(import.meta.url);
process._archPath = require.resolve(`../${appFolder}`);
await import(process._archPath);
}

View File

@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"target":"ESNext",
"outDir": ".",
"moduleResolution": "bundler"
},
"include": [
".",
"../ambient.d.ts"
],
"exclude": []
}

View File

@@ -19,8 +19,12 @@
},
"files": [
"dist/*",
"entry-asar/*",
"!entry-asar/**/*.ts",
"entry-asar/**/*",
"!entry-asar/**/has-asar.ts",
"!entry-asar/**/no-asar.ts",
"!entry-asar/**/has-asar.mts",
"!entry-asar/**/no-asar.mts",
"!entry-asar/**/tsconfig.json",
"README.md"
],
"author": "Samuel Attard",
@@ -28,12 +32,11 @@
"provenance": true
},
"scripts": {
"build": "tsc -p tsconfig.json && tsc -p tsconfig.entry-asar.json",
"build": "tsc -p tsconfig.json && tsc -p entry-asar/esm/tsconfig.json && tsc -p entry-asar/cjs/tsconfig.json",
"build:docs": "npx typedoc",
"lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
"prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
"prepublishOnly": "npm run build",
"pretest": "npm run build",
"prepack": "npm run build",
"test": "vitest run",
"prepare": "husky"
},
@@ -65,5 +68,6 @@
"*.ts": [
"prettier --write"
]
}
},
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f"
}

View File

@@ -3,15 +3,19 @@ import path from 'node:path';
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 UNPACKED_ASAR_PATH = path.join('Contents', 'Resources', 'app.asar.unpacked');
export enum AppFileType {
MACHO,
PLAIN,
INFO_PLIST,
SNAPSHOT,
APP_CODE,
SINGLE_ARCH,
}
export type AppFile = {
@@ -19,11 +23,37 @@ export type AppFile = {
type: AppFileType;
};
export type GetAllAppFilesOpts = {
singleArchFiles?: string;
};
const isSingleArchFile = (relativePath: string, opts: GetAllAppFilesOpts): boolean => {
if (opts.singleArchFiles === undefined) {
return false;
}
const unpackedPath = path.relative(UNPACKED_ASAR_PATH, relativePath);
// Outside of app.asar.unpacked
if (unpackedPath.startsWith('..')) {
return false;
}
return minimatch(unpackedPath, opts.singleArchFiles, {
matchBase: true,
});
};
/**
*
* @param appPath Path to the application
*/
export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
export const getAllAppFiles = async (
appPath: string,
opts: GetAllAppFilesOpts,
): Promise<AppFile[]> => {
const unpackedPath = path.join('Contents', 'Resources', 'app.asar.unpacked');
const files: AppFile[] = [];
const visited = new Set<string>();
@@ -35,6 +65,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
const info = await fs.promises.stat(p);
if (info.isSymbolicLink()) return;
if (info.isFile()) {
const relativePath = path.relative(appPath, p);
let fileType = AppFileType.PLAIN;
var fileOutput = '';
@@ -49,6 +81,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
}
if (p.endsWith('.asar')) {
fileType = AppFileType.APP_CODE;
} else if (isSingleArchFile(relativePath, opts)) {
fileType = AppFileType.SINGLE_ARCH;
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
fileType = AppFileType.MACHO;
} else if (p.endsWith('.bin')) {
@@ -58,7 +92,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
}
files.push({
relativePath: path.relative(appPath, p),
relativePath,
type: fileType,
});
}

View File

@@ -6,7 +6,7 @@ import * as asar from '@electron/asar';
import { spawn } from '@malept/cross-spawn-promise';
import * as dircompare from 'dir-compare';
import { minimatch } from 'minimatch';
import * as plist from 'plist';
import plist from 'plist';
import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils.js';
import { AppFile, AppFileType, fsMove, getAllAppFiles, readMachOHeader } from './file-utils.js';
@@ -75,7 +75,12 @@ export type MakeUniversalOpts = {
};
const dupedFiles = (files: AppFile[]) =>
files.filter((f) => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
files.filter(
(f) =>
f.type !== AppFileType.SNAPSHOT &&
f.type !== AppFileType.APP_CODE &&
f.type !== AppFileType.SINGLE_ARCH,
);
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
d('making a universal app with options', opts);
@@ -121,8 +126,8 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const uniqueToX64: string[] = [];
const uniqueToArm64: string[] = [];
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp));
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath));
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp), opts);
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath), opts);
for (const file of dupedFiles(x64Files)) {
if (!arm64Files.some((f) => f.relativePath === file.relativePath))
@@ -143,7 +148,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
);
}
for (const file of x64Files.filter((f) => f.type === AppFileType.PLAIN)) {
// Single Arch files are copied as is without processing.
const multiArchFiles = x64Files.filter((f) => f.type !== AppFileType.SINGLE_ARCH);
for (const file of multiArchFiles.filter((f) => f.type === AppFileType.PLAIN)) {
const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
if (x64Sha !== arm64Sha) {
@@ -159,7 +166,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
}
}
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.promises.realpath(path.resolve(tmpApp, machOFile.relativePath));
const second = await fs.promises.realpath(
path.resolve(opts.arm64AppPath, machOFile.relativePath),
@@ -247,17 +254,30 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'no-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
let pj = JSON.parse(
await fs.promises.readFile(
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
'utf8',
),
);
pj.main = 'index.js';
// Load a shim that redirects to the correct folder for the architecture.
// This needs to be a different file depending on if the app entrypoint is CommonJS or ESM.
if (pj.type === 'module' || pj.main.endsWith('.mjs')) {
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'esm', 'no-asar.mjs'),
path.resolve(entryAsar, 'index.mjs'),
);
pj.main = 'index.mjs';
} else {
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'cjs', 'no-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
pj.main = 'index.js';
}
await fs.promises.writeFile(
path.resolve(entryAsar, 'package.json'),
JSON.stringify(pj) + '\n',
@@ -330,10 +350,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'has-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
let pj = JSON.parse(
(
await asar.extractFile(
@@ -342,7 +358,23 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
)
).toString('utf8'),
);
pj.main = 'index.js';
// Load a shim that redirects to the correct `app.asar` for the architecture.
// This needs to be a different file depending on if the app entrypoint is CommonJS or ESM.
if (pj.type === 'module' || pj.main.endsWith('.mjs')) {
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'esm', 'has-asar.mjs'),
path.resolve(entryAsar, 'index.mjs'),
);
pj.main = 'index.mjs';
} else {
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'cjs', 'has-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
pj.main = 'index.js';
}
await fs.promises.writeFile(
path.resolve(entryAsar, 'package.json'),
JSON.stringify(pj) + '\n',
@@ -355,9 +387,9 @@ 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) {
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath);

View File

@@ -17,13 +17,20 @@ export interface AsarIntegrity {
[key: string]: HeaderHash;
}
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
export type ComputeIntegrityDataOpts = {
singleArchFiles?: string;
};
export async function computeIntegrityData(
contentsPath: string,
opts: ComputeIntegrityDataOpts,
): Promise<AsarIntegrity> {
const root = await fs.promises.realpath(contentsPath);
const resourcesRelativePath = 'Resources';
const resourcesPath = path.resolve(root, resourcesRelativePath);
const resources = await getAllAppFiles(resourcesPath);
const resources = await getAllAppFiles(resourcesPath, opts);
const resourceAsars = resources
.filter((file) => file.type === AppFileType.APP_CODE)
.reduce<IntegrityMap>(

View File

@@ -339,6 +339,86 @@ exports[`makeUniversalApp > asar mode > should merge two different asars when \`
}
`;
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": {

View File

@@ -12,8 +12,8 @@ describe('file-utils', () => {
let noAsarFiles: AppFile[];
beforeAll(async () => {
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'));
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'));
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'), {});
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'), {});
});
it('should correctly identify plist files', async () => {

View File

@@ -142,6 +142,65 @@ describe('makeUniversalApp', () => {
},
);
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(
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
{ timeout: VERIFY_APP_TIMEOUT },

View File

@@ -33,7 +33,12 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
// verify header
const asarFs = getRawHeader(path.resolve(resourcesDir, asar));
expect(
removeUnstableProperties(asarFs.header, containsRuntimeGeneratedMacho ? ['hello-world'] : []),
removeUnstableProperties(
asarFs.header,
containsRuntimeGeneratedMacho
? ['hello-world', 'hello-world-arm64', 'hello-world-x64']
: [],
),
).toMatchSnapshot();
}
@@ -53,7 +58,7 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
await verifyFileTree(path.resolve(resourcesDir, dir));
}
const allFiles = await fileUtils.getAllAppFiles(appPath);
const allFiles = await fileUtils.getAllAppFiles(appPath, {});
const infoPlists = allFiles
.filter(
(appFile) =>
@@ -89,7 +94,7 @@ const extractAsarIntegrity = async (infoPlist: string) => {
export const verifyFileTree = async (dirPath: string) => {
const { expect } = await import('vitest');
const dirFiles = await fileUtils.getAllAppFiles(dirPath);
const dirFiles = await fileUtils.getAllAppFiles(dirPath, {});
const files = dirFiles.map((file) => {
const it = path.join(dirPath, file.relativePath);
const name = toSystemIndependentPath(file.relativePath);
@@ -229,6 +234,7 @@ export const generateNativeApp = async (options: {
createAsar: boolean;
nativeModuleArch?: string;
additionalFiles?: Record<string, string>;
singleArchBindings?: boolean;
}) => {
const {
appNameWithExtension,
@@ -236,6 +242,7 @@ export const generateNativeApp = async (options: {
createAsar,
nativeModuleArch = arch,
additionalFiles,
singleArchBindings,
} = options;
const appPath = await templateApp(appNameWithExtension, arch, async (appPath) => {
const resources = path.join(appPath, 'Contents', 'Resources');
@@ -247,14 +254,19 @@ export const generateNativeApp = async (options: {
path.basename(appNameWithExtension, '.app'),
additionalFiles,
);
await fs.promises.cp(
path.join(appsDir, `hello-world-${nativeModuleArch}`),
path.join(testPath, 'hello-world'),
{ recursive: true, verbatimSymlinks: true },
);
let targetBinding: string;
if (singleArchBindings) {
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) {
await createPackageWithOptions(testPath, path.resolve(resources, 'app.asar'), {
unpack: '**/hello-world',
unpack: '**/hello-world*',
});
} else {
await fs.promises.cp(testPath, resourcesApp, { recursive: true, verbatimSymlinks: true });

View File

@@ -1,23 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"lib": [
"es2017"
],
"sourceMap": true,
"strict": true,
"outDir": "entry-asar",
"types": [
"node",
],
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"esModuleInterop": true,
"declaration": false
},
"include": [
"entry-asar"
],
"exclude": []
}

4596
yarn.lock

File diff suppressed because it is too large Load Diff