Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/file-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
throw e;
}
}
if (p.includes('app.asar')) {
if (p.endsWith('.asar')) {
Copy link

@valeriangalliat valeriangalliat Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BlackHole1 this changed the way some resources are identified i.e. before:

  {
    "relativePath": "Contents/Resources/app.asar",
    "type": 4
  },
  {
    "relativePath": "Contents/Resources/app.asar.unpacked/node_modules/fsevents/fsevents.node",
    "type": 4
  },

And after:

  {
    "relativePath": "Contents/Resources/app.asar",
    "type": 4
  },
  {
    "relativePath": "Contents/Resources/app.asar.unpacked/node_modules/fsevents/fsevents.node",
    "type": 0
  },

(4 = APP_CODE and 0 = MACHO)

I'm not sure if that's intentional or not but in the second case, it's considering fsevents.node as MACHO instead of APP_CODE and it leads into running this check here for this file where it wouldn't hit it before, leading to this error for Electron apps that includes the fsevents package and other packages with universal .node binaries:

Error: Detected file "Contents/Resources/app.asar.unpacked/node_modules/fsevents/fsevents.node" that's the same in both x64 and arm64 builds and not covered by the x64ArchFiles rule

Do you think in the above example app.asar.unpacked/.../fsevents.node should be identified as APP_CODE or MACHO? Trying to figure out what a fix for this would look like

If it should stay APP_CODE, maybe doing if (p.includes('.asar')) is a good enough fix..., or maybe p.endsWith('.asar') || p.includes('.asar.unpacked/') to be stricter

Edit: made a PR #133 with a tentative fix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree that all contents in unpacked folder should be APP_CODE, as .node is a macho file. I think your issue being reported here:

other packages with universal .node binaries:

Should be resolved through #126 though

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, even better! I'll watch that PR and pin 2.0.1 in the meantime then, thanks for the pointer 👌

fileType = AppFileType.APP_CODE;
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
fileType = AppFileType.MACHO;
Expand Down
18 changes: 4 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import * as plist from 'plist';
import * as dircompare from 'dir-compare';

import { AppFile, AppFileType, getAllAppFiles } from './file-utils';
import { AsarMode, detectAsarMode, generateAsarIntegrity, mergeASARs } from './asar-utils';
import { AsarMode, detectAsarMode, mergeASARs } from './asar-utils';
import { sha } from './sha';
import { d } from './debug';
import { computeIntegrityData } from './integrity';

/**
* Options to pass into the {@link makeUniversalApp} function.
Expand Down Expand Up @@ -251,9 +252,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
}
}

const generatedIntegrity: Record<string, { algorithm: 'SHA256'; hash: string }> = {};
let didSplitAsar = false;

/**
* If we have an ASAR we just need to check if the two "app.asar" files have the same hash,
* if they are, same as above, we can leave one there and call it a day. If they're different
Expand All @@ -271,8 +269,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
outputAsarPath: output,
singleArchFiles: opts.singleArchFiles,
});

generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(output);
} else if (x64AsarMode === AsarMode.HAS_ASAR) {
d('checking if the x64 and arm64 asars are identical');
const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
Expand All @@ -281,7 +277,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
);

if (x64AsarSha !== arm64AsarSha) {
didSplitAsar = true;
d('x64 and arm64 asars are different');
const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar');
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath);
Expand Down Expand Up @@ -329,18 +324,13 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar');
await asar.createPackage(entryAsar, asarPath);

generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(asarPath);
generatedIntegrity['Resources/app-x64.asar'] = generateAsarIntegrity(x64AsarPath);
generatedIntegrity['Resources/app-arm64.asar'] = generateAsarIntegrity(arm64AsarPath);
} else {
d('x64 and arm64 asars are the same');
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
);
}
}

const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'));

const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST);
for (const plistFile of plistFiles) {
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
Expand Down
51 changes: 51 additions & 0 deletions src/integrity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as fs from 'fs-extra';
import path from 'path';
import { AppFileType, getAllAppFiles } from './file-utils';
import { sha } from './sha';
import { generateAsarIntegrity } from './asar-utils';

type IntegrityMap = {
[filepath: string]: string;
};

export interface HeaderHash {
algorithm: 'SHA256';
hash: string;
}

export interface AsarIntegrity {
[key: string]: HeaderHash;
}

export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
const root = await fs.realpath(contentsPath);

const resourcesRelativePath = 'Resources';
const resourcesPath = path.resolve(root, resourcesRelativePath);

const resources = await getAllAppFiles(resourcesPath);
const resourceAsars = resources
.filter((file) => file.type === AppFileType.APP_CODE)
.reduce<IntegrityMap>(
(prev, file) => ({
...prev,
[path.join(resourcesRelativePath, file.relativePath)]: path.join(
resourcesPath,
file.relativePath,
),
}),
{},
);

// sort to produce constant result
const allAsars = Object.entries(resourceAsars).sort(([name1], [name2]) =>
name1.localeCompare(name2),
);
const hashes = await Promise.all(allAsars.map(async ([, from]) => generateAsarIntegrity(from)));
const asarIntegrity: AsarIntegrity = {};
for (let i = 0; i < allAsars.length; i++) {
const [asar] = allAsars[i];
asarIntegrity[asar] = hashes[i];
}
return asarIntegrity;
}
138 changes: 137 additions & 1 deletion test/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,137 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
}
`;

exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 1`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
],
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
},
"size": 66,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
],
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
},
"size": 41,
},
"private": {
"files": {
"var": {
"files": {
"app": {
"files": {
"file.txt": {
"link": "private/var/file.txt",
},
},
},
"file.txt": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
],
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
"size": 11,
},
},
},
},
},
"var": {
"link": "private/var",
},
},
}
`;

exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 2`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
],
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
},
"size": 66,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
],
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
},
"size": 41,
},
"private": {
"files": {
"var": {
"files": {
"app": {
"files": {
"file.txt": {
"link": "private/var/file.txt",
},
},
},
"file.txt": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
],
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
"size": 11,
},
},
},
},
},
"var": {
"link": "private/var",
},
},
}
`;

exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 3`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
},
"Resources/webbapp.asar": {
"algorithm": "SHA256",
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
},
},
}
`;

exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 1`] = `
{
"files": {
Expand Down Expand Up @@ -581,6 +712,11 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 4`]

exports[`makeUniversalApp no asar mode should shim two different app folders 5`] = `
{
"Contents/Info.plist": {},
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "27433ee3e34b3b0dabb29d18d40646126e80c56dbce8c4bb2adef7278b5a46c0",
},
},
}
`;
44 changes: 43 additions & 1 deletion test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ describe('makeUniversalApp', () => {
VERIFY_APP_TIMEOUT,
);

it(
// TODO: Investigate if this should even be allowed.
// Current logic detects all unpacked files as APP_CODE, which doesn't seem correct since it could also be a macho file requiring lipo
// https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49
it.skip(
'should shim asars with different unpacked dirs',
async () => {
const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
Expand Down Expand Up @@ -191,6 +194,45 @@ describe('makeUniversalApp', () => {
},
VERIFY_APP_TIMEOUT,
);

it(
'should generate AsarIntegrity for all asars in the application',
async () => {
const { testPath } = await createTestApp('app-2');
const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
await createPackage(testPath, testAsarPath);

const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => {
await fs.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
await fs.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'),
);
});
const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => {
await fs.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
await fs.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'),
);
});
const outAppPath = path.resolve(appsOutPath, 'MultipleAsars.app');
await makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath,
mergeASARs: true,
});
await verifyApp(outAppPath);
},
VERIFY_APP_TIMEOUT,
);
});

describe('no asar mode', () => {
Expand Down
1 change: 1 addition & 0 deletions test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const VERIFY_APP_TIMEOUT = 80 * 1000;

export const asarsDir = path.resolve(__dirname, 'fixtures', 'asars');
export const appsDir = path.resolve(__dirname, 'fixtures', 'apps');
export const appsOutPath = path.resolve(appsDir, 'out');

export const verifyApp = async (appPath: string) => {
await ensureUniversal(appPath);
Expand Down