chore: cleanup files and split into modules
This commit is contained in:
10
package.json
10
package.json
@@ -40,5 +40,15 @@
|
|||||||
"asar": "^3.0.3",
|
"asar": "^3.0.3",
|
||||||
"dir-compare": "^2.4.0",
|
"dir-compare": "^2.4.0",
|
||||||
"fs-extra": "^9.0.1"
|
"fs-extra": "^9.0.1"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.ts": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/asar-utils.ts
Normal file
15
src/asar-utils.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export enum AsarMode {
|
||||||
|
NO_ASAR,
|
||||||
|
HAS_ASAR,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const detectAsarMode = async (appPath: string) => {
|
||||||
|
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(asarPath))) return AsarMode.NO_ASAR;
|
||||||
|
|
||||||
|
return AsarMode.HAS_ASAR;
|
||||||
|
};
|
||||||
61
src/file-utils.ts
Normal file
61
src/file-utils.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { spawn } from '@malept/cross-spawn-promise';
|
||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const MACHO_PREFIX = 'Mach-O ';
|
||||||
|
|
||||||
|
export enum AppFileType {
|
||||||
|
MACHO,
|
||||||
|
PLAIN,
|
||||||
|
SNAPSHOT,
|
||||||
|
APP_CODE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppFile = {
|
||||||
|
relativePath: string;
|
||||||
|
type: AppFileType;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param appPath Path to the application
|
||||||
|
*/
|
||||||
|
export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
|
||||||
|
const files: AppFile[] = [];
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
let fileType = AppFileType.PLAIN;
|
||||||
|
|
||||||
|
const fileOutput = await spawn('file', ['--brief', '--no-pad', p]);
|
||||||
|
if (p.includes('app.asar')) {
|
||||||
|
fileType = AppFileType.APP_CODE;
|
||||||
|
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
|
||||||
|
fileType = AppFileType.MACHO;
|
||||||
|
} else if (p.endsWith('.bin')) {
|
||||||
|
fileType = AppFileType.SNAPSHOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({
|
||||||
|
relativePath: path.relative(appPath, p),
|
||||||
|
type: fileType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.isDirectory()) {
|
||||||
|
for (const child of await fs.readdir(p)) {
|
||||||
|
await traverse(path.resolve(p, child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await traverse(appPath);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
183
src/index.ts
183
src/index.ts
@@ -1,12 +1,12 @@
|
|||||||
import { spawn } from '@malept/cross-spawn-promise';
|
import { spawn } from '@malept/cross-spawn-promise';
|
||||||
import * as asar from 'asar';
|
import * as asar from 'asar';
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as dircompare from 'dir-compare';
|
import * as dircompare from 'dir-compare';
|
||||||
|
import { AppFile, AppFileType, getAllAppFiles } from './file-utils';
|
||||||
const MACHO_PREFIX = 'Mach-O ';
|
import { AsarMode, detectAsarMode } from './asar-utils';
|
||||||
|
import { sha } from './sha';
|
||||||
|
|
||||||
type MakeUniversalOpts = {
|
type MakeUniversalOpts = {
|
||||||
/**
|
/**
|
||||||
@@ -29,76 +29,8 @@ type MakeUniversalOpts = {
|
|||||||
force: boolean;
|
force: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AsarMode {
|
const dupedFiles = (files: AppFile[]) =>
|
||||||
NO_ASAR,
|
files.filter((f) => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
|
||||||
HAS_ASAR,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const detectAsarMode = async (appPath: string) => {
|
|
||||||
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(asarPath))) return AsarMode.NO_ASAR;
|
|
||||||
|
|
||||||
return AsarMode.HAS_ASAR;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum AppFileType {
|
|
||||||
MACHO,
|
|
||||||
PLAIN,
|
|
||||||
SNAPSHOT,
|
|
||||||
APP_CODE,
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppFile = {
|
|
||||||
relativePath: string;
|
|
||||||
type: AppFileType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllFiles = async (appPath: string): Promise<AppFile[]> => {
|
|
||||||
const files: AppFile[] = [];
|
|
||||||
|
|
||||||
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()) {
|
|
||||||
let fileType = AppFileType.PLAIN;
|
|
||||||
|
|
||||||
const fileOutput = await spawn('file', ['--brief', '--no-pad', p]);
|
|
||||||
if (p.includes('app.asar')) {
|
|
||||||
fileType = AppFileType.APP_CODE;
|
|
||||||
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
|
|
||||||
fileType = AppFileType.MACHO;
|
|
||||||
} else if (p.endsWith('.bin')) {
|
|
||||||
fileType = AppFileType.SNAPSHOT;
|
|
||||||
}
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
relativePath: path.relative(appPath, p),
|
|
||||||
type: fileType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.isDirectory()) {
|
|
||||||
for (const child of await fs.readdir(p)) {
|
|
||||||
await traverse(path.resolve(p, child));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await traverse(appPath);
|
|
||||||
|
|
||||||
return files;
|
|
||||||
};
|
|
||||||
|
|
||||||
const dupedFiles = (files: AppFile[]) => files.filter(f => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE);
|
|
||||||
|
|
||||||
const sha = async (filePath: string) => {
|
|
||||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
|
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
|
||||||
if (process.platform !== 'darwin')
|
if (process.platform !== 'darwin')
|
||||||
@@ -136,14 +68,16 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
|||||||
|
|
||||||
const uniqueToX64: string[] = [];
|
const uniqueToX64: string[] = [];
|
||||||
const uniqueToArm64: string[] = [];
|
const uniqueToArm64: string[] = [];
|
||||||
const x64Files = await getAllFiles(await fs.realpath(tmpApp));
|
const x64Files = await getAllAppFiles(await fs.realpath(tmpApp));
|
||||||
const arm64Files = await getAllFiles(opts.arm64AppPath);
|
const arm64Files = await getAllAppFiles(opts.arm64AppPath);
|
||||||
|
|
||||||
for (const file of dupedFiles(x64Files)) {
|
for (const file of dupedFiles(x64Files)) {
|
||||||
if (!arm64Files.some(f => f.relativePath === file.relativePath)) uniqueToX64.push(file.relativePath);
|
if (!arm64Files.some((f) => f.relativePath === file.relativePath))
|
||||||
|
uniqueToX64.push(file.relativePath);
|
||||||
}
|
}
|
||||||
for (const file of dupedFiles(arm64Files)) {
|
for (const file of dupedFiles(arm64Files)) {
|
||||||
if (!x64Files.some(f => f.relativePath === file.relativePath)) uniqueToArm64.push(file.relativePath);
|
if (!x64Files.some((f) => f.relativePath === file.relativePath))
|
||||||
|
uniqueToArm64.push(file.relativePath);
|
||||||
}
|
}
|
||||||
if (uniqueToX64.length !== 0 || uniqueToArm64.length !== 0) {
|
if (uniqueToX64.length !== 0 || uniqueToArm64.length !== 0) {
|
||||||
console.error({
|
console.error({
|
||||||
@@ -155,16 +89,18 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of x64Files.filter(f => f.type === AppFileType.PLAIN)) {
|
for (const file of x64Files.filter((f) => f.type === AppFileType.PLAIN)) {
|
||||||
const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
|
const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
|
||||||
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
|
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
|
||||||
if (x64Sha !== arm64Sha) {
|
if (x64Sha !== arm64Sha) {
|
||||||
console.error(`${x64Sha} !== ${arm64Sha}`);
|
console.error(`${x64Sha} !== ${arm64Sha}`);
|
||||||
throw new Error(`Expected all non-binary files to have identical SHAs when creating a universal build but "${file.relativePath}" did not`);
|
throw new Error(
|
||||||
|
`Expected all non-binary files to have identical SHAs when creating a universal build but "${file.relativePath}" did not`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const machOFile of x64Files.filter(f => f.type === AppFileType.MACHO)) {
|
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) {
|
||||||
await spawn('lipo', [
|
await spawn('lipo', [
|
||||||
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
|
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
|
||||||
await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath)),
|
await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath)),
|
||||||
@@ -175,51 +111,104 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (x64AsarMode === AsarMode.NO_ASAR) {
|
if (x64AsarMode === AsarMode.NO_ASAR) {
|
||||||
const comparison = dircompare.compareSync(path.resolve(tmpApp, 'Contents', 'Resources', 'app'), path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'), { compareSize: true, compareContent: true });
|
const comparison = dircompare.compareSync(
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app'),
|
||||||
|
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
|
||||||
|
{ compareSize: true, compareContent: true },
|
||||||
|
);
|
||||||
|
|
||||||
if (!comparison.same) {
|
if (!comparison.same) {
|
||||||
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64'));
|
await fs.move(
|
||||||
await fs.copy(path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'));
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app'),
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64'),
|
||||||
|
);
|
||||||
|
await fs.copy(
|
||||||
|
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'),
|
||||||
|
);
|
||||||
|
|
||||||
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
||||||
await fs.mkdir(entryAsar);
|
await fs.mkdir(entryAsar);
|
||||||
await fs.copy(path.resolve(__dirname, '..', '..', 'entry-asar', 'no-asar.js'), path.resolve(entryAsar, 'index.js'));
|
await fs.copy(
|
||||||
let pj = await fs.readJson(path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'));
|
path.resolve(__dirname, '..', '..', 'entry-asar', 'no-asar.js'),
|
||||||
|
path.resolve(entryAsar, 'index.js'),
|
||||||
|
);
|
||||||
|
let pj = await fs.readJson(
|
||||||
|
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
|
||||||
|
);
|
||||||
pj.main = 'index.js';
|
pj.main = 'index.js';
|
||||||
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
||||||
await asar.createPackage(entryAsar, path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
|
await asar.createPackage(
|
||||||
|
entryAsar,
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (x64AsarMode === AsarMode.HAS_ASAR) {
|
if (x64AsarMode === AsarMode.HAS_ASAR) {
|
||||||
const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
|
const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
|
||||||
const arm64AsarSha = await sha(path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'));
|
const arm64AsarSha = await sha(
|
||||||
|
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
);
|
||||||
|
|
||||||
if (x64AsarSha !== arm64AsarSha) {
|
if (x64AsarSha !== arm64AsarSha) {
|
||||||
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar'));
|
await fs.move(
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar'),
|
||||||
|
);
|
||||||
const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked');
|
const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked');
|
||||||
if (await fs.pathExists(x64Unpacked)) {
|
if (await fs.pathExists(x64Unpacked)) {
|
||||||
await fs.move(x64Unpacked, path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'));
|
await fs.move(
|
||||||
|
x64Unpacked,
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.copy(path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar'));
|
await fs.copy(
|
||||||
const arm64Unpacked = path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar.unpacked');
|
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar'),
|
||||||
|
);
|
||||||
|
const arm64Unpacked = path.resolve(
|
||||||
|
opts.arm64AppPath,
|
||||||
|
'Contents',
|
||||||
|
'Resources',
|
||||||
|
'app.asar.unpacked',
|
||||||
|
);
|
||||||
if (await fs.pathExists(arm64Unpacked)) {
|
if (await fs.pathExists(arm64Unpacked)) {
|
||||||
await fs.copy(arm64Unpacked, path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'));
|
await fs.copy(
|
||||||
|
arm64Unpacked,
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
const entryAsar = path.resolve(tmpDir, 'entry-asar');
|
||||||
await fs.mkdir(entryAsar);
|
await fs.mkdir(entryAsar);
|
||||||
await fs.copy(path.resolve(__dirname, '..', '..', 'entry-asar', 'has-asar.js'), path.resolve(entryAsar, 'index.js'));
|
await fs.copy(
|
||||||
let pj = JSON.parse((await asar.extractFile(path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app.asar'), 'package.json')).toString('utf8'));
|
path.resolve(__dirname, '..', '..', 'entry-asar', 'has-asar.js'),
|
||||||
|
path.resolve(entryAsar, 'index.js'),
|
||||||
|
);
|
||||||
|
let pj = JSON.parse(
|
||||||
|
(
|
||||||
|
await asar.extractFile(
|
||||||
|
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
'package.json',
|
||||||
|
)
|
||||||
|
).toString('utf8'),
|
||||||
|
);
|
||||||
pj.main = 'index.js';
|
pj.main = 'index.js';
|
||||||
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
|
||||||
await asar.createPackage(entryAsar, path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
|
await asar.createPackage(
|
||||||
|
entryAsar,
|
||||||
|
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const snapshotsFile of arm64Files.filter(f => f.type === AppFileType.SNAPSHOT)) {
|
for (const snapshotsFile of arm64Files.filter((f) => f.type === AppFileType.SNAPSHOT)) {
|
||||||
await fs.copy(path.resolve(opts.arm64AppPath, snapshotsFile.relativePath), path.resolve(tmpApp, snapshotsFile.relativePath));
|
await fs.copy(
|
||||||
|
path.resolve(opts.arm64AppPath, snapshotsFile.relativePath),
|
||||||
|
path.resolve(tmpApp, snapshotsFile.relativePath),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await spawn('mv', [tmpApp, opts.outAppPath]);
|
await spawn('mv', [tmpApp, opts.outAppPath]);
|
||||||
|
|||||||
13
src/sha.ts
Normal file
13
src/sha.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
export const sha = async (filePath: string) => {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
fileStream.pipe(hash);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
fileStream.on('end', () => resolve());
|
||||||
|
fileStream.on('error', (err) => reject(err));
|
||||||
|
});
|
||||||
|
return hash.digest('hex');
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user