From 4bdf5348a4d0758d686c6575f874fe8aaccbd47d Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Fri, 31 Jul 2026 16:48:24 +0800 Subject: [PATCH 1/4] fix(release): install both CPU keyring bindings --- pnpm-workspace.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 39d5101e1..7bf55e878 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,13 @@ packages: nodeLinker: hoisted autoInstallPeers: true strictPeerDependencies: false +# Desktop releases cross-pack both CPU architectures and need both native optional dependencies. +supportedArchitectures: + os: + - current + cpu: + - x64 + - arm64 # Turbo runs each package's script via `pnpm run`, which otherwise triggers a per-package deps check. # With hoisted layout, dependency-less packages (schema/ipc) have no local node_modules, so that check # falsely reports "node_modules missing". Turbo already orders/builds deps, so disable the per-run check. From 620970c963a6970b12cb053406b3525b484777fd Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Fri, 31 Jul 2026 18:10:34 +0800 Subject: [PATCH 2/4] fix(release): stage desktop dependencies per architecture --- .github/workflows/build-desktop.yml | 7 +- apps/desktop/AGENTS.md | 2 +- apps/desktop/package.json | 1 + apps/desktop/scripts/package-app.mts | 108 ++++++++++++------ apps/desktop/scripts/update-feed.mts | 21 ++++ .../desktop/src/__tests__/update-feed.test.ts | 32 ++++++ docs/RELEASE.md | 2 +- pnpm-lock.yaml | 3 + pnpm-workspace.yaml | 7 -- 9 files changed, 133 insertions(+), 50 deletions(-) create mode 100644 apps/desktop/scripts/update-feed.mts create mode 100644 apps/desktop/src/__tests__/update-feed.test.ts diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 4bf7cefa7..57b77e983 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -145,10 +145,9 @@ jobs: - name: Package with electron-builder working-directory: ${{ env.DESKTOP_DIR }} shell: bash # unify quoting across runners for the conditional signing args below - # package-app.mts materializes a single-importer staging dir and points electron-builder's - # --projectDir at it (see the script header): fixes the Windows native-rebuild miss and the - # module-collector drops without the app-builder-lib patch. azureSignOptions is forwarded via - # -c only on signed Windows builds so unsigned builds skip signing instead of failing. + # package-app.mts materializes one CPU-scoped staging dir per target arch and points + # electron-builder's --projectDir at it (see the script header). azureSignOptions is + # forwarded via -c only on signed Windows builds so unsigned builds skip signing. run: | # electron-builder treats a *set but empty* CSC_LINK as a certificate path and dies on # " not a file". Actions cannot conditionally unset an env key, so the cert diff --git a/apps/desktop/AGENTS.md b/apps/desktop/AGENTS.md index d9df87146..49db11663 100644 --- a/apps/desktop/AGENTS.md +++ b/apps/desktop/AGENTS.md @@ -19,7 +19,7 @@ The rules below are desktop-only — the **system plane**. ## Packaging & asar invariants - **`electron-builder.yml` is the packaging config** (YAML, not a `package.json` build key). `node scripts/build.mts` (three plain-Vite builds — `vite.{main,preload,renderer}.config.ts`, in that order; `scripts/dev.mts` is the dev counterpart) compiles into `out/`; electron-builder only wraps `out/**` + `package.json`. `directories.output=release` must equal `OUTPUT_DIR` in `build-desktop.yml`; `buildResources=build-resources` (NOT `build/`, which is a gitignored turbo output). `electronVersion` is hardcoded (electron-builder can't read pnpm's `catalog:` protocol) — the pinned number and its sync rule live in [`docs/RELEASE.md`](../../docs/RELEASE.md). -- **Packaging always goes through `scripts/package-app.mts` (CODE-107), never a bare `electron-builder`.** It `pnpm --prod deploy`s the app's production closure into a self-contained staging dir **outside** the workspace, then runs electron-builder with `--projectDir` pointed there so `appDir === projectDir === workspaceRoot`. This is what makes `@electron/rebuild` find better-sqlite3 (pnpm hoists it to the repo-root `node_modules`, which electron-builder's Windows workspace-root probe can't locate — the pre-CODE-107 daemon shipped an un-rebuilt binding and died at boot on Windows) and makes the asar module collector see one importer instead of the whole monorepo. `package:devshell` uses `electron-builder.devshell.yml` + `--dir`; `package` is the production variant; both run through the script, which redirects `directories.output` back to `release/` and the icons to the shared `assets/`. There is no `dist` script (release packaging is CI-only). +- **Packaging always goes through `scripts/package-app.mts` (CODE-107), never a bare `electron-builder`.** It runs `pnpm --prod deploy --cpu=` into one staging dir per target architecture **outside** the workspace, then runs electron-builder once per dir with `--projectDir` pointed there. The CPU-scoped closure carries only that arch's native optional bindings; `appDir === projectDir === workspaceRoot` makes `@electron/rebuild` find better-sqlite3 on Windows and makes the asar collector see one importer instead of the whole monorepo. macOS/Windows update manifests are merged after the single-arch builds; Linux feed names are already arch-specific. `package:devshell` uses `electron-builder.devshell.yml` + `--dir`; `package` is the production variant; both redirect `directories.output` back to `release/` and icons to shared `assets/`. There is no `dist` script (release packaging is CI-only). - **`extraResources` ship real executables OUTSIDE the asar — the OS cannot exec an asar member:** `sidecar/${arch}` (the `linkcode-pty` PTY sidecar everywhere, plus the `linkcode-sim` iOS Simulator sidecar on macOS — the crate list lives in `scripts/stage-sidecar.mts`). `sidecar/` and `release/` are gitignored, so a fresh clone has none until staged; `pnpm -F @linkcode/desktop stage:host-runtime` builds the daemon + stages the sidecars. **Agent CLI binaries do not ship** (CODE-114): `files` globs exclude the SDK platform packages from the asar (each is ~220 MB, host-arch only — broken cross-arch anyway), and the daemon spawns a detected user install or a managed download instead (resolution in [`packages/host/agent-adapter/AGENTS.md`](../../packages/host/agent-adapter/AGENTS.md)); The pi npm closure is likewise absent (CODE-219): its SDK is a devDependency of agent-adapter, so the `--prod deploy` staging never contains it — the daemon downloads it into the asset store on first use. `verify-artifacts.mts` fails the build if a platform package, a pi-closure package, or `Resources/agent-bin` sneaks back in, or any artifact exceeds 200 MB. - **The bundled daemon comes from the `bundle-daemon-artifact` plugin** in `vite.main.config.ts`: it copies `apps/daemon/dist/index.js` → `out/daemon/index.mjs` (and `instrument.js` → `instrument.mjs` for Sentry preload; both renamed `.mjs` because they leave the daemon's `type=module` scope) and `apps/daemon/drizzle` → `out/drizzle`. It throws `` apps/daemon/dist is missing — run `pnpm -F @linkcode/daemon build` first `` if the daemon wasn't built; turbo `^build` guarantees ordering. - **`linkcode://` deep-link registration differs by build path.** Packaged builds register it via the `electron-builder.yml` `protocols` block (macOS Info.plist + Windows installer); dev shells register it at runtime via `setAsDefaultProtocolClient` (`cloud-auth/client.ts`). OAuth callback routing therefore depends on how the app was shipped. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1fc78fe6e..34e4a8820 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -77,6 +77,7 @@ "typescript": "catalog:", "unplugin-icons": "catalog:", "vite": "catalog:", + "yaml": "^2.9.0", "zustand": "catalog:" } } diff --git a/apps/desktop/scripts/package-app.mts b/apps/desktop/scripts/package-app.mts index 63ffeccbd..59a6b0227 100644 --- a/apps/desktop/scripts/package-app.mts +++ b/apps/desktop/scripts/package-app.mts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Package the desktop app from a materialized, single-importer staging directory (CODE-107): + * Package the desktop app from one materialized, single-importer staging directory per arch: * `node scripts/package-app.mts [mac|win|linux] [--x64|--arm64] [--devshell] [builder args]`. * * Packing apps/desktop in place fails silently twice under pnpm's hoisted layout: on Windows the @@ -15,11 +15,20 @@ * better-sqlite3 and the collector sees exactly one importer. The .pnpmfile.cjs * drizzle-orm↔expo-sqlite sever stays — it keeps the expo tree out of this deploy closure. */ -import { cpSync, readdirSync, rmSync, statSync } from 'node:fs'; +import { + cpSync, + existsSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import process from 'node:process'; import crossSpawn from 'cross-spawn'; +import { mergeUpdateFeeds } from './update-feed.mts'; const HOST_PLATFORM: Partial> = { darwin: 'mac', @@ -29,6 +38,7 @@ const HOST_PLATFORM: Partial> = { const BUILDER_PLATFORMS = ['mac', 'win', 'linux'] as const; type BuilderPlatform = (typeof BUILDER_PLATFORMS)[number]; const BUILDER_ARCHES = ['x64', 'arm64'] as const; +type BuilderArch = (typeof BUILDER_ARCHES)[number]; const desktopDir = join(import.meta.dirname, '..'); const repoRoot = join(desktopDir, '..', '..'); @@ -39,7 +49,9 @@ const releaseDir = join(desktopDir, 'release'); * OUTSIDE the workspace on purpose: a staging dir under the repo would be rediscovered as a * workspace member, reintroducing the multi-importer collection this flow exists to avoid. */ -const stagingDir = join(tmpdir(), 'linkcode-desktop-staging'); +function stagingDir(arch: BuilderArch): string { + return join(tmpdir(), `linkcode-desktop-staging-${arch}`); +} /** * Run a command, inheriting stdio, throwing on failure. cross-spawn is required on Windows: a bare @@ -69,22 +81,24 @@ const passthrough = args.filter( !BUILDER_ARCHES.some((arch) => arg === `--${arch}`), ); -/** Deploy the production closure into a fresh staging dir, then sync the build outputs into it. */ -function materializeStaging(): void { - rmSync(stagingDir, { recursive: true, force: true }); +/** Deploy one arch's production closure, then sync the build outputs into it. */ +function materializeStaging(arch: BuilderArch): string { + const target = stagingDir(arch); + rmSync(target, { recursive: true, force: true }); // --legacy: deploy without pnpm's inject-workspace-packages requirement (v10+ default refusal). run( 'pnpm', - ['--filter', '@linkcode/desktop', '--prod', 'deploy', '--legacy', stagingDir], + ['--filter', '@linkcode/desktop', '--prod', 'deploy', '--legacy', `--cpu=${arch}`, target], repoRoot, ); // deploy's file selection skips .gitignore'd paths inconsistently across pnpm versions; sync the // build outputs in explicitly so `files: out/**` and `extraResources: sidecar/${arch}` resolve. for (const dir of ['out', 'sidecar']) { - const dest = join(stagingDir, dir); + const dest = join(target, dir); rmSync(dest, { recursive: true, force: true }); cpSync(join(desktopDir, dir), dest, { recursive: true }); } + return target; } /** Doc files that must survive the prune: license/attribution texts we redistribute. */ @@ -100,10 +114,10 @@ const MARKDOWN_RE = /\.(?:md|markdown)$/i; * `files` globs in electron-builder.yml instead; notably better-sqlite3/deps must stay HERE in * staging because @electron/rebuild compiles from it before collection. */ -function pruneStaging(): void { +function pruneStaging(target: string): void { let files = 0; let bytes = 0; - for (const entry of readdirSync(join(stagingDir, 'node_modules'), { + for (const entry of readdirSync(join(target, 'node_modules'), { recursive: true, withFileTypes: true, })) { @@ -128,10 +142,9 @@ function pruneStaging(): void { * resolve an arch that wasn't (CI stages both via `stage-sidecar --all`; a local * `stage:host-runtime` stages just the host). */ -function stagedArches(): string[] { - const staged = readdirSync(join(desktopDir, 'sidecar')).filter((name) => - BUILDER_ARCHES.some((arch) => arch === name), - ); +function stagedArches(): BuilderArch[] { + const stagedNames = new Set(readdirSync(join(desktopDir, 'sidecar'))); + const staged = BUILDER_ARCHES.filter((arch) => stagedNames.has(arch)); if (staged.length === 0) throw new Error('no staged sidecar arch; run stage:host-runtime first'); const arches = requestedArches.length === 0 ? staged : requestedArches; for (const arch of arches) { @@ -140,33 +153,54 @@ function stagedArches(): string[] { return arches; } +function updateFeedName(arch: BuilderArch): string { + if (platform === 'mac') return 'latest-mac.yml'; + if (platform === 'win') return 'latest.yml'; + return arch === 'arm64' ? 'latest-linux-arm64.yml' : 'latest-linux.yml'; +} + function build(): void { // Both extend the shared electron-builder.yml base; each adds its own deep-link scheme (release // `linkcode://`, dev shell `linkcode-dev://`). The base is never passed directly — it has none. const config = devshell ? 'electron-builder.devshell.yml' : 'electron-builder.release.yml'; - const builderArgs = [ - 'exec', - 'electron-builder', - `--${platform}`, - ...stagedArches().map((arch) => `--${arch}`), - '--projectDir', - stagingDir, - '--config', - join(desktopDir, config), - // projectDir is the staging dir, so config-relative paths would resolve under it; redirect - // output back to where CI/verify-artifacts expect it and icons to the shared repo-root assets. - `-c.directories.output=${releaseDir}`, - `-c.mac.icon=${join(assetsDir, 'linkcode.icon')}`, - `-c.win.icon=${join(assetsDir, 'icon.png')}`, - // A directory of per-size PNGs — app-builder-lib 26+ won't expand a single PNG into a size set, - // so a lone raster installs only hicolor/1024x1024 (unindexed → GNOME fallback icon). - `-c.linux.icon=${join(assetsDir, 'linux-icons')}`, - ...(devshell ? ['--dir'] : []), - ...passthrough, - ]; - run('pnpm', builderArgs, desktopDir); + const feeds = new Map(); + for (const arch of stagedArches()) { + const target = materializeStaging(arch); + pruneStaging(target); + const feedName = updateFeedName(arch); + const feedPath = join(releaseDir, feedName); + rmSync(feedPath, { force: true }); + run( + 'pnpm', + [ + 'exec', + 'electron-builder', + `--${platform}`, + `--${arch}`, + '--projectDir', + target, + '--config', + join(desktopDir, config), + // projectDir is the staging dir, so config-relative paths would resolve under it; redirect + // output back to where CI/verify-artifacts expect it and icons to the shared repo-root assets. + `-c.directories.output=${releaseDir}`, + `-c.mac.icon=${join(assetsDir, 'linkcode.icon')}`, + `-c.win.icon=${join(assetsDir, 'icon.png')}`, + // A directory of per-size PNGs — app-builder-lib 26+ won't expand a single PNG into a size + // set, so a lone raster installs only hicolor/1024x1024 (unindexed → GNOME fallback icon). + `-c.linux.icon=${join(assetsDir, 'linux-icons')}`, + ...(devshell ? ['--dir'] : []), + ...passthrough, + ], + desktopDir, + ); + if (existsSync(feedPath)) { + const text = readFileSync(feedPath, 'utf8'); + const existing = feeds.get(feedName); + feeds.set(feedName, existing === undefined ? text : mergeUpdateFeeds(existing, text)); + } + } + for (const [name, text] of feeds) writeFileSync(join(releaseDir, name), text); } -materializeStaging(); -pruneStaging(); build(); diff --git a/apps/desktop/scripts/update-feed.mts b/apps/desktop/scripts/update-feed.mts new file mode 100644 index 000000000..799ebe130 --- /dev/null +++ b/apps/desktop/scripts/update-feed.mts @@ -0,0 +1,21 @@ +import { appendArrayInPlace } from 'foxts/append-array-in-place'; +import { parse, stringify } from 'yaml'; + +interface UpdateFeed { + files: unknown[]; + [key: string]: unknown; +} + +function parseUpdateFeed(text: string): UpdateFeed { + const value: unknown = parse(text); + if (typeof value !== 'object' || value === null || !Array.isArray((value as UpdateFeed).files)) { + throw new Error('electron-builder update feed has no files array'); + } + return value as UpdateFeed; +} + +export function mergeUpdateFeeds(first: string, second: string): string { + const merged = parseUpdateFeed(first); + appendArrayInPlace(merged.files, parseUpdateFeed(second).files); + return stringify(merged); +} diff --git a/apps/desktop/src/__tests__/update-feed.test.ts b/apps/desktop/src/__tests__/update-feed.test.ts new file mode 100644 index 000000000..8451ce9ab --- /dev/null +++ b/apps/desktop/src/__tests__/update-feed.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { parse } from 'yaml'; +import { mergeUpdateFeeds } from '../../scripts/update-feed.mts'; + +describe('mergeUpdateFeeds', () => { + it('keeps the first arch as the legacy path and lists both arch artifacts', () => { + const first = `version: 0.13.1 +files: + - url: LinkCode-0.13.1-x64.zip + sha512: x64 +path: LinkCode-0.13.1-x64.zip +sha512: x64 +`; + const second = `version: 0.13.1 +files: + - url: LinkCode-0.13.1-arm64.zip + sha512: arm64 +path: LinkCode-0.13.1-arm64.zip +sha512: arm64 +`; + + expect(parse(mergeUpdateFeeds(first, second))).toEqual({ + version: '0.13.1', + files: [ + { url: 'LinkCode-0.13.1-x64.zip', sha512: 'x64' }, + { url: 'LinkCode-0.13.1-arm64.zip', sha512: 'arm64' }, + ], + path: 'LinkCode-0.13.1-x64.zip', + sha512: 'x64', + }); + }); +}); diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 668d8c0cc..71332b37b 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -49,7 +49,7 @@ All signing and R2 secrets live in the repo's GitHub **`release` Environment** ( ## Packaging inputs (staging & version pins) -- **Single-importer staging (CODE-107).** electron-builder never packs `apps/desktop` in place; `apps/desktop/scripts/package-app.mts` first `pnpm --prod deploy --legacy`s the app's production closure into a self-contained dir **outside** the workspace, then points electron-builder's `--projectDir` at it. This is load-bearing on Windows: pnpm's hoisted layout puts runtime deps (better-sqlite3, js-yaml, …) in the repo-root `node_modules`, and electron-builder's Windows workspace-root probe (`pnpm --workspace-root exec pwd` — no `pwd` on Windows) mis-resolves to `apps/desktop`, so `@electron/rebuild` finds no native module and ships a plain-Node-ABI `better-sqlite3` — the daemon then dies on `require` and every client shows "Unable to connect to the daemon" (this shipped from 0.1.0 through 0.2.1). In the staging dir `appDir === projectDir === workspaceRoot`, so the rebuild walker finds better-sqlite3 in step one on every platform and the module collector sees exactly one importer (falling through to the filesystem-traversal collector). CI runs `node scripts/package-app.mts --publish never …` in place of a bare `electron-builder`. +- **Per-arch single-importer staging (CODE-107).** electron-builder never packs `apps/desktop` in place; `apps/desktop/scripts/package-app.mts` runs `pnpm --prod deploy --legacy --cpu=` into one self-contained dir per target architecture **outside** the workspace, then invokes electron-builder once per dir. This is load-bearing twice: selecting one CPU keeps napi-rs optional bindings target-pure, while `appDir === projectDir === workspaceRoot` makes `@electron/rebuild` find better-sqlite3 on Windows and keeps the module collector on one importer. Separate macOS/Windows invocations target the same updater manifest, so the script merges their `files` arrays afterward while retaining x64 as the legacy `path`/`sha512`; Linux already names updater manifests per architecture. CI runs `node scripts/package-app.mts --publish never …` in place of a bare `electron-builder`. - **Cross-arch staging.** Each platform build stages *both* arches' sidecar trees before packaging so `extraResources: sidecar/${arch}` resolves: "Build PTY sidecar (both arches)" (`.github/actions/build-sidecar`). **Agent CLI binaries do not ship** (CODE-114): `files` globs in `electron-builder.yml` exclude the SDK platform packages from the asar, and the daemon spawns a detected user install or a managed download from its own asset store (`@linkcode/assets`, CODE-111). The runtime `LINKCODE_PTY_SIDECAR_PATH` contract is desktop-owned — see [`apps/desktop/AGENTS.md`](../apps/desktop/AGENTS.md). - **Electron version.** `electron-builder.yml` pins `electronVersion: 43.2.0` because electron-builder can't read pnpm's `catalog:` protocol and downloads per-platform Electron by exact version. Keep it in sync with the `electron: ^43.2.0` catalog entry in `pnpm-workspace.yaml`, or a drift silently packages the wrong Electron. On an Electron bump, also refresh `NODE_TARGET` / `CHROME_TARGET` in `apps/desktop/vite.shared.ts` from the new binary (`ELECTRON_RUN_AS_NODE=1 electron -p "process.versions"`). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ed09efd7..e5b29834a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -416,6 +416,9 @@ importers: vite: specifier: 'catalog:' version: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) + yaml: + specifier: ^2.9.0 + version: 2.9.0 zustand: specifier: 'catalog:' version: 5.0.14(@types/react@19.2.17)(react@19.3.0-canary-96fcba90-20260728)(use-sync-external-store@1.6.0(react@19.3.0-canary-96fcba90-20260728)) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7bf55e878..39d5101e1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,13 +8,6 @@ packages: nodeLinker: hoisted autoInstallPeers: true strictPeerDependencies: false -# Desktop releases cross-pack both CPU architectures and need both native optional dependencies. -supportedArchitectures: - os: - - current - cpu: - - x64 - - arm64 # Turbo runs each package's script via `pnpm run`, which otherwise triggers a per-package deps check. # With hoisted layout, dependency-less packages (schema/ipc) have no local node_modules, so that check # falsely reports "node_modules missing". Turbo already orders/builds deps, so disable the per-run check. From 9df200520480099eb6a30d5034d074cccf5c75e1 Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Fri, 31 Jul 2026 18:29:01 +0800 Subject: [PATCH 3/4] fix(release): let staging select builder architecture --- apps/desktop/electron-builder.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml index 633d79c23..15d8573a3 100644 --- a/apps/desktop/electron-builder.yml +++ b/apps/desktop/electron-builder.yml @@ -118,10 +118,8 @@ mac: entitlementsInherit: build-resources/entitlements.mac.inherit.plist notarize: true # notarytool via App Store Connect API key (APPLE_API_KEY / _KEY_ID / _ISSUER) target: - - target: dmg - arch: [x64, arm64] - - target: zip # REQUIRED for macOS auto-update (electron-updater pulls the .zip, not the .dmg) - arch: [x64, arm64] + - dmg + - zip # REQUIRED for macOS auto-update (electron-updater pulls the .zip, not the .dmg) # UDBZ (bzip2) instead of the default ULFO (lzfse): smaller first-download dmg for slower mount. # ULMO (lzma) would be smaller still but electron-builder's schema doesn't accept it. Only the @@ -131,9 +129,7 @@ dmg: win: icon: ../../assets/icon.png # shared repo-root raster (no Liquid Glass on Windows) - target: - - target: nsis - arch: [x64, arm64] + target: nsis nsis: oneClick: false @@ -155,10 +151,8 @@ linux: # executable explicitly. executableName: linkcode target: - - target: AppImage # the only Linux format electron-updater can self-update - arch: [x64, arm64] - - target: deb - arch: [x64, arm64] + - AppImage # the only Linux format electron-updater can self-update + - deb # rpm/snap need extra host tooling (rpm/fakeroot, snapcraft); add install steps to # build-desktop.yml's Linux job before enabling them here. From 0701e6acb7a3dfd24757f014523c2d17d28e3fc2 Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Fri, 31 Jul 2026 18:46:47 +0800 Subject: [PATCH 4/4] fix(release): keep builder Linux output layouts --- .github/workflows/build-desktop.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 57b77e983..3b8b70cc9 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -159,13 +159,9 @@ jobs: # Electron 43 needs Clang 15; the arm64 rebuild also needs an explicit cross target. CC=clang-15 CXX=clang++-15 \ node scripts/package-app.mts linux --x64 --publish never - mv release/linux-unpacked "$RUNNER_TEMP/linkcode-linux-x64-unpacked" CC='clang-15 --target=aarch64-linux-gnu' \ CXX='clang++-15 --target=aarch64-linux-gnu' \ node scripts/package-app.mts linux --arm64 --publish never - # Single-arch Linux builds both use linux-unpacked; preserve the verifier's arch layout. - mv release/linux-unpacked release/linux-arm64-unpacked - mv "$RUNNER_TEMP/linkcode-linux-x64-unpacked" release/linux-unpacked else node scripts/package-app.mts ${{ matrix.platform }} --publish never \ ${{ (runner.os == 'Windows' && inputs.sign) && format('-c.win.azureSignOptions.publisherName="{0}" -c.win.azureSignOptions.endpoint="{1}" -c.win.azureSignOptions.codeSigningAccountName="{2}" -c.win.azureSignOptions.certificateProfileName="{3}"', env.AZURE_PUBLISHER_NAME, env.AZURE_SIGN_ENDPOINT, env.AZURE_CODE_SIGNING_ACCOUNT, env.AZURE_CERTIFICATE_PROFILE) || '' }}