Skip to content

feat(desktop): add Linux .deb and .rpm installer output formats#35296

Merged
crowlKats merged 1 commit into
denoland:mainfrom
crowlKats:feat/desktop-deb-rpm-installers
Jun 19, 2026
Merged

feat(desktop): add Linux .deb and .rpm installer output formats#35296
crowlKats merged 1 commit into
denoland:mainfrom
crowlKats:feat/desktop-deb-rpm-installers

Conversation

@crowlKats

Copy link
Copy Markdown
Member

Summary

Adds .deb and .rpm as deno desktop output formats, alongside the existing .dmg and .AppImage. As with those, the format is chosen by the --output extension (or the per-platform output key in the desktop block of deno.json); the driver strips the extension, builds the intermediate Linux app dir, then wraps it.

Both formats produce the same install tree and are assembled in pure Rust, so they cross-compile from any host (only the target must be Linux):

/usr/lib/<pkg>/            ← staged app dir contents
/usr/bin/<pkg>             ← symlink → ../lib/<pkg>/<launcher>
/usr/share/applications/<pkg>.desktop
/usr/share/icons/hicolor/512x512/apps/<pkg>.png

.deb

Hand-rolled ar archive (debian-binary + control.tar.gz + data.tar.gz) using the already-present tar/flate2 deps — no new crate. Depends carries the curated CEF runtime libraries by Debian package name; Installed-Size is computed.

.rpm

Built with the pure-Rust rpm crate. Two notable choices:

  • zstd payload, not gziprpm's gzip-compression feature pulls flate2's zlib-rs C shim, which collides at link time with deno's vendored zlib-ng (libz-sys). deno already depends on zstd, and modern RPM tooling reads zstd payloads.
  • Requires use CEF sonames with the ()(64bit) ELF-class suffix (e.g. libgtk-3.so.0()(64bit)), which is what RPM auto-Provides on 64-bit distros; a bare soname wouldn't match.

Metadata

Derived from existing config (app-dir name → sanitized package name, --identifier, default 1.0.0 version matching the macOS bundle). No new config fields.

Why

Rounds out the desktop installer story for Linux. .AppImage already shipped; .deb/.rpm are what most Linux users actually install through their package manager.

Tests

Added pure-Rust unit tests: package-name sanitization, triple→arch mapping, .deb ar-member order / control fields / install layout, and .rpm parse-back of name/arch/Requires/files. All pass; clippy clean.

The matching-OS integration check (install / launch / upgrade / uninstall) can't run in the cross-platform unit suite, so the curated dependency lists are the most likely thing needing tuning once tested on a real Debian/Fedora box.

New dependency

rpm = "0.25.1" (payload + zstd-compression, default features off). Net-new transitive crates: enum-display-derive, enum-primitive-derive, num-derive, rpm-version.

Recognize .deb and .rpm via the --output extension (like .dmg/.AppImage)
and wrap the staged Linux app dir in the chosen package.

- .deb: hand-rolled ar + tar.gz (existing tar/flate2 deps, no new crate).
- .rpm: pure-Rust rpm crate with a zstd payload (gzip would pull flate2's
  zlib-rs shim, clashing at link time with deno's zlib-ng). Requires use
  CEF sonames with the ()(64bit) ELF-class suffix.

Both produce the same install tree (/usr/lib/<pkg>, /usr/bin symlink,
.desktop, hicolor icon) and carry the curated CEF runtime dependency list.
Metadata derives from existing config; no new config fields. Bails with a
clear message when the target isn't Linux.
@crowlKats
crowlKats merged commit 818af9c into denoland:main Jun 19, 2026
268 of 270 checks passed
littledivy added a commit that referenced this pull request Jun 21, 2026
## Summary

Adds `.msi` as a `deno desktop` output format, alongside the existing
`.dmg`/`.AppImage` and the `.deb`/`.rpm` formats added in #35296. As
with those, the format is chosen by the `--output` extension (or the
per-platform `output` key in the `desktop` block of `deno.json` — whose
Windows description already documented `MyApp.msi`); the driver strips
the extension, builds the intermediate Windows app dir, then wraps it in
an MSI.

The MSI database is authored entirely in pure Rust via the
[`msi`](https://crates.io/crates/msi) crate, with the file payload
stored in an embedded MSZIP cabinet
([`cab`](https://crates.io/crates/cab) crate), so it **cross-compiles
from any host** — only the *target* must be Windows (gated like
`.deb`/`.rpm` are gated on a Linux target). The app installs per-machine
under `ProgramFiles64Folder\<AppName>\`, mirroring the staged app-dir
tree exactly (nested directories like CEF's `locales/` are preserved);
uninstall removes it.

```
%ProgramFiles%\<AppName>\
  <AppName>.bat          (launcher)
  laufey.exe             (CEF backend)
  libcef.dll, ...        (CEF support files)
  denort.dll             (compiled runtime + user code)
  ...                    (nested dirs preserved)
```

### Notable choices
- **Deterministic GUIDs** — `ProductCode`, `UpgradeCode`, the package
code, and per-component GUIDs are derived via UUIDv5 from the app
identity (`--identifier`), so identical inputs produce an identical
installer (matching the `source_date(0)` / `mtime: 0` reproducibility of
the `.rpm`/`.deb`/AppImage paths) and the `UpgradeCode` is stable across
versions.
- **Directory/Component model** — one `Component` per install directory
(a file's directory is carried by its component), `KeyPath` = the first
file in that directory, components flagged 64-bit
(`msidbComponentAttributes64bit`). A single `Feature` ties them
together.
- **Embedded cabinet** — one MSZIP folder holding every file named by
its MSI `File` key, referenced by `Media.Cabinet = #appcab`. Summary
stream marks the source compressed with long file names (`x64;1033` /
`Arm64;1033` template).
- **No new config fields** — metadata is derived from the staged app-dir
name, `--identifier`, and the default `1.0.0` version that matches the
macOS bundle.

### Metadata
Derived from existing config (app-dir name → `ProductName`,
`--identifier` → GUID seed, default `1.0.0` version). Help text for
`--output` now lists `MyApp.msi`.

## Why
This rounds out the desktop installer story for Windows — `.msi` was
explicitly left out of the original desktop PR (#33441). `.msi` is what
most Windows users/IT deployments install through.

## Tests
Pure-Rust unit tests (cross-platform): triple→arch mapping,
deterministic+valid GUID derivation, 8.3 short-name minting, and a full
`create_windows_msi` round-trip that parses the produced MSI back and
asserts the `Property`/`Directory`/`File`/`Component`/`Media` tables,
the 1..N file sequence, the two-component layout for a nested dir, and
that a file's bytes round-trip out of the embedded cabinet. All pass;
clippy clean.

As with `.deb`/`.rpm`, the matching-OS integration check (install /
launch / upgrade / uninstall on a real Windows box) can't run in the
cross-platform unit suite. Major-upgrade automation (an `Upgrade` table
+ `RemoveExistingProducts`) is intentionally left as a follow-up.

## New dependencies
`msi = "0.10.0"` and `cab = "0.6.0"` (both pure Rust). Also enables the
`v5` feature on the existing `uuid` dep.

Closes #35377
Ref mdsteele/rust-msi#59
Closes denoland/divybot#582

---------

Co-authored-by: divybot <[email protected]>
Co-authored-by: Divy Srivastava <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants