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