forked from electron/universal
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 163f495
Showing
9 changed files
with
5,363 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
dist | ||
*.app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"trailingComma": "all", | ||
"tabWidth": 2, | ||
"singleQuote": true, | ||
"printWidth": 100, | ||
"parser": "typescript" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# @electron/universal | ||
|
||
> Create universal macOS Electron applicatiojns | ||
[](https://circleci.com/gh/electron/universal) | ||
|
||
## Usage | ||
|
||
|
||
```typescript | ||
import { makeUniversalApp } from '@electron/universal'; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"extends": "./tsconfig.json", | ||
"compilerOptions": { | ||
"module": "esnext", | ||
"outDir": "dist/esm" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
Oops, something went wrong.