Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b67c905a6 | ||
|
|
740dd4aab3 | ||
|
|
d90d573ccf | ||
|
|
7c0ad6caa5 | ||
|
|
d76ca76072 | ||
|
|
4bf33415ec | ||
|
|
caa0567b76 | ||
|
|
7f59407631 | ||
|
|
915c061908 | ||
|
|
ef4ce1f9ac | ||
|
|
bf62ed4113 | ||
|
|
dd52b47795 | ||
|
|
9495fc3840 | ||
|
|
03b841956e | ||
|
|
a7d68c490d | ||
|
|
03e27e5a1d | ||
|
|
dfe5236357 | ||
|
|
89c99b438a | ||
|
|
e6d2697cbc | ||
|
|
c71eed1a06 | ||
|
|
5f8afd1b05 | ||
|
|
9236bd5853 | ||
|
|
a837738a09 | ||
|
|
44baf3b723 | ||
|
|
0e3b472c9a | ||
|
|
da359016d5 | ||
|
|
5d7bdf3ff3 | ||
|
|
16ce6ffbea | ||
|
|
df604873fa | ||
|
|
57201b124c | ||
|
|
20b1b02c11 | ||
|
|
b6f0c88db4 |
@@ -1,37 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
cfa: continuousauth/npm@1.0.2
|
||||
node: electronjs/node@1.4.1
|
||||
|
||||
workflows:
|
||||
test_and_release:
|
||||
# Run the test jobs first, then the release only when all the test jobs are successful
|
||||
jobs:
|
||||
- node/test:
|
||||
executor: node/macos
|
||||
name: test-mac-<< matrix.node-version >>
|
||||
override-ci-command: yarn install --frozen-lockfile --ignore-engines
|
||||
test-steps:
|
||||
- run: yarn build
|
||||
- run: yarn lint
|
||||
- run: yarn test
|
||||
use-test-steps: true
|
||||
matrix:
|
||||
alias: test
|
||||
parameters:
|
||||
node-version:
|
||||
- 20.5.0
|
||||
- 18.17.0
|
||||
- 16.20.1
|
||||
- 14.21.3
|
||||
- 12.22.12
|
||||
- 10.24.1
|
||||
- cfa/release:
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
context: cfa-release
|
||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
2
.github/workflows/add-to-project.yml
vendored
2
.github/workflows/add-to-project.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
creds: ${{ secrets.ECOSYSTEM_ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Add to Project
|
||||
uses: dsanders11/project-actions/add-item@3a81985616963f32fae17d1d1b406c631f3201a1 # v1.1.0
|
||||
uses: dsanders11/project-actions/add-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
with:
|
||||
field: Opened
|
||||
field-value: ${{ github.event.pull_request.created_at || github.event.issue.created_at }}
|
||||
|
||||
35
.github/workflows/release.yml
vendored
Normal file
35
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
environment: npm
|
||||
permissions:
|
||||
id-token: write # for CFA and npm provenance
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'yarn'
|
||||
- name: Install
|
||||
run: yarn install --frozen-lockfile
|
||||
- uses: continuousauth/action@4e8a2573eeb706f6d7300d6a9f3ca6322740b72d # v1.0.5
|
||||
with:
|
||||
project-id: ${{ secrets.CFA_PROJECT_ID }}
|
||||
secret: ${{ secrets.CFA_SECRET }}
|
||||
npm-token: ${{ secrets.NPM_TOKEN }}
|
||||
2
.github/workflows/semantic.yml
vendored
2
.github/workflows/semantic.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: semantic-pull-request
|
||||
uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 # v5.2.0
|
||||
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
44
.github/workflows/test.yml
vendored
Normal file
44
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '0 22 * * 3'
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version:
|
||||
- '20.5'
|
||||
- '18.17'
|
||||
- '16.20'
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version: "${{ matrix.node-version }}"
|
||||
cache: 'yarn'
|
||||
- name: Install (Node.js v18+)
|
||||
if : ${{ matrix.node-version != '16.20' }}
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Install (Node.js < v18)
|
||||
if : ${{ matrix.node-version == '16.20' }}
|
||||
run: yarn install --frozen-lockfile --ignore-engines
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
- name: Test
|
||||
run: yarn test
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,3 +3,7 @@ dist
|
||||
entry-asar/*.js*
|
||||
entry-asar/*.ts
|
||||
*.app
|
||||
test/fixtures/apps
|
||||
coverage
|
||||
docs
|
||||
.vscode
|
||||
@@ -3,5 +3,13 @@
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"parser": "typescript"
|
||||
"parser": "typescript",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.json", "*.jsonc", "*.json5"],
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
106
README.md
106
README.md
@@ -2,12 +2,16 @@
|
||||
|
||||
> Create universal macOS Electron applications
|
||||
|
||||
[](https://circleci.com/gh/electron/universal)
|
||||
[](https://github.com/electron/universal/actions/workflows/test.yml)
|
||||
[](https://npm.im/@electron/universal)
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
This package takes an x64 app and an arm64 app and glues them together into a
|
||||
[Universal macOS binary](https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary).
|
||||
|
||||
Note that parameters need to be **absolute** paths.
|
||||
|
||||
```typescript
|
||||
import { makeUniversalApp } from '@electron/universal';
|
||||
|
||||
@@ -18,20 +22,104 @@ await makeUniversalApp({
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
The basic usage patterns will work for most apps out of the box. Additional configuration
|
||||
options are available for advanced usecases.
|
||||
|
||||
### Merging ASAR archives to reduce app size
|
||||
|
||||
**Added in [v1.2.0](https://github.com/electron/universal/commit/38ab1c3559e25382957d608e49e624dc72a4409c)**
|
||||
|
||||
If you are using ASAR archives to store your Electron app's JavaScript code, you can use the
|
||||
`mergeASARs` option to merge your x64 and arm64 ASAR files to reduce the bundle size of
|
||||
the output Universal app.
|
||||
|
||||
If some files are present in only the x64 app but not the arm64 version (or vice-versa),
|
||||
you can exclude them from the merging process by specifying a `minimatch` pattern
|
||||
in `singleArchFiles`.
|
||||
|
||||
```typescript
|
||||
import { makeUniversalApp } from '@electron/universal';
|
||||
|
||||
await makeUniversalApp({
|
||||
x64AppPath: 'path/to/App_x64.app',
|
||||
arm64AppPath: 'path/to/App_arm64.app',
|
||||
outAppPath: 'path/to/App_universal.app',
|
||||
mergeASARs: true,
|
||||
singleArchFiles: 'node_modules/some-native-module/lib/binding/Release/**', // if you have files in your asar that are unique to x64 or arm64 apps
|
||||
});
|
||||
```
|
||||
|
||||
If `@electron/universal` detects an architecture-unique file that isn't covered by the
|
||||
`singleArchFiles` rule, an error will be thrown.
|
||||
|
||||
### Skip lipo for certain binaries in your Universal app
|
||||
|
||||
**Added in [1.3.0](https://github.com/electron/universal/commit/01dfb8a9636965fe154192b07934670dd42509f3)**
|
||||
|
||||
If your Electron app contains binary resources that are already merged with the
|
||||
`lipo` tool, providing a [`minimatch`] pattern to matching files in the `x64ArchFiles`
|
||||
parameter will prevent `@electron/universal` from attempting to merge them a second time.
|
||||
|
||||
```typescript
|
||||
import { makeUniversalApp } from '@electron/universal';
|
||||
|
||||
await makeUniversalApp({
|
||||
x64AppPath: 'path/to/App_x64.app',
|
||||
arm64AppPath: 'path/to/App_arm64.app',
|
||||
outAppPath: 'path/to/App_universal.app',
|
||||
mergeASARs: true,
|
||||
x64ArchFiles: '*/electron-helper', // `electron-helper` is a binary merged using `lipo`
|
||||
});
|
||||
```
|
||||
|
||||
If `@electron/universal` detects a lipo'd file that isn't covered by the `x64ArchFiles` rule,
|
||||
an error will be thrown.
|
||||
|
||||
### Including already codesigned app bundles into your Universal app
|
||||
|
||||
**Added in [v1.4.0](https://github.com/electron/universal/commit/b02ce7697fd2a3c2c79e1f6ab6bf7052125865cc)**
|
||||
|
||||
By default, the merging process will generate an `ElectronAsarIntegrity` key for
|
||||
any `Info.plist` files in your Electron app.
|
||||
|
||||
If your Electron app bundles another `.app` that is already signed, you need to use
|
||||
the `infoPlistsToIgnore` option to avoid modifying that app's plist.
|
||||
|
||||
```typescript
|
||||
import { makeUniversalApp } from '@electron/universal';
|
||||
|
||||
await makeUniversalApp({
|
||||
x64AppPath: 'path/to/App_x64.app',
|
||||
arm64AppPath: 'path/to/App_arm64.app',
|
||||
outAppPath: 'path/to/App_universal.app',
|
||||
infoPlistsToIgnore: 'my-internal.app/Contents/Info.plist'
|
||||
});
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
#### The app is twice as big now, why?
|
||||
|
||||
Well, a Universal app isn't anything magical. It is literally the x64 app and
|
||||
the arm64 app glued together into a single application. It's twice as big
|
||||
because it contains two apps in one.
|
||||
A Universal app is just the x64 app and the arm64 app glued together into a single application.
|
||||
It's twice as big because it contains two apps in one.
|
||||
|
||||
Merging your ASAR bundles can yield significant app size reductions depending on how large
|
||||
your `app.asar` file is.
|
||||
|
||||
#### What about native modules?
|
||||
|
||||
The way `@electron/universal` works today means you don't need to worry about
|
||||
things like building universal versions of your native modules. As long as
|
||||
your x64 and arm64 apps work in isolation the Universal app will work as well.
|
||||
Out of the box, you don't need to worry about building universal versions of your
|
||||
native modules. As long as your x64 and arm64 apps work in isolation, the Universal
|
||||
app will work as well.
|
||||
|
||||
Note that if you are using `mergeASARs`, you may need to add architecture-specific
|
||||
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)
|
||||
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
|
||||
|
||||
16
jest.config.js
Normal file
16
jest.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/** @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,
|
||||
};
|
||||
51
jest.setup.ts
Normal file
51
jest.setup.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { appsDir, asarsDir, templateApp } from './test/util';
|
||||
|
||||
export default async () => {
|
||||
await fs.remove(appsDir);
|
||||
await fs.mkdirp(appsDir);
|
||||
await templateApp('Arm64Asar.app', 'arm64', async (appPath) => {
|
||||
await fs.copy(
|
||||
path.resolve(asarsDir, 'app.asar'),
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
);
|
||||
});
|
||||
|
||||
// contains `extra-file.txt`
|
||||
await templateApp('Arm64AsarExtraFile.app', 'arm64', async (appPath) => {
|
||||
await fs.copy(
|
||||
path.resolve(asarsDir, 'app2.asar'),
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
);
|
||||
});
|
||||
|
||||
await templateApp('X64Asar.app', 'x64', async (appPath) => {
|
||||
await fs.copy(
|
||||
path.resolve(asarsDir, 'app.asar'),
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
);
|
||||
});
|
||||
|
||||
await templateApp('Arm64NoAsar.app', 'arm64', async (appPath) => {
|
||||
await fs.copy(
|
||||
path.resolve(asarsDir, 'app'),
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app'),
|
||||
);
|
||||
});
|
||||
|
||||
// contains `extra-file.txt`
|
||||
await templateApp('Arm64NoAsarExtraFile.app', 'arm64', async (appPath) => {
|
||||
await fs.copy(
|
||||
path.resolve(asarsDir, 'app2'),
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app'),
|
||||
);
|
||||
});
|
||||
|
||||
await templateApp('X64NoAsar.app', 'x64', async (appPath) => {
|
||||
await fs.copy(
|
||||
path.resolve(asarsDir, 'app'),
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app'),
|
||||
);
|
||||
});
|
||||
};
|
||||
55
package.json
55
package.json
@@ -12,10 +12,10 @@
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/electron/universal.git"
|
||||
"url": "git+https://github.com/electron/universal.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"node": ">=16.4"
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
@@ -24,38 +24,51 @@
|
||||
"README.md"
|
||||
],
|
||||
"author": "Samuel Attard",
|
||||
"publishConfig": {
|
||||
"provenance": true
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.entry-asar.json",
|
||||
"lint": "prettier --check \"{src,entry-asar}/**/*.ts\"",
|
||||
"prettier:write": "prettier --write \"{src,entry-asar}/**/*.ts\"",
|
||||
"build:docs": "npx typedoc",
|
||||
"lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
|
||||
"prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "exit 0",
|
||||
"test": "jest",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@continuous-auth/semantic-release-npm": "^3.0.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/fs-extra": "^9.0.4",
|
||||
"@types/minimatch": "^3.0.5",
|
||||
"@types/node": "^14.14.7",
|
||||
"@types/plist": "^3.0.2",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^10.5.1",
|
||||
"prettier": "^2.1.2",
|
||||
"typescript": "^4.0.5"
|
||||
"@electron/get": "^3.0.0",
|
||||
"@types/cross-zip": "^4.0.1",
|
||||
"@types/debug": "^4.1.10",
|
||||
"@types/fs-extra": "^11.0.3",
|
||||
"@types/jest": "^29.5.7",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/plist": "^3.0.4",
|
||||
"cross-zip": "^4.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"prettier": "^3.0.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typedoc": "~0.25.13",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"@malept/cross-spawn-promise": "^1.1.0",
|
||||
"@electron/asar": "^3.3.1",
|
||||
"@malept/cross-spawn-promise": "^2.0.0",
|
||||
"debug": "^4.3.1",
|
||||
"dir-compare": "^3.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"minimatch": "^3.0.4",
|
||||
"plist": "^3.0.4"
|
||||
"dir-compare": "^4.2.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"minimatch": "^9.0.3",
|
||||
"plist": "^3.1.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"jackspeak": "2.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { execFileSync } from 'child_process';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import minimatch from 'minimatch';
|
||||
import { minimatch } from 'minimatch';
|
||||
import os from 'os';
|
||||
import { d } from './debug';
|
||||
|
||||
@@ -25,18 +25,15 @@ export type MergeASARsOptions = {
|
||||
// See: https://github.com/apple-opensource-mirror/llvmCore/blob/0c60489d96c87140db9a6a14c6e82b15f5e5d252/include/llvm/Object/MachOFormat.h#L108-L112
|
||||
const MACHO_MAGIC = new Set([
|
||||
// 32-bit Mach-O
|
||||
0xfeedface,
|
||||
0xcefaedfe,
|
||||
0xfeedface, 0xcefaedfe,
|
||||
|
||||
// 64-bit Mach-O
|
||||
0xfeedfacf,
|
||||
0xcffaedfe,
|
||||
0xfeedfacf, 0xcffaedfe,
|
||||
]);
|
||||
|
||||
const MACHO_UNIVERSAL_MAGIC = new Set([
|
||||
// universal
|
||||
0xcafebabe,
|
||||
0xbebafeca,
|
||||
0xcafebabe, 0xbebafeca,
|
||||
]);
|
||||
|
||||
export const detectAsarMode = async (appPath: string) => {
|
||||
@@ -87,8 +84,10 @@ export const mergeASARs = async ({
|
||||
}: MergeASARsOptions): Promise<void> => {
|
||||
d(`merging ${x64AsarPath} and ${arm64AsarPath}`);
|
||||
|
||||
const x64Files = new Set(asar.listPackage(x64AsarPath).map(toRelativePath));
|
||||
const arm64Files = new Set(asar.listPackage(arm64AsarPath).map(toRelativePath));
|
||||
const x64Files = new Set(asar.listPackage(x64AsarPath, { isPack: false }).map(toRelativePath));
|
||||
const arm64Files = new Set(
|
||||
asar.listPackage(arm64AsarPath, { isPack: false }).map(toRelativePath),
|
||||
);
|
||||
|
||||
//
|
||||
// Build set of unpacked directories and files
|
||||
|
||||
@@ -45,7 +45,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (p.includes('app.asar')) {
|
||||
if (p.endsWith('.asar')) {
|
||||
fileType = AppFileType.APP_CODE;
|
||||
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
|
||||
fileType = AppFileType.MACHO;
|
||||
|
||||
77
src/index.ts
77
src/index.ts
@@ -1,51 +1,74 @@
|
||||
import { spawn } from '@malept/cross-spawn-promise';
|
||||
import * as asar from '@electron/asar';
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import minimatch from 'minimatch';
|
||||
import { minimatch } from 'minimatch';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as plist from 'plist';
|
||||
import * as dircompare from 'dir-compare';
|
||||
|
||||
import { AppFile, AppFileType, getAllAppFiles } from './file-utils';
|
||||
import { AsarMode, detectAsarMode, generateAsarIntegrity, mergeASARs } from './asar-utils';
|
||||
import { AsarMode, detectAsarMode, mergeASARs } from './asar-utils';
|
||||
import { sha } from './sha';
|
||||
import { d } from './debug';
|
||||
import { computeIntegrityData } from './integrity';
|
||||
|
||||
/**
|
||||
* Options to pass into the {@link makeUniversalApp} function.
|
||||
*
|
||||
* Requires absolute paths for input x64 and arm64 apps and an absolute path to the
|
||||
* output universal app.
|
||||
*/
|
||||
export type MakeUniversalOpts = {
|
||||
/**
|
||||
* Absolute file system path to the x64 version of your application. E.g. /Foo/bar/MyApp_x64.app
|
||||
* Absolute file system path to the x64 version of your application (e.g. `/Foo/bar/MyApp_x64.app`).
|
||||
*/
|
||||
x64AppPath: string;
|
||||
/**
|
||||
* Absolute file system path to the arm64 version of your application. E.g. /Foo/bar/MyApp_arm64.app
|
||||
* Absolute file system path to the arm64 version of your application (e.g. `/Foo/bar/MyApp_arm64.app`).
|
||||
*/
|
||||
arm64AppPath: string;
|
||||
/**
|
||||
* Absolute file system path you want the universal app to be written to. E.g. /Foo/var/MyApp_universal.app
|
||||
* Absolute file system path you want the universal app to be written to (e.g. `/Foo/var/MyApp_universal.app`).
|
||||
*
|
||||
* If this file exists it will be overwritten ONLY if "force" is set to true
|
||||
* If this file exists on disk already, it will be overwritten ONLY if {@link MakeUniversalOpts.force} is set to `true`.
|
||||
*/
|
||||
outAppPath: string;
|
||||
/**
|
||||
* Forcefully overwrite any existing files that are in the way of generating the universal application
|
||||
* Forcefully overwrite any existing files that are in the way of generating the universal application.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
force: boolean;
|
||||
force?: boolean;
|
||||
/**
|
||||
* Merge x64 and arm64 ASARs into one.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
mergeASARs?: boolean;
|
||||
/**
|
||||
* Minimatch pattern of paths that are allowed to be present in one of the ASAR files, but not in the other.
|
||||
* If {@link MakeUniversalOpts.mergeASARs} is enabled, this property provides a
|
||||
* {@link https://github.com/isaacs/minimatch?tab=readme-ov-file#features | minimatch}
|
||||
* pattern of paths that are allowed to be present in one of the ASAR files, but not in the other.
|
||||
*
|
||||
*/
|
||||
singleArchFiles?: string;
|
||||
/**
|
||||
* Minimatch pattern of binaries that are expected to be the same x64 binary in both of the ASAR files.
|
||||
* A {@link https://github.com/isaacs/minimatch?tab=readme-ov-file#features | minimatch}
|
||||
* pattern of binaries that are expected to be the same x64 binary in both
|
||||
*
|
||||
* Use this if your application contains binaries that have already been merged into a universal file
|
||||
* using the `lipo` tool.
|
||||
*
|
||||
* @see Apple's {@link https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary | Building a universal macOS binary} documentation
|
||||
*
|
||||
*/
|
||||
x64ArchFiles?: string;
|
||||
/**
|
||||
* Minimatch pattern of paths that should not receive an injected ElectronAsarIntegrity value
|
||||
* A {@link https://github.com/isaacs/minimatch?tab=readme-ov-file#features | minimatch} pattern of `Info.plist`
|
||||
* paths that should not receive an injected `ElectronAsarIntegrity` value.
|
||||
*
|
||||
* Use this if your application contains another bundle that's already signed.
|
||||
*/
|
||||
infoPlistsToIgnore?: string;
|
||||
};
|
||||
@@ -134,7 +157,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const knownMergedMachOFiles = new Set();
|
||||
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) {
|
||||
const first = await fs.realpath(path.resolve(tmpApp, machOFile.relativePath));
|
||||
const second = await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath));
|
||||
@@ -171,6 +194,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
'-output',
|
||||
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
|
||||
]);
|
||||
knownMergedMachOFiles.add(machOFile.relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,8 +210,18 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
|
||||
{ compareSize: true, compareContent: true },
|
||||
);
|
||||
const differences = comparison.diffSet!.filter((difference) => difference.state !== 'equal');
|
||||
d(`Found ${differences.length} difference(s) between the x64 and arm64 folders`);
|
||||
const nonMergedDifferences = differences.filter(
|
||||
(difference) =>
|
||||
!difference.name1 ||
|
||||
!knownMergedMachOFiles.has(
|
||||
path.join('Contents', 'Resources', 'app', difference.relativePath, difference.name1),
|
||||
),
|
||||
);
|
||||
d(`After discluding MachO files merged with lipo ${nonMergedDifferences.length} remain.`);
|
||||
|
||||
if (!comparison.same) {
|
||||
if (nonMergedDifferences.length > 0) {
|
||||
d('x64 and arm64 app folders are different, creating dynamic entry ASAR');
|
||||
await fs.move(
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app'),
|
||||
@@ -218,9 +252,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
}
|
||||
}
|
||||
|
||||
const generatedIntegrity: Record<string, { algorithm: 'SHA256'; hash: string }> = {};
|
||||
let didSplitAsar = false;
|
||||
|
||||
/**
|
||||
* If we have an ASAR we just need to check if the two "app.asar" files have the same hash,
|
||||
* if they are, same as above, we can leave one there and call it a day. If they're different
|
||||
@@ -238,8 +269,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
outputAsarPath: output,
|
||||
singleArchFiles: opts.singleArchFiles,
|
||||
});
|
||||
|
||||
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(output);
|
||||
} else if (x64AsarMode === AsarMode.HAS_ASAR) {
|
||||
d('checking if the x64 and arm64 asars are identical');
|
||||
const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
|
||||
@@ -248,7 +277,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
);
|
||||
|
||||
if (x64AsarSha !== arm64AsarSha) {
|
||||
didSplitAsar = true;
|
||||
d('x64 and arm64 asars are different');
|
||||
const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar');
|
||||
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath);
|
||||
@@ -296,18 +324,13 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
||||
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
||||
const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar');
|
||||
await asar.createPackage(entryAsar, asarPath);
|
||||
|
||||
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(asarPath);
|
||||
generatedIntegrity['Resources/app-x64.asar'] = generateAsarIntegrity(x64AsarPath);
|
||||
generatedIntegrity['Resources/app-arm64.asar'] = generateAsarIntegrity(arm64AsarPath);
|
||||
} else {
|
||||
d('x64 and arm64 asars are the same');
|
||||
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(
|
||||
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'));
|
||||
|
||||
const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST);
|
||||
for (const plistFile of plistFiles) {
|
||||
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
|
||||
|
||||
51
src/integrity.ts
Normal file
51
src/integrity.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { AppFileType, getAllAppFiles } from './file-utils';
|
||||
import { sha } from './sha';
|
||||
import { generateAsarIntegrity } from './asar-utils';
|
||||
|
||||
type IntegrityMap = {
|
||||
[filepath: string]: string;
|
||||
};
|
||||
|
||||
export interface HeaderHash {
|
||||
algorithm: 'SHA256';
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export interface AsarIntegrity {
|
||||
[key: string]: HeaderHash;
|
||||
}
|
||||
|
||||
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
|
||||
const root = await fs.realpath(contentsPath);
|
||||
|
||||
const resourcesRelativePath = 'Resources';
|
||||
const resourcesPath = path.resolve(root, resourcesRelativePath);
|
||||
|
||||
const resources = await getAllAppFiles(resourcesPath);
|
||||
const resourceAsars = resources
|
||||
.filter((file) => file.type === AppFileType.APP_CODE)
|
||||
.reduce<IntegrityMap>(
|
||||
(prev, file) => ({
|
||||
...prev,
|
||||
[path.join(resourcesRelativePath, file.relativePath)]: path.join(
|
||||
resourcesPath,
|
||||
file.relativePath,
|
||||
),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
// sort to produce constant result
|
||||
const allAsars = Object.entries(resourceAsars).sort(([name1], [name2]) =>
|
||||
name1.localeCompare(name2),
|
||||
);
|
||||
const hashes = await Promise.all(allAsars.map(async ([, from]) => generateAsarIntegrity(from)));
|
||||
const asarIntegrity: AsarIntegrity = {};
|
||||
for (let i = 0; i < allAsars.length; i++) {
|
||||
const [asar] = allAsars[i];
|
||||
asarIntegrity[asar] = hashes[i];
|
||||
}
|
||||
return asarIntegrity;
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as crypto from 'crypto';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
import { d } from './debug';
|
||||
|
||||
export const sha = async (filePath: string) => {
|
||||
d('hashing', filePath);
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.setEncoding('hex');
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
fileStream.pipe(hash);
|
||||
await new Promise((resolve, reject) => {
|
||||
fileStream.on('end', () => resolve());
|
||||
fileStream.on('error', (err) => reject(err));
|
||||
});
|
||||
await pipeline(fs.createReadStream(filePath), hash);
|
||||
return hash.read();
|
||||
};
|
||||
|
||||
722
test/__snapshots__/index.spec.ts.snap
Normal file
722
test/__snapshots__/index.spec.ts.snap
Normal file
@@ -0,0 +1,722 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`makeUniversalApp asar mode should correctly merge two identical asars 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
],
|
||||
"hash": "8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
},
|
||||
"size": 64,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
],
|
||||
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
},
|
||||
"size": 41,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should correctly merge two identical asars 2`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "85fff474383bd8df11cd9c5784e8fcd1525af71ff140a8a882e1dc9d5b39fcbf",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"extra-file.txt": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"b8f261b95f81761658c8875b33a68001d8750fd898f447373bf6347e779bc3de",
|
||||
],
|
||||
"hash": "b8f261b95f81761658c8875b33a68001d8750fd898f447373bf6347e779bc3de",
|
||||
},
|
||||
"size": 15,
|
||||
},
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
],
|
||||
"hash": "8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
},
|
||||
"size": 64,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
],
|
||||
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
},
|
||||
"size": 41,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 2`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
],
|
||||
"hash": "8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
},
|
||||
"size": 64,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
],
|
||||
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
},
|
||||
"size": 41,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 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 create a shim if asars are different between architectures 4`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app-arm64.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "71db54541357128943df64d54480a22d0cdf4c283f2044f48101fb1fc6e6fb2d",
|
||||
},
|
||||
"Resources/app-x64.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "85fff474383bd8df11cd9c5784e8fcd1525af71ff140a8a882e1dc9d5b39fcbf",
|
||||
},
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "b62aaaed07ff72dc33da1720d900e0443c060285ef374ce1bdaef1d4f28b5fe4",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
"file.txt": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
],
|
||||
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
},
|
||||
"size": 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"var": {
|
||||
"link": "private/var",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 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 generate AsarIntegrity for all asars in the application 3`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
|
||||
},
|
||||
"Resources/webbapp.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"extra-file.txt": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"b8f261b95f81761658c8875b33a68001d8750fd898f447373bf6347e779bc3de",
|
||||
],
|
||||
"hash": "b8f261b95f81761658c8875b33a68001d8750fd898f447373bf6347e779bc3de",
|
||||
},
|
||||
"size": 15,
|
||||
},
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
],
|
||||
"hash": "8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
},
|
||||
"size": 64,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
],
|
||||
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
},
|
||||
"size": 41,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 2`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "71db54541357128943df64d54480a22d0cdf4c283f2044f48101fb1fc6e6fb2d",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
"file.txt": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
],
|
||||
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
},
|
||||
"size": 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"var": {
|
||||
"link": "private/var",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 2`] = `
|
||||
{
|
||||
"Contents/Info.plist": undefined,
|
||||
"Contents/Resources/SubApp-1.app/Contents/Info.plist": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
|
||||
],
|
||||
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
|
||||
},
|
||||
"size": 66,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
],
|
||||
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
},
|
||||
"size": 41,
|
||||
},
|
||||
"private": {
|
||||
"files": {
|
||||
"var": {
|
||||
"files": {
|
||||
"app": {
|
||||
"files": {
|
||||
"file.txt": {
|
||||
"link": "private/var/file.txt",
|
||||
"unpacked": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"file.txt": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
],
|
||||
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
},
|
||||
"size": 11,
|
||||
"unpacked": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"var": {
|
||||
"link": "private/var",
|
||||
"unpacked": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 2`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
|
||||
],
|
||||
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
|
||||
},
|
||||
"size": 66,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
],
|
||||
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
},
|
||||
"size": 41,
|
||||
},
|
||||
"private": {
|
||||
"files": {
|
||||
"var": {
|
||||
"files": {
|
||||
"app": {
|
||||
"files": {
|
||||
"file.txt": {
|
||||
"link": "private/var/file.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
"file.txt": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
],
|
||||
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
||||
},
|
||||
"size": 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"var": {
|
||||
"link": "private/var",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 3`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"b7e5f58d3c0fddc1a57d1279a7f19a34a01784f4036920d4b60a1e33f6d1635b",
|
||||
],
|
||||
"hash": "b7e5f58d3c0fddc1a57d1279a7f19a34a01784f4036920d4b60a1e33f6d1635b",
|
||||
},
|
||||
"size": 1068,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
|
||||
],
|
||||
"hash": "2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
|
||||
},
|
||||
"size": 33,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 4`] = `
|
||||
[
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp asar mode should shim asars with different unpacked dirs 5`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app-arm64.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "d06a628e759f54def7ff8785a077b3a3d756882cb84ee99e9725966226e1f195",
|
||||
},
|
||||
"Resources/app-x64.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
|
||||
},
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "b62aaaed07ff72dc33da1720d900e0443c060285ef374ce1bdaef1d4f28b5fe4",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp force packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
],
|
||||
"hash": "8c8cefe616b330a70980c457e479360417a320f53f484d34df65227ce3add026",
|
||||
},
|
||||
"size": 64,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
],
|
||||
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
|
||||
},
|
||||
"size": 41,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp force packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 2`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "85fff474383bd8df11cd9c5784e8fcd1525af71ff140a8a882e1dc9d5b39fcbf",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 1`] = `
|
||||
[
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 2`] = `
|
||||
{
|
||||
"Contents/Info.plist": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode should shim two different app folders 1`] = `
|
||||
{
|
||||
"files": {
|
||||
"index.js": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
|
||||
],
|
||||
"hash": "f1e14240f7c833900fca84fabc2f0ff27084efdf1c5b228b015515de3f8fa28e",
|
||||
},
|
||||
"size": 1063,
|
||||
},
|
||||
"package.json": {
|
||||
"integrity": {
|
||||
"algorithm": "SHA256",
|
||||
"blockSize": 4194304,
|
||||
"blocks": [
|
||||
"2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
|
||||
],
|
||||
"hash": "2873266521e41d58d02e7acfbbbdb046edfa04b6ce262b8987de8e8548671fc7",
|
||||
},
|
||||
"size": 33,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode should shim two different app folders 2`] = `
|
||||
[
|
||||
"private/var/i-aint-got-no-rhythm.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode should shim two different app folders 3`] = `
|
||||
[
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
"private/var/i-aint-got-no-rhythm.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode should shim two different app folders 4`] = `
|
||||
[
|
||||
"index.js",
|
||||
{
|
||||
"content": "{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}",
|
||||
"name": "package.json",
|
||||
},
|
||||
{
|
||||
"content": "hello world",
|
||||
"name": "private/var/file.txt",
|
||||
},
|
||||
"private/var/hello-world.bin",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeUniversalApp no asar mode should shim two different app folders 5`] = `
|
||||
{
|
||||
"Contents/Info.plist": {
|
||||
"Resources/app.asar": {
|
||||
"algorithm": "SHA256",
|
||||
"hash": "27433ee3e34b3b0dabb29d18d40646126e80c56dbce8c4bb2adef7278b5a46c0",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
28
test/asar-utils.spec.ts
Normal file
28
test/asar-utils.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import { AsarMode, detectAsarMode, generateAsarIntegrity } from '../src/asar-utils';
|
||||
|
||||
const asarsPath = path.resolve(__dirname, 'fixtures', 'asars');
|
||||
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
|
||||
|
||||
describe('asar-utils', () => {
|
||||
describe('detectAsarMode', () => {
|
||||
it('should correctly detect an asar enabled app', async () => {
|
||||
expect(await detectAsarMode(path.resolve(appsPath, 'Arm64Asar.app'))).toBe(AsarMode.HAS_ASAR);
|
||||
});
|
||||
|
||||
it('should correctly detect an app without an asar', async () => {
|
||||
expect(await detectAsarMode(path.resolve(appsPath, 'Arm64NoAsar.app'))).toBe(
|
||||
AsarMode.NO_ASAR,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAsarIntegrity', () => {
|
||||
it('should deterministically hash an asar header', async () => {
|
||||
expect(generateAsarIntegrity(path.resolve(asarsPath, 'app.asar')).hash).toEqual(
|
||||
'85fff474383bd8df11cd9c5784e8fcd1525af71ff140a8a882e1dc9d5b39fcbf',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
61
test/file-utils.spec.ts
Normal file
61
test/file-utils.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import { AppFile, AppFileType, getAllAppFiles } from '../src/file-utils';
|
||||
|
||||
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
|
||||
|
||||
describe('file-utils', () => {
|
||||
describe('getAllAppFiles', () => {
|
||||
let asarFiles: AppFile[];
|
||||
let noAsarFiles: AppFile[];
|
||||
|
||||
beforeAll(async () => {
|
||||
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'));
|
||||
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'));
|
||||
});
|
||||
|
||||
it('should correctly identify plist files', async () => {
|
||||
expect(asarFiles.find((f) => f.relativePath === 'Contents/Info.plist')?.type).toBe(
|
||||
AppFileType.INFO_PLIST,
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly identify asar files as app code', async () => {
|
||||
expect(asarFiles.find((f) => f.relativePath === 'Contents/Resources/app.asar')?.type).toBe(
|
||||
AppFileType.APP_CODE,
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly identify non-asar code files as plain text', async () => {
|
||||
expect(
|
||||
noAsarFiles.find((f) => f.relativePath === 'Contents/Resources/app/index.js')?.type,
|
||||
).toBe(AppFileType.PLAIN);
|
||||
});
|
||||
|
||||
it('should correctly identify the Electron binary as Mach-O', async () => {
|
||||
expect(noAsarFiles.find((f) => f.relativePath === 'Contents/MacOS/Electron')?.type).toBe(
|
||||
AppFileType.MACHO,
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly identify the Electron Framework as Mach-O', async () => {
|
||||
expect(
|
||||
noAsarFiles.find(
|
||||
(f) =>
|
||||
f.relativePath ===
|
||||
'Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework',
|
||||
)?.type,
|
||||
).toBe(AppFileType.MACHO);
|
||||
});
|
||||
|
||||
it('should correctly identify the v8 context snapshot', async () => {
|
||||
expect(
|
||||
noAsarFiles.find(
|
||||
(f) =>
|
||||
f.relativePath ===
|
||||
'Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/v8_context_snapshot.arm64.bin',
|
||||
)?.type,
|
||||
).toBe(AppFileType.SNAPSHOT);
|
||||
});
|
||||
});
|
||||
});
|
||||
BIN
test/fixtures/asars/app.asar
vendored
Normal file
BIN
test/fixtures/asars/app.asar
vendored
Normal file
Binary file not shown.
2
test/fixtures/asars/app/index.js
vendored
Normal file
2
test/fixtures/asars/app/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
console.log('I am an app folder', process.arch);
|
||||
process.exit(0);
|
||||
4
test/fixtures/asars/app/package.json
vendored
Normal file
4
test/fixtures/asars/app/package.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}
|
||||
BIN
test/fixtures/asars/app2.asar
vendored
Normal file
BIN
test/fixtures/asars/app2.asar
vendored
Normal file
Binary file not shown.
1
test/fixtures/asars/app2/extra-file.txt
vendored
Normal file
1
test/fixtures/asars/app2/extra-file.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
erick was here!
|
||||
2
test/fixtures/asars/app2/index.js
vendored
Normal file
2
test/fixtures/asars/app2/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
console.log('I am an app.asar', process.arch);
|
||||
process.exit(0);
|
||||
4
test/fixtures/asars/app2/package.json
vendored
Normal file
4
test/fixtures/asars/app2/package.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "app",
|
||||
"main": "index.js"
|
||||
}
|
||||
1
test/fixtures/tohash
vendored
Normal file
1
test/fixtures/tohash
vendored
Normal file
@@ -0,0 +1 @@
|
||||
hello there
|
||||
283
test/index.spec.ts
Normal file
283
test/index.spec.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { makeUniversalApp } from '../dist/cjs/index';
|
||||
import { createTestApp, templateApp, VERIFY_APP_TIMEOUT, verifyApp } from './util';
|
||||
import { createPackage, createPackageWithOptions } from '@electron/asar';
|
||||
|
||||
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
|
||||
const appsOutPath = path.resolve(__dirname, 'fixtures', 'apps', 'out');
|
||||
|
||||
// See `jest.setup.ts` for app fixture setup process
|
||||
describe('makeUniversalApp', () => {
|
||||
afterEach(async () => {
|
||||
await fs.emptyDir(appsOutPath);
|
||||
});
|
||||
|
||||
it('throws an error if asar is only detected in one arch', async () => {
|
||||
const out = path.resolve(appsOutPath, 'Error.app');
|
||||
await expect(
|
||||
makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
|
||||
arm64AppPath: path.resolve(appsPath, 'Arm64NoAsar.app'),
|
||||
outAppPath: out,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'Both the x64 and arm64 versions of your application need to have been built with the same asar settings (enabled vs disabled)',
|
||||
);
|
||||
});
|
||||
|
||||
it.todo('works for lipo binary resources');
|
||||
|
||||
describe('force', () => {
|
||||
it('throws an error if `out` bundle already exists and `force` is `false`', async () => {
|
||||
const out = path.resolve(appsOutPath, 'Error.app');
|
||||
await fs.mkdirp(out);
|
||||
await expect(
|
||||
makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
|
||||
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
|
||||
outAppPath: out,
|
||||
}),
|
||||
).rejects.toThrow(/The out path ".*" already exists and force is not set to true/);
|
||||
});
|
||||
|
||||
it(
|
||||
'packages successfully if `out` bundle already exists and `force` is `true`',
|
||||
async () => {
|
||||
const out = path.resolve(appsOutPath, 'Error.app');
|
||||
await fs.mkdirp(out);
|
||||
await makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
|
||||
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
|
||||
outAppPath: out,
|
||||
force: true,
|
||||
});
|
||||
await verifyApp(out);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
});
|
||||
|
||||
describe('asar mode', () => {
|
||||
it(
|
||||
'should correctly merge two identical asars',
|
||||
async () => {
|
||||
const out = path.resolve(appsOutPath, 'MergedAsar.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
|
||||
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
|
||||
outAppPath: out,
|
||||
});
|
||||
await verifyApp(out);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'should create a shim if asars are different between architectures',
|
||||
async () => {
|
||||
const out = path.resolve(appsOutPath, 'ShimmedAsar.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
|
||||
arm64AppPath: path.resolve(appsPath, 'Arm64AsarExtraFile.app'),
|
||||
outAppPath: out,
|
||||
});
|
||||
await verifyApp(out);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'should merge two different asars when `mergeASARs` is enabled',
|
||||
async () => {
|
||||
const out = path.resolve(appsOutPath, 'MergedAsar.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
|
||||
arm64AppPath: path.resolve(appsPath, 'Arm64AsarExtraFile.app'),
|
||||
outAppPath: out,
|
||||
mergeASARs: true,
|
||||
singleArchFiles: 'extra-file.txt',
|
||||
});
|
||||
await verifyApp(out);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique file',
|
||||
async () => {
|
||||
const out = path.resolve(appsOutPath, 'Error.app');
|
||||
await expect(
|
||||
makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
|
||||
arm64AppPath: path.resolve(appsPath, 'Arm64AsarExtraFile.app'),
|
||||
outAppPath: out,
|
||||
mergeASARs: true,
|
||||
singleArchFiles: 'bad-rule',
|
||||
}),
|
||||
).rejects.toThrow(/Detected unique file "extra-file\.txt"/);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
|
||||
async () => {
|
||||
const arm64AppPath = await templateApp('Arm64-1.app', 'arm64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('Arm64-1');
|
||||
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
|
||||
await templateApp('SubApp-1.app', 'arm64', async (subArm64AppPath) => {
|
||||
await fs.move(
|
||||
subArm64AppPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
|
||||
);
|
||||
});
|
||||
});
|
||||
const x64AppPath = await templateApp('X64-1.app', 'x64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('X64-1');
|
||||
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
|
||||
await templateApp('SubApp-1.app', 'x64', async (subArm64AppPath) => {
|
||||
await fs.move(
|
||||
subArm64AppPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
|
||||
);
|
||||
});
|
||||
});
|
||||
const outAppPath = path.resolve(appsOutPath, 'UnmodifiedPlist.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath,
|
||||
mergeASARs: true,
|
||||
infoPlistsToIgnore: 'SubApp-1.app/Contents/Info.plist',
|
||||
});
|
||||
await verifyApp(outAppPath);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
// TODO: Investigate if this should even be allowed.
|
||||
// Current logic detects all unpacked files as APP_CODE, which doesn't seem correct since it could also be a macho file requiring lipo
|
||||
// https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49
|
||||
it.skip(
|
||||
'should shim asars with different unpacked dirs',
|
||||
async () => {
|
||||
const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('UnpackedAppArm64');
|
||||
await createPackageWithOptions(
|
||||
testPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
{
|
||||
unpackDir: 'var',
|
||||
unpack: '*.txt',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const x64AppPath = await templateApp('UnpackedX64.app', 'x64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('UnpackedAppX64');
|
||||
await createPackageWithOptions(
|
||||
testPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
const outAppPath = path.resolve(appsOutPath, 'UnpackedDir.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath,
|
||||
});
|
||||
await verifyApp(outAppPath);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'should generate AsarIntegrity for all asars in the application',
|
||||
async () => {
|
||||
const { testPath } = await createTestApp('app-2');
|
||||
const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
|
||||
await createPackage(testPath, testAsarPath);
|
||||
|
||||
const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => {
|
||||
await fs.copyFile(
|
||||
testAsarPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
);
|
||||
await fs.copyFile(
|
||||
testAsarPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'),
|
||||
);
|
||||
});
|
||||
const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => {
|
||||
await fs.copyFile(
|
||||
testAsarPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
);
|
||||
await fs.copyFile(
|
||||
testAsarPath,
|
||||
path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'),
|
||||
);
|
||||
});
|
||||
const outAppPath = path.resolve(appsOutPath, 'MultipleAsars.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath,
|
||||
mergeASARs: true,
|
||||
});
|
||||
await verifyApp(outAppPath);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
});
|
||||
|
||||
describe('no asar mode', () => {
|
||||
it(
|
||||
'should correctly merge two identical app folders',
|
||||
async () => {
|
||||
const out = path.resolve(appsOutPath, 'MergedNoAsar.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath: path.resolve(appsPath, 'X64NoAsar.app'),
|
||||
arm64AppPath: path.resolve(appsPath, 'Arm64NoAsar.app'),
|
||||
outAppPath: out,
|
||||
});
|
||||
await verifyApp(out);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'should shim two different app folders',
|
||||
async () => {
|
||||
const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('shimArm64', {
|
||||
'i-aint-got-no-rhythm.bin': 'boomshakalaka',
|
||||
});
|
||||
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'));
|
||||
});
|
||||
|
||||
const x64AppPath = await templateApp('ShimX64.app', 'x64', async (appPath) => {
|
||||
const { testPath } = await createTestApp('shimX64', { 'hello-world.bin': 'Hello World' });
|
||||
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'));
|
||||
});
|
||||
|
||||
const outAppPath = path.resolve(appsOutPath, 'ShimNoAsar.app');
|
||||
await makeUniversalApp({
|
||||
x64AppPath,
|
||||
arm64AppPath,
|
||||
outAppPath,
|
||||
});
|
||||
await verifyApp(outAppPath);
|
||||
},
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Add tests for
|
||||
// * different app dirs with different macho files
|
||||
// * identical app dirs with universal macho files
|
||||
});
|
||||
11
test/sha.spec.ts
Normal file
11
test/sha.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import { sha } from '../src/sha';
|
||||
|
||||
describe('sha', () => {
|
||||
it('should correctly hash a file', async () => {
|
||||
expect(await sha(path.resolve(__dirname, 'fixtures', 'tohash'))).toEqual(
|
||||
'12998c017066eb0d2a70b94e6ed3192985855ce390f321bbdb832022888bd251',
|
||||
);
|
||||
});
|
||||
});
|
||||
181
test/util.ts
Normal file
181
test/util.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { downloadArtifact } from '@electron/get';
|
||||
import { spawn } from '@malept/cross-spawn-promise';
|
||||
import * as zip from 'cross-zip';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import plist from 'plist';
|
||||
import * as fileUtils from '../dist/cjs/file-utils';
|
||||
import { getRawHeader } from '@electron/asar';
|
||||
|
||||
// We do a LOT of verifications in `verifyApp` 😅
|
||||
// exec universal binary -> verify ALL asars -> verify ALL app dirs -> verify ALL asar integrity entries
|
||||
// plus some tests create fixtures at runtime
|
||||
export const VERIFY_APP_TIMEOUT = 80 * 1000;
|
||||
|
||||
export const asarsDir = path.resolve(__dirname, 'fixtures', 'asars');
|
||||
export const appsDir = path.resolve(__dirname, 'fixtures', 'apps');
|
||||
export const appsOutPath = path.resolve(appsDir, 'out');
|
||||
|
||||
export const verifyApp = async (appPath: string) => {
|
||||
await ensureUniversal(appPath);
|
||||
|
||||
const resourcesDir = path.resolve(appPath, 'Contents', 'Resources');
|
||||
const resourcesDirContents = await fs.readdir(resourcesDir);
|
||||
|
||||
// sort for consistent result
|
||||
const asars = resourcesDirContents.filter((p) => p.endsWith('.asar')).sort();
|
||||
for await (const asar of asars) {
|
||||
// verify header
|
||||
const asarFs = getRawHeader(path.resolve(resourcesDir, asar));
|
||||
expect(removeUnstableProperties(asarFs.header)).toMatchSnapshot();
|
||||
}
|
||||
|
||||
// check all app and unpacked dirs (incl. shimmed)
|
||||
const dirsToSnapshot = [
|
||||
'app',
|
||||
'app.asar.unpacked',
|
||||
'app-x64',
|
||||
'app-x64.asar.unpacked',
|
||||
'app-arm64',
|
||||
'app-arm64.asar.unpacked',
|
||||
];
|
||||
const appDirs = resourcesDirContents
|
||||
.filter((p) => dirsToSnapshot.includes(path.basename(p)))
|
||||
.sort();
|
||||
for await (const dir of appDirs) {
|
||||
await verifyFileTree(path.resolve(resourcesDir, dir));
|
||||
}
|
||||
|
||||
const allFiles = await fileUtils.getAllAppFiles(appPath);
|
||||
const infoPlists = allFiles
|
||||
.filter(
|
||||
(appFile) =>
|
||||
appFile.type === fileUtils.AppFileType.INFO_PLIST &&
|
||||
// These are test app fixtures, no need to snapshot within `TestApp.app/Contents/Frameworks`
|
||||
!appFile.relativePath.includes(path.join('Contents', 'Frameworks')),
|
||||
)
|
||||
.map((af) => af.relativePath)
|
||||
.sort();
|
||||
|
||||
const integrityMap: Record<string, string> = {};
|
||||
const integrity = await Promise.all(
|
||||
infoPlists.map((ip) => extractAsarIntegrity(path.resolve(appPath, ip))),
|
||||
);
|
||||
for (let i = 0; i < integrity.length; i++) {
|
||||
const relativePath = infoPlists[i];
|
||||
const asarIntegrity = integrity[i];
|
||||
integrityMap[relativePath] = asarIntegrity;
|
||||
}
|
||||
expect(integrityMap).toMatchSnapshot();
|
||||
};
|
||||
|
||||
// note: `infoPlistsToIgnore` will not have integrity in sub-app plists
|
||||
const extractAsarIntegrity = async (infoPlist: string) => {
|
||||
const { ElectronAsarIntegrity: integrity, ...otherData } = plist.parse(
|
||||
await fs.readFile(infoPlist, 'utf-8'),
|
||||
) as any;
|
||||
return integrity;
|
||||
};
|
||||
|
||||
export const verifyFileTree = async (dirPath: string) => {
|
||||
const dirFiles = await fileUtils.getAllAppFiles(dirPath);
|
||||
const files = dirFiles.map((file) => {
|
||||
const it = path.join(dirPath, file.relativePath);
|
||||
const name = toSystemIndependentPath(file.relativePath);
|
||||
if (it.endsWith('.txt') || it.endsWith('.json')) {
|
||||
return { name, content: fs.readFileSync(it, 'utf-8') };
|
||||
}
|
||||
return name;
|
||||
});
|
||||
expect(files).toMatchSnapshot();
|
||||
};
|
||||
|
||||
export const ensureUniversal = async (app: string) => {
|
||||
const exe = path.resolve(app, 'Contents', 'MacOS', 'Electron');
|
||||
const result = await spawn(exe);
|
||||
expect(result).toContain('arm64');
|
||||
const result2 = await spawn('arch', ['-x86_64', exe]);
|
||||
expect(result2).toContain('x64');
|
||||
};
|
||||
|
||||
export const toSystemIndependentPath = (s: string): string => {
|
||||
return path.sep === '/' ? s : s.replace(/\\/g, '/');
|
||||
};
|
||||
|
||||
export const removeUnstableProperties = (data: any) => {
|
||||
return JSON.parse(
|
||||
JSON.stringify(data, (name, value) => {
|
||||
if (name === 'offset') {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Directory structure:
|
||||
* testName
|
||||
* ├── private
|
||||
* │ └── var
|
||||
* │ ├── app
|
||||
* │ │ └── file.txt -> ../file.txt
|
||||
* │ └── file.txt
|
||||
* └── var -> private/var
|
||||
* ├── index.js
|
||||
* ├── package.json
|
||||
*/
|
||||
export const createTestApp = async (
|
||||
testName: string | undefined,
|
||||
additionalFiles: Record<string, string> = {},
|
||||
) => {
|
||||
const outDir = (testName || 'app') + Math.floor(Math.random() * 100); // tests run in parallel, randomize dir suffix to prevent naming collisions
|
||||
const testPath = path.join(appsDir, outDir);
|
||||
await fs.remove(testPath);
|
||||
|
||||
await fs.copy(path.join(asarsDir, 'app'), testPath);
|
||||
|
||||
const privateVarPath = path.join(testPath, 'private', 'var');
|
||||
const varPath = path.join(testPath, 'var');
|
||||
|
||||
await fs.mkdir(privateVarPath, { recursive: true });
|
||||
await fs.symlink(path.relative(testPath, privateVarPath), varPath);
|
||||
|
||||
const files = {
|
||||
'file.txt': 'hello world',
|
||||
...additionalFiles,
|
||||
};
|
||||
for await (const [filename, fileData] of Object.entries(files)) {
|
||||
const originFilePath = path.join(varPath, filename);
|
||||
await fs.writeFile(originFilePath, fileData);
|
||||
}
|
||||
const appPath = path.join(varPath, 'app');
|
||||
await fs.mkdirp(appPath);
|
||||
await fs.symlink('../file.txt', path.join(appPath, 'file.txt'));
|
||||
|
||||
return {
|
||||
testPath,
|
||||
varPath,
|
||||
appPath,
|
||||
};
|
||||
};
|
||||
|
||||
export const templateApp = async (
|
||||
name: string,
|
||||
arch: string,
|
||||
modify: (appPath: string) => Promise<void>,
|
||||
) => {
|
||||
const electronZip = await downloadArtifact({
|
||||
artifactName: 'electron',
|
||||
version: '27.0.0',
|
||||
platform: 'darwin',
|
||||
arch,
|
||||
});
|
||||
const appPath = path.resolve(appsDir, name);
|
||||
zip.unzipSync(electronZip, appsDir);
|
||||
await fs.rename(path.resolve(appsDir, 'Electron.app'), appPath);
|
||||
await fs.remove(path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar'));
|
||||
await modify(appPath);
|
||||
|
||||
return appPath;
|
||||
};
|
||||
11
tsconfig.jest.json
Normal file
11
tsconfig.jest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"outDir": "dist/esm",
|
||||
"types": [
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
6
typedoc.json
Normal file
6
typedoc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPoints": ["./src/index.ts"],
|
||||
"excludeInternal": true,
|
||||
"sort": ["source-order"]
|
||||
}
|
||||
Reference in New Issue
Block a user