Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
MarshallOfSound committed Oct 21, 2020
0 parents commit 163f495
Show file tree
Hide file tree
Showing 9 changed files with 5,363 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
*.app
7 changes: 7 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "all",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"parser": "typescript"
}
9 changes: 9 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@continuous-auth/semantic-release-npm",
"@semantic-release/github"
]
}

12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @electron/universal

> Create universal macOS Electron applicatiojns
[![CircleCI](https://circleci.com/gh/electron/universal.svg?style=svg)](https://circleci.com/gh/electron/universal)

## Usage


```typescript
import { makeUniversalApp } from '@electron/universal';
```
42 changes: 42 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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 changes: 171 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm"
}
}
21 changes: 21 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
Loading

0 comments on commit 163f495

Please sign in to comment.