Commit beecaa5
authored
feat(desktop): ship macOS x64 and Linux packages (#4468)
* feat(desktop): ship macOS x64 and Linux packages
Desktop published only macOS arm64 and Windows x64, so Intel Macs and every
Linux desktop had no build at all. Both nightly and formal releases now package
five targets: macOS arm64 and x64, Windows x64, and Linux x64 and arm64, with
Linux shipping an AppImage and a deb.
Each architecture builds on a runner of its own because the native Runtime Host
peer and the packaged smoke probes are host binaries; cross-building would embed
the wrong architecture's addon and produce a package that builds green and dies
on launch.
That makes one update feed carry several payloads for the first time. macOS puts
both architectures in one feed and electron-updater picks its payload out of the
`files` list, but each architecture is packaged separately and writes a feed
naming only its own zip. Packaging now names the macOS feed after its
architecture so the two uploads cannot overwrite each other, and publication
merges them back into the single feed clients read. Linux advertises both the
AppImage and the deb in one feed, so `verifyDesktopUpdateArtifacts` takes the
full payload list instead of a single name, and requires a blockmap for every
payload except the fpm targets, which ship without one.
`desktop-release-targets.mjs` becomes the one place that knows what a Desktop
target produces. Both publication channels read it, so the nightly workflow no
longer repeats artifact names in YAML and the provenance step no longer counts
subjects against a hardcoded number. Adding a target is now one edit.
The macOS automatic-update proof stays on arm64: the updater is one code path
across both architectures, and package-macos-autoupdate-next.mjs already
enforces that restriction.
Generated-by: Claude Code
* fix(desktop): hold the packaging descriptor to what electron-builder builds
The target descriptor asserted three things about electron-builder that were
all false, and nothing checked any of them.
electron-builder ignores the command line's architecture flags for any target
the configuration names: computeArchToTargetNamesMap only falls back to the CLI
when `target.arch` is absent. `mac.target` carried `arch: ['arm64', 'x64']`, so
`--mac --arm64` built both architectures on every macOS runner, and `--linux`
built both on every Linux runner — each embedding the host's Runtime Host peer
and, on macOS, leaving the cross-built DMG unnotarized. The targets move to the
packaging scripts, which now name the target and the architecture together, and
the configuration says nothing about either.
`${arch}` in a Linux artifactName is the packaging ecosystem's spelling, not
Node's: getArtifactArchName rewrites x64 to `x86_64` for an AppImage and to
`amd64` for a deb. The descriptor expected `-linux-x64`, so the x64 runner
would have failed on its own output.
An AppImage has no `<file>.blockmap`: appImageUtil appends its block map to the
AppImage itself, and the client reads it back out from there. The descriptor
listed a sidecar that is never written, and the feed verifier demanded one for
every payload that is not a deb. It now names the two payloads that do have a
sidecar instead of guessing from what does not.
desktop-release-targets.test.mjs drives electron-builder's own resolution
functions against the packaging scripts and the configuration, so the descriptor
can no longer state something electron-builder does not do.
Two further defects on the same seam:
The AppImage and the deb are built from one unpacked tree, and the deb target
writes a `package-type` marker into it that electron-updater reads to choose
DebUpdater over AppImageUpdater. An AppImage carrying that marker updates itself
by installing a deb. Linux packaging now builds the AppImage in a run of its
own, before the deb, and merges the two feeds the runs write; verify-linux.mjs
fails if the marker reaches the AppImage anyway.
Publication never merged the two macOS feeds. Each runner uploads a feed named
after its architecture and `releaseAssets` names only the merged feed, so the
exact-manifest check could not have passed. `mergedFrom` now survives into the
release identity and the publish job merges before it verifies.
Also restores the provenance count guard the previous commit dropped — a
process substitution hides its exit code, so a failing lister left the step
green with nothing verified — binds release.yml's matrix to the descriptor,
and brings the release notes and RELEASE_CHECKLIST.md up to five targets.
Generated-by: Claude Opus 5 via Claude Code
* fix(desktop): give the Linux packages a launchable name and a homepage
Neither Linux target could be built. electron-builder derives the Linux
executable name from the npm package name, and this one is scoped:
`sanitizeFileName('@maka/desktop')` is `@makadesktop`, which a desktop
entry's `Exec=` cannot launch. It has a scoped-package fix for the deb's
package name and none for this. fpm then refuses to run at all without a
project homepage, which neither manifest declared and which cannot be
recovered from the git remote on a fork.
Both are settled before electron-builder packages anything, so the
contract test now drives that resolution instead of a runner discovering
it: it builds a packager per Linux packaging script, requires the
executable name to be one a shell will accept, and calls `checkOptions`
on each target, which is where fpm demands its metadata. The AppImage
target inherits an empty `checkOptions`, so the name assertion is what
covers it. This costs 180ms on any host and fails with the same message
the build would have.
Only Linux resolves an executable name this way and only fpm demands a
homepage, which is why macOS and Windows never showed either defect.
Generated-by: Claude Opus 5 via Claude Code
* fix(desktop): verify the deb instead of only hashing it
The verifier opened the AppImage and applied every packaged-resource,
update and dependency assertion to it, then handed the deb a checksum
without ever reading it. Replacing the deb with a text file left the gate
green and produced a checksum for that text.
Its comment explained why: both payloads came from one unpacked tree, so
proving the contract on the AppImage proved it for both. That stopped
being true in this branch. Keeping the deb's `package-type` marker out of
the AppImage required splitting packaging into two electron-builder runs,
and the second one packs its own tree. Nothing about one payload carries
over to the other any more.
Both are now extracted and asserted. The deb additionally has to carry
`package-type=deb` — the mirror of the AppImage forbidding it, and what
sends the packaged updater down DebUpdater — and to declare the
architecture its filename claims, so a runner that built the wrong one
cannot publish it under the right name. Checksums are issued only after
every assertion has accepted the payload.
Where the deb keeps its resources is discovered from the extracted tree
rather than derived from the install prefix. Deriving what a payload
should contain, instead of reading what it does, is how this verifier
came to trust a file it had never opened.
Generated-by: Claude Opus 5 via Claude Code
* ci: run the Linux release path on pull requests
Desktop packaging went from two targets to five, but the pull-request
checks did not move: `package` is the Windows lane, and nothing built
macOS x64 or either Linux package before merge. Both Linux defects fixed
in this branch survived review for that reason — no check could see them,
because nothing outside a real Linux build ever ran that path.
`release-windows-check.yml` already carries this lesson, in its own
words: the Windows release path first ran on release day, on main, with a
tag reserved. This is the same lane for Linux, and its `paths` list
overlaps because the packaging configuration and the manifests decide the
identity both platforms need — those files changed here, the Windows lane
ran, and it passed, because neither defect was observable on Windows.
x64 only. arm64 is built from the same scripts, configuration and
descriptor, so this lane would re-prove everything it can reach and
nothing that differs — a 16K-page host and its own native peer. The
Nightly matrix builds and verifies arm64 every night.
This covers what the contract test structurally cannot: fpm, the AppImage
runtime, the native peer, and verification passing over real payloads.
Generated-by: Claude Opus 5 via Claude Code
* ci: name the Linux check job so it is distinguishable
Both lanes named their job `package`, so a pull request showed two checks
called `package` with no way to tell which platform failed.
Generated-by: Claude Opus 5 via Claude Code
* ci: drop the Linux check's cron
The Windows lane pairs its path filter with a nightly cron because the
filter is a pre-filter, not an import closure. This lane needs no such
pairing: the Nightly matrix already runs `package:linux` and
`verify:linux` for both architectures on every npm Nightly publication,
which is more often than a daily cron. The Windows cron earns its runner
by also exercising the installer lifecycle, automatic update and
rollback; this one would only buy a second copy of the same run.
Generated-by: Claude Opus 5 via Claude Code
* ci: hold the Linux check to the repository's lane policy
`ci-workflow-policy.test.mjs` rejected the lane on three counts, and it
was right on all three.
Restores the cron I had removed one commit earlier. I argued the Nightly
made it redundant because that matrix runs the same two commands. The
policy is enumerated over the whole workflow directory precisely so that
reasoning cannot be applied lane by lane: a path filter is a pre-filter,
not an import closure, so the lane itself must be reachable without
consulting it. The Nightly is a different workflow, gated on a successful
npm publication and a repository variable, so it is not this lane's
escape from its own filter.
Aligns the timeout to 45, the single tier every pull-request lane
carries, rather than the 60 I picked with nothing behind it. The first
real run packaged and verified in 6m22s.
Registers the lane in the pull-request allowlist, which exists because a
workflow that nobody remembered to name once kept an unbounded trigger.
Generated-by: Claude Opus 5 via Claude Code
* fix(desktop): let the desktop environment recognise the running window
electron-builder warned about this once per Linux target in the packaging
log, and named the fix; nobody read the log, because the lane was green.
Electron derives its app_id — the window's WM_CLASS — from `desktopName`
in the manifest. Absent that field, the desktop entry's `StartupWMClass`
falls back to the product name, so the entry advertises `Maka` while the
window reports `maka`. They never match, and a desktop environment that
cannot link the two shows a generic icon and silently ignores an attempt
to pin the app. Every Linux user meets this on first launch.
Setting `desktopName` in the manifest and `linux.syncDesktopName` in the
configuration derives the entry's filename and the app_id from one string.
Generated-by: Claude Opus 5 via Claude Code
* test(desktop): verify Linux as deeply as macOS and Windows
Every Linux assertion read files. `verify-macos-dmg.mjs` asserts the
executable's architecture with `lipo`, runs a pty probe, smokes the
filesystem worker and launches the renderer over CDP; the Windows lane
runs its sandbox probe. Linux never ran anything it had built. Each
defect this branch collected — the unlaunchable executable name, the
unread deb, the unset desktop name — was some check the other two
platforms already had and Linux did not.
Three assertions close that gap:
The Runtime Host peer's ELF `e_machine` is read in both payloads. The
packaging scripts refuse to cross-build precisely because a mismatched
peer produces a package that installs and dies at launch, and Linux was
the only platform where nothing could detect it. `assertElfArchitecture`
is exported and covered by `verify-linux-harness.test.mjs`, which builds
ELF headers byte by byte and so runs on any host.
The deb's `Package:` field must be an installable Debian name.
electron-builder derives it from the product name, and `dpkg` rejects a
capital letter, so a package nobody could install would otherwise reach
the Nightly — which publishes without a human. I could not settle this by
reading fpm's sources, and reading sources instead of running them is the
mistake this branch keeps repeating, so it is now a question CI answers.
The extracted AppImage is launched under `xvfb-run`. Only this can fail on
a package that is structurally perfect and still will not start. It is
applied to the AppImage alone: extracting it yields the tree its runtime
mounts at launch, whereas `dpkg-deb -x` output is not an installation —
`dpkg` would still have to set the sandbox helper's setuid bit — so
launching that would prove something about a tree no user has.
Also drops the lane comment claiming it covered "the AppImage runtime and
the native peer", which described none of what the lane did; requires
exactly one `resources/app.asar` in the deb rather than taking whichever
`find` returned first; and extends the path filter to what the packaging
configuration and the verifier actually import, plus the icon catalogue
`assertPackagedResources` enumerates to build its own required list.
Generated-by: Claude Opus 5 via Claude Code
* refactor(desktop): let the descriptor name the macOS DMG
This branch exists to make `desktop-release-targets.mjs` the one place
that knows what a target produces, and it freed the Nightly's artifact
staging from spelling names in YAML. Then it added three new spellings in
the same diff: macOS went from one architecture to two, so the hardcoded
`-mac-arm64.dmg` became `-mac-${{ matrix.arch }}.dmg` in the signing step,
the verification step, and again in the Nightly. A second authority on the
artifact name, and unlike the descriptor nothing checked this one.
`verify:macos` now takes an architecture and resolves the path from the
descriptor, the way `verify:linux` already did — which also means the
verification fails if the descriptor and the build ever disagree.
The signing and notarization steps discover the DMG instead: a runner
builds one architecture, so exactly one exists, and signing never needed
its name. Finding none or several now fails the step rather than passing
a wrong path to `codesign`.
Two spellings remain, both older than this branch and neither on the
matrix: the arm64-only automatic-update ZIP and the Windows installer.
Generated-by: Claude Opus 5 via Claude Code
* refactor(scripts): drop an identity remap and two unused exports
`resolveProductReleaseIdentity` rebuilt each published feed as
`{name, advertised, mergedFrom}` — the exact shape `desktopPublishedFeeds`
already returns, field for field. The comment above it described what the
function itself does.
`mergeDesktopUpdateFeedDocuments` and `desktopNightlyPublishedFeeds` were
exported with no consumer outside their own modules, tests included. Two
fewer public contracts to keep.
Generated-by: Claude Opus 5 via Claude Code
* fix(test): copy the builder configuration through JSON
The contract test isolates a configuration copy per packager because
electron-builder rewrites the object it is handed. structuredClone did
that locally on Node 26 and failed on the Node 24 the CI lane runs:
"Unable to deserialize cloned data due to invalid or unsupported version".
Everything electron-builder reads out of this configuration has to
survive serialization anyway, and JSON round-tripping behaves the same on
every version. `beforePack` is the one function in it and is carried
across by reference.
Generated-by: Claude Opus 5 via Claude Code
* fix(desktop): verify the update the updater chose, not a platform table
The attestation verifier held a hand-written platform/arch to filename
table that named only the macOS arm64 ZIP and the Windows installer, so
every target this branch adds ended in a download error with a valid
feed, payload and attestation. The table was also a second authority for
what is built, and could not be completed: electron-updater serves the
arm64 ZIP to an x64 build under Rosetta, and one Linux tuple installs
either the AppImage or the deb.
Identify the download by the name electron-updater gave it, require that
name among the feed entries the updater was offered and under the
released version, and keep the attestation subject as the trust anchor.
* fix(release): hand Finalize the published set the publish job verified
Merging the per-architecture macOS feeds split the set each runner
uploads from the set that is published, but the merged result lived only
in the publish job's working directory. Finalize rebuilt its input from
the raw runner artifacts and failed the exact-set check deterministically
(missing latest-mac.yml; unexpected latest-mac-arm64.yml,
latest-mac-x64.yml), and would have attested the per-architecture feeds
rather than the one users download. The checklist's `list` command
printed the same stale set.
Upload the verified release-assets directory as a run artifact, have
Finalize consume it instead of reconstructing it, and make `list` report
the published set.
* test(desktop): verify the merged Linux update feed before checksums
Linux is the one platform whose feed this repository assembles itself,
and the merged bytes were first read at publication. Verify them in the
Linux lane against the payloads it just accepted.
* fix(ci): run the Linux harness after dependencies are installed
Reading a feed goes through the yaml package, so the suite can no longer
run in the planner step that has only node builtins.
* test: drop a vacuous checksum guard and a hand-copied payload list
The harness asserted that a function with no write path wrote nothing,
and the attestation suite restated the descriptor's payload names that
desktop-release-targets.test.mjs already pins to electron-builder.
* refactor(scripts): let every packaging and verify script read the descriptor
The target descriptor was declared the one authority on what each
desktop target builds, advertises and checksums, but only the Linux
scripts read it: macOS and Windows packaging still spelled their own
payload and feed names, and their verifiers hard-coded which files get a
checksum while the descriptor's `checksums` mirrored them by hand.
Resolve every name from the descriptor, and let the verifiers take an
architecture the way `verify:linux` does.
* refactor(release): drop the restated Windows installer name and the matrix target key
The identity's `exe` output and ten inline spellings of the installer
name were the Windows twin of the macOS `dmg` output this branch already
removed; the workflows now discover the installer the way they discover
the DMG, and verify it through the descriptor. The matrix `target` key
was always `${platform}-${arch}`, which both policy tests asserted.
* refactor(scripts): resolve a release target in one place
Six scripts repeated the same lookup — manifest version, nightly
override, descriptor entry, release directory — and the macOS autoupdate
step still spelled its ZIP in YAML. One resolver owns it now, and the
runner-label assertions check that each matrix row's runner suits its
platform and architecture instead of restating the list.
* docs(scripts): state the rule behind the checksum set
A checksum is issued only for a payload the verify script opened; the
macOS script opens the DMG alone.
* fix(release): run the Linux verification under xvfb on every lane
The verifier launches the extracted AppImage's renderer over CDP, and
only the pull-request lane ran it under xvfb-run; the release and
Nightly matrices called it bare, which fails on a runner without a
display. Wrap all three, install xvfb the way ci.yml does, and hold
every workflow to it.
Alongside, the checks the review exposed as never executed now run over
real functions: the Linux harness merges two single-document feeds with
mergeDesktopUpdateFeeds before verifying, and a release test drives the
publish job's merge and verify over a directory built from every staged
group. The macOS verifier asserts the architecture it was asked for, and
the three verifiers take the channel from the resolved target instead of
re-reading the Nightly environment.
* fix(desktop): resolve the local setup package without naming a peer target
A packaged build never reads the development peer target — it ships its
own setup package — but both local callers evaluated it eagerly, and the
whitelist has no darwin-x64 because no npm prebuild exists for it. An
Intel Mac, which this branch now ships with its own native peer, would
throw at first use of local remote access. Read the target only where a
development build needs it; remote callers keep choosing their own.
* fix(desktop): require the downloaded update to be this platform's package
The feed-derived identity accepted any same-version artifact the release
attested, including the CLI archive and other platforms' packages. Match
the platform and format only — never the architecture, so a Rosetta
process still accepts the arm64 ZIP — and read a legacy feed without a
files list as offering nothing.
* docs: describe the five-target Desktop in the README and release notes1 parent 43aa35e commit beecaa5
51 files changed
Lines changed: 2735 additions & 450 deletions
File tree
- .github
- workflows
- apps/desktop
- src/main
- __tests__
- docs
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
35 | | - | |
| 34 | + | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
| 32 | + | |
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
| |||
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
87 | | - | |
| 88 | + | |
88 | 89 | | |
89 | 90 | | |
90 | 91 | | |
91 | | - | |
92 | | - | |
| 92 | + | |
| 93 | + | |
93 | 94 | | |
94 | 95 | | |
95 | 96 | | |
| |||
100 | 101 | | |
101 | 102 | | |
102 | 103 | | |
103 | | - | |
| 104 | + | |
104 | 105 | | |
105 | 106 | | |
106 | 107 | | |
| |||
131 | 132 | | |
132 | 133 | | |
133 | 134 | | |
134 | | - | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
135 | 140 | | |
136 | 141 | | |
137 | 142 | | |
| |||
168 | 173 | | |
169 | 174 | | |
170 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
171 | 193 | | |
172 | 194 | | |
173 | | - | |
| 195 | + | |
174 | 196 | | |
175 | 197 | | |
176 | 198 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
| 90 | + | |
90 | 91 | | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
91 | 95 | | |
| 96 | + | |
92 | 97 | | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
93 | 104 | | |
94 | 105 | | |
95 | 106 | | |
| |||
145 | 156 | | |
146 | 157 | | |
147 | 158 | | |
148 | | - | |
| 159 | + | |
149 | 160 | | |
150 | 161 | | |
151 | 162 | | |
152 | 163 | | |
153 | 164 | | |
154 | 165 | | |
155 | 166 | | |
156 | | - | |
157 | 167 | | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
158 | 177 | | |
159 | 178 | | |
160 | 179 | | |
161 | 180 | | |
162 | 181 | | |
163 | 182 | | |
164 | | - | |
| 183 | + | |
165 | 184 | | |
166 | 185 | | |
167 | 186 | | |
168 | 187 | | |
169 | 188 | | |
170 | 189 | | |
171 | 190 | | |
172 | | - | |
173 | | - | |
174 | | - | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
175 | 206 | | |
176 | 207 | | |
177 | 208 | | |
178 | 209 | | |
179 | 210 | | |
180 | 211 | | |
181 | | - | |
182 | | - | |
183 | | - | |
184 | | - | |
185 | | - | |
186 | | - | |
187 | | - | |
188 | | - | |
189 | | - | |
190 | | - | |
191 | | - | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
197 | 217 | | |
198 | 218 | | |
199 | 219 | | |
200 | 220 | | |
201 | | - | |
| 221 | + | |
202 | 222 | | |
203 | 223 | | |
204 | 224 | | |
| |||
236 | 256 | | |
237 | 257 | | |
238 | 258 | | |
239 | | - | |
| 259 | + | |
240 | 260 | | |
241 | 261 | | |
242 | 262 | | |
| |||
263 | 283 | | |
264 | 284 | | |
265 | 285 | | |
| 286 | + | |
266 | 287 | | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
267 | 295 | | |
268 | 296 | | |
269 | 297 | | |
| |||
273 | 301 | | |
274 | 302 | | |
275 | 303 | | |
276 | | - | |
277 | | - | |
| 304 | + | |
| 305 | + | |
278 | 306 | | |
279 | 307 | | |
280 | 308 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
235 | 235 | | |
236 | 236 | | |
237 | 237 | | |
238 | | - | |
| 238 | + | |
239 | 239 | | |
240 | | - | |
241 | 240 | | |
242 | 241 | | |
243 | 242 | | |
| |||
0 commit comments