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
11 changes: 11 additions & 0 deletions .github/workflows/_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ jobs:
fi
for app in "${apps[@]}"; do
codesign --verify --deep --strict --verbose=4 "$app"
main_executable="$app/Contents/MacOS/$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Contents/Info.plist")"
expected_arch="$(lipo -archs "$main_executable")"
if [[ "$expected_arch" == *arm64* ]]; then
while IFS= read -r -d '' binary; do
binary_arches="$(lipo -archs "$binary" 2>/dev/null || true)"
if [[ -n "$binary_arches" && "$binary_arches" != *arm64* ]]; then
echo "::error file=$binary::Apple-silicon app contains a Mach-O binary without an arm64 slice: $binary_arches"
exit 1
fi
done < <(find "$app" -type f -print0)
fi
done
shell: bash

Expand Down
70 changes: 59 additions & 11 deletions build/after-pack.cjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
// electron-builder afterPack hook.
//
// node-pty ships a `spawn-helper` binary that posix_spawn invokes to set up
// the pty. electron-builder's asar-unpack copy can strip the execute bit,
// which makes posix_spawnp fail at runtime with the opaque
// "posix_spawnp failed." error. Restore +x on every prebuild we ship.
// the pty. Keep only the target platform/architecture prebuild in each thin
// package, then restore +x because electron-builder's asar-unpack copy can
// strip the execute bit and make posix_spawnp fail with the opaque
// "posix_spawnp failed." error.

const { existsSync, statSync, chmodSync, readdirSync, mkdirSync, cpSync } = require("node:fs");
const {
existsSync,
statSync,
chmodSync,
readdirSync,
mkdirSync,
cpSync,
rmSync,
} = require("node:fs");
const { join, resolve } = require("node:path");

// electron-builder's Arch enum is numeric: ia32=0, x64=1, armv7l=2, arm64=3,
Expand Down Expand Up @@ -46,6 +55,45 @@ function lookupAsarEntry(header, segments) {
return node;
}

function platformTag(electronPlatformName) {
if (electronPlatformName === "darwin" || electronPlatformName === "mas") return "darwin";
if (electronPlatformName === "win32") return "win32";
return "linux";
}

// npm installs every node-pty prebuild from its package tarball. electron-builder
// preserves all of them under app.asar.unpacked, including the Intel-only macOS
// spawn-helper in an arm64 app. macOS treats that nested helper as an Intel-based
// component and displays its end-of-support warning even though Poracode itself
// and the helper it actually loads are arm64. Remove all foreign prebuilds from
// each thin package before signing. Older better-sqlite3 packages used a similar
// bin/<platform>-<arch>-<abi> layout, so prune those defensively as well.
function pruneForeignNativePrebuilds(resourcesDir, electronPlatformName, archName) {
const expectedPrefix = `${platformTag(electronPlatformName)}-${archName}`;
const roots = [
{
path: join(resourcesDir, "app.asar.unpacked", "node_modules", "node-pty", "prebuilds"),
keep: (entry) => entry === expectedPrefix,
},
{
path: join(resourcesDir, "app.asar.unpacked", "node_modules", "better-sqlite3", "bin"),
keep: (entry) => entry === expectedPrefix || entry.startsWith(`${expectedPrefix}-`),
},
];

const removed = [];
for (const root of roots) {
if (!existsSync(root.path)) continue;
for (const entry of readdirSync(root.path)) {
if (root.keep(entry)) continue;
const foreignPath = join(root.path, entry);
rmSync(foreignPath, { recursive: true, force: true });
removed.push(foreignPath);
}
}
return removed;
}

// The packaging script (build-desktop-artifact.mjs) rebuilds better-sqlite3 for
// every target arch and stages each under <stageRoot>/native/<arch>/. A single
// multi-arch electron-builder pass packs only one of those binaries into BOTH
Expand Down Expand Up @@ -93,12 +141,7 @@ function assertNativeBinaries(resourcesDir, electronPlatformName, arch) {
if (!archName) {
throw new Error(`[afterPack] FATAL: unknown electron-builder Arch enum value ${arch}`);
}
const platTag =
electronPlatformName === "darwin" || electronPlatformName === "mas"
? "darwin"
: electronPlatformName === "win32"
? "win32"
: "linux";
const platTag = platformTag(electronPlatformName);

const unpacked = join(resourcesDir, "app.asar.unpacked", "node_modules");

Expand Down Expand Up @@ -191,7 +234,12 @@ module.exports = async function afterPack(context) {
}
// Replace the (possibly off-host-arch) binary electron-builder packed with the
// arch-correct one staged per target arch.
injectBetterSqliteBinary(context, resourcesDir, ARCH_NAME[context.arch]);
const archName = ARCH_NAME[context.arch];
injectBetterSqliteBinary(context, resourcesDir, archName);
const removed = pruneForeignNativePrebuilds(resourcesDir, context.electronPlatformName, archName);
for (const path of removed) {
console.log(`[afterPack] pruned foreign native prebuild ${path}`);
}
// Throws if a required native binary is missing or mis-packed, so a broken
// app can never be packaged or published.
assertNativeBinaries(resourcesDir, context.electronPlatformName, context.arch);
Expand Down