Initial Commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.app
|
||||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"parser": "typescript"
|
||||||
|
}
|
||||||
9
.releaserc.json
Normal file
9
.releaserc.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@continuous-auth/semantic-release-npm",
|
||||||
|
"@semantic-release/github"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
12
README.md
Normal file
12
README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# @electron/universal
|
||||||
|
|
||||||
|
> Create universal macOS Electron applicatiojns
|
||||||
|
|
||||||
|
[](https://circleci.com/gh/electron/universal)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { makeUniversalApp } from '@electron/universal';
|
||||||
|
```
|
||||||
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "@electron/universal",
|
||||||
|
"version": "0.0.0-development",
|
||||||
|
"description": "Utility for creating Universal macOS applications from two x64 and arm64 Electron applications",
|
||||||
|
"main": "dist/cjs/index.js",
|
||||||
|
"module": "dist/esm/index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"electron",
|
||||||
|
"apple silicon",
|
||||||
|
"universal"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"author": "Samuel Attard",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && tsc -p tsconfig.esm.json",
|
||||||
|
"lint": "prettier --check \"src/**/*.ts\"",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@continuous-auth/semantic-release-npm": "^2.0.0",
|
||||||
|
"@types/fs-extra": "^9.0.2",
|
||||||
|
"@types/node": "^14.11.10",
|
||||||
|
"husky": "^4.3.0",
|
||||||
|
"lint-staged": "^10.4.2",
|
||||||
|
"prettier": "^2.1.2",
|
||||||
|
"semantic-release": "^17.2.1",
|
||||||
|
"typescript": "^4.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@malept/cross-spawn-promise": "^1.1.0",
|
||||||
|
"asar": "^3.0.3",
|
||||||
|
"fs-extra": "^9.0.1",
|
||||||
|
"macho": "^1.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
171
src/index.ts
Normal file
171
src/index.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { spawn } from '@malept/cross-spawn-promise';
|
||||||
|
import * as asar from 'asar';
|
||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const MACHO_PREFIX = 'Mach-O ';
|
||||||
|
const macho = require('macho');
|
||||||
|
|
||||||
|
const machoParse = async (
|
||||||
|
p: string,
|
||||||
|
): Promise<{
|
||||||
|
bits: number;
|
||||||
|
cpu: {
|
||||||
|
type: 'x86_64' | 'arm64';
|
||||||
|
subtype: string;
|
||||||
|
};
|
||||||
|
}> => {
|
||||||
|
return macho.parse(await fs.readFile(p));
|
||||||
|
};
|
||||||
|
|
||||||
|
type MakeUniversalOpts = {
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
arm64AppPath: string;
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
outAppPath: string;
|
||||||
|
/**
|
||||||
|
* Forcefully overwrite any existing files that are in the way of generating the universal application
|
||||||
|
*/
|
||||||
|
force: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum AsarMode {
|
||||||
|
NO_ASAR,
|
||||||
|
PURE_ASAR_EMBEDDED_NATIVE_MODULES,
|
||||||
|
PURE_ASAR_UNPACKED_NATIVE_MODULES,
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectAsarMode = async (appPath: string) => {
|
||||||
|
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
|
||||||
|
const asarUnpackedPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar.unpacked');
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(asarPath))) return AsarMode.NO_ASAR;
|
||||||
|
const nativeContents = asar.listPackage(asarPath).filter((p) => p.endsWith('.node'));
|
||||||
|
for (const nativeModule of nativeContents) {
|
||||||
|
if (!(await fs.pathExists(path.resolve(asarUnpackedPath, nativeModule.substr(1)))))
|
||||||
|
return AsarMode.PURE_ASAR_EMBEDDED_NATIVE_MODULES;
|
||||||
|
}
|
||||||
|
return AsarMode.PURE_ASAR_UNPACKED_NATIVE_MODULES;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllMachOFiles = async (appPath: string) => {
|
||||||
|
const machoOFiles: string[] = [];
|
||||||
|
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const traverse = async (p: string) => {
|
||||||
|
p = await fs.realpath(p);
|
||||||
|
if (visited.has(p)) return;
|
||||||
|
visited.add(p);
|
||||||
|
|
||||||
|
const info = await fs.stat(p);
|
||||||
|
if (info.isSymbolicLink()) return;
|
||||||
|
if (info.isFile()) {
|
||||||
|
const fileOutput = await spawn('file', ['--brief', '--no-pad', p]);
|
||||||
|
if (fileOutput.startsWith(MACHO_PREFIX)) {
|
||||||
|
machoOFiles.push(path.relative(appPath, p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.isDirectory()) {
|
||||||
|
for (const child of await fs.readdir(p)) {
|
||||||
|
await traverse(path.resolve(p, child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await traverse(appPath);
|
||||||
|
|
||||||
|
return machoOFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
|
||||||
|
if (process.platform !== 'darwin')
|
||||||
|
throw new Error('@electron/universal is only supported on darwin platforms');
|
||||||
|
if (!opts.x64AppPath || !path.isAbsolute(opts.x64AppPath))
|
||||||
|
throw new Error('Expected opts.x64AppPath to be an absolute path but it was not');
|
||||||
|
if (!opts.arm64AppPath || !path.isAbsolute(opts.arm64AppPath))
|
||||||
|
throw new Error('Expected opts.arm64AppPath to be an absolute path but it was not');
|
||||||
|
if (!opts.outAppPath || !path.isAbsolute(opts.outAppPath))
|
||||||
|
throw new Error('Expected opts.outAppPath to be an absolute path but it was not');
|
||||||
|
|
||||||
|
if (await fs.pathExists(opts.outAppPath)) {
|
||||||
|
if (!opts.force) {
|
||||||
|
throw new Error(
|
||||||
|
`The out path "${opts.outAppPath}" already exists and force is not set to true`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await fs.remove(opts.outAppPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const x64AsarMode = await detectAsarMode(opts.x64AppPath);
|
||||||
|
const arm64AsarMode = await detectAsarMode(opts.arm64AppPath);
|
||||||
|
|
||||||
|
if (x64AsarMode !== arm64AsarMode)
|
||||||
|
throw new Error(
|
||||||
|
'Both the x64 and arm64 versions of your application need to have been built with the same asar settings (enabled vs disabled)',
|
||||||
|
);
|
||||||
|
if (x64AsarMode === AsarMode.PURE_ASAR_EMBEDDED_NATIVE_MODULES)
|
||||||
|
throw new Error(
|
||||||
|
'@electron/universal does not currently support apps that contain native modules in ASAR files. Please use asar.unpacked',
|
||||||
|
);
|
||||||
|
if (arm64AsarMode === AsarMode.PURE_ASAR_EMBEDDED_NATIVE_MODULES)
|
||||||
|
throw new Error(
|
||||||
|
'@electron/universal does not currently support apps that contain native modules in ASAR files. Please use asar.unpacked',
|
||||||
|
);
|
||||||
|
|
||||||
|
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tmpApp = path.resolve(tmpDir, 'Tmp.app');
|
||||||
|
await spawn('cp', ['-R', opts.x64AppPath, tmpApp]);
|
||||||
|
|
||||||
|
const uniqueToX64: string[] = [];
|
||||||
|
const uniqueToArm64: string[] = [];
|
||||||
|
const x64MachOFiles = await getAllMachOFiles(await fs.realpath(tmpApp));
|
||||||
|
const arm64MachoOFiles = await getAllMachOFiles(opts.arm64AppPath);
|
||||||
|
|
||||||
|
for (const file of x64MachOFiles) {
|
||||||
|
if (!arm64MachoOFiles.includes(file)) uniqueToX64.push(file);
|
||||||
|
}
|
||||||
|
for (const file of arm64MachoOFiles) {
|
||||||
|
if (!x64MachOFiles.includes(file)) uniqueToArm64.push(file);
|
||||||
|
}
|
||||||
|
if (uniqueToX64.length !== 0 || uniqueToArm64.length !== 0) {
|
||||||
|
console.error({
|
||||||
|
uniqueToX64,
|
||||||
|
uniqueToArm64,
|
||||||
|
});
|
||||||
|
throw new Error(
|
||||||
|
'While trying to merge mach-o files across your apps we found a mismatch, the number of mach-o files is not the same between the arm64 and x64 builds',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(x64MachOFiles);
|
||||||
|
for (const machOFile of x64MachOFiles) {
|
||||||
|
await spawn('lipo', [
|
||||||
|
await fs.realpath(path.resolve(tmpApp, machOFile)),
|
||||||
|
await fs.realpath(path.resolve(opts.arm64AppPath, machOFile)),
|
||||||
|
'-create',
|
||||||
|
'-output',
|
||||||
|
await fs.realpath(path.resolve(tmpApp, machOFile)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await spawn('mv', [tmpApp, opts.outAppPath]);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
await fs.remove(tmpDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
7
tsconfig.esm.json
Normal file
7
tsconfig.esm.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"outDir": "dist/esm"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2017",
|
||||||
|
"lib": [
|
||||||
|
"es2017"
|
||||||
|
],
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "dist/cjs",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
],
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user