fix(desktop): exclude dist/** from multi-arch package contents

Merging after independent reviews confirmed the fix and all CI checks passed.
This commit is contained in:
Multica Eve
2026-07-20 12:58:14 +08:00
committed by GitHub
parent 1483ce0825
commit 612b2db3b9
3 changed files with 73 additions and 1 deletions

View File

@@ -8,6 +8,20 @@ files:
- "!electron.vite.config.*"
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
- "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}"
# Never repack the electron-builder output directory into the app.
# Multi-arch release builds (scripts/package.mjs) run each target in the
# same apps/desktop dir with per-arch output at dist/<platform>-<arch>.
# electron-builder only auto-excludes the *current* target's output dir,
# so the earlier arch's dist/ (its full .app + DMG + ZIP) would otherwise
# be pulled into the next arch's app.asar. That inflated v0.4.3/v0.4.4's
# Intel (x64) DMG until the HFS volume was full and the real
# `Electron Framework/Versions/A/Electron Framework` binary was dropped,
# so Intel Macs crashed on launch with a dyld missing-library error
# (github.com/multica-ai/multica/issues/5595). Excluding all of dist/**
# fixes it regardless of build order; a pre-build clean alone does not,
# because the first arch writes into dist/ mid-run before the second is
# packaged.
- "!dist/**"
protocols:
- name: Multica
schemes:

View File

@@ -27,6 +27,7 @@
// real `git describe` invocation against a throwaway repo.
import { execFileSync, spawnSync } from "node:child_process";
import { rmSync } from "node:fs";
import { delimiter, dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -365,6 +366,17 @@ function main() {
`[package] build matrix → ${buildMatrix.map(formatTarget).join(", ")}`,
);
// Step 0: start every release from an empty output directory. Stale
// artifacts from a prior run would otherwise be repacked into this run's
// app.asar (see the `!dist/**` note in electron-builder.yml). This clean
// is belt-and-braces only — it does NOT by itself prevent the same-run
// cross-arch contamination that broke the Intel DMG, because the first
// arch writes into dist/ mid-run before the next arch is packaged; the
// `!dist/**` files exclusion is what actually guarantees isolation.
const distDir = resolve(desktopRoot, "dist");
rmSync(distDir, { recursive: true, force: true });
console.log(`[package] cleaned output dir → ${distDir}`);
// Step 1: build the Electron main/preload/renderer bundles. Without
// this step electron-builder silently packages whatever is already in
// out/, which on a fresh checkout (or after a partial build) ships an

View File

@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join, resolve } from "node:path";
import { afterEach, describe, it, expect } from "vitest";
@@ -431,3 +431,49 @@ describe("envWithLocalBins", () => {
]);
});
});
describe("electron-builder.yml packaging config", () => {
// Regression guard for github.com/multica-ai/multica/issues/5595. The
// multi-arch release build writes each target's output to
// dist/<platform>-<arch> in the same apps/desktop dir; electron-builder
// only auto-excludes the *current* target's output dir, so without an
// explicit `!dist/**` the earlier arch's dist/ was repacked into the next
// arch's app.asar. That inflated the Intel (x64) DMG until its Electron
// Framework binary was dropped and Intel Macs crashed on launch. Keep the
// exclusion pinned so a future edit to the files list cannot drop it
// unnoticed.
// Resolve electron-builder.yml relative to cwd, tolerating vitest running
// from either the desktop package dir or the repo root — import.meta.url is
// not a file:// URL under the test transform, so avoid fileURLToPath here.
const configPath = [
resolve(process.cwd(), "electron-builder.yml"),
resolve(process.cwd(), "apps/desktop/electron-builder.yml"),
].find((candidate) => existsSync(candidate));
// Extract the entries of the top-level `files:` block sequence without a
// YAML dependency: collect the ` - "…"` items that follow `files:` up to
// the next top-level key. Commented (`#`) lines are ignored, so a
// commented-out exclusion would (correctly) not count.
function readFilesBlock(raw) {
const lines = raw.split("\n");
const start = lines.findIndex((l) => /^files:\s*$/.test(l));
if (start === -1) return [];
const entries = [];
for (let i = start + 1; i < lines.length; i += 1) {
const line = lines[i];
if (/^\S/.test(line)) break; // next top-level key ends the block
const trimmed = line.trim();
if (trimmed === "" || trimmed.startsWith("#")) continue;
const m = trimmed.match(/^-\s*"?(.*?)"?\s*$/);
if (m) entries.push(m[1]);
}
return entries;
}
it("excludes the dist output directory from the packaged files", () => {
expect(configPath, "electron-builder.yml not found").toBeTruthy();
const entries = readFilesBlock(readFileSync(configPath, "utf-8"));
expect(entries.length).toBeGreaterThan(0);
expect(entries).toContain("!dist/**");
});
});