Skip to content

Linux support: fix mp4 export (no AAC encoder), ship deb/rpm/AppImage/Arch, and make dapi work - #58

Open
zbcoding wants to merge 6 commits into
diffusionstudio:mainfrom
zbcoding:linux-support
Open

Linux support: fix mp4 export (no AAC encoder), ship deb/rpm/AppImage/Arch, and make dapi work#58
zbcoding wants to merge 6 commits into
diffusionstudio:mainfrom
zbcoding:linux-support

Conversation

@zbcoding

@zbcoding zbcoding commented Sep 3, 2026

Copy link
Copy Markdown

Linux support with fixes and packages like .appimage

Claude AI Pull Request

Linux support in six commits that can be taken separately. Built, packaged, launched and driven on Linux (KDE Plasma 6.7 Wayland, NVIDIA, Chromium/Electron with no system AAC encoder); every change is either platform-gated or a strict superset, so macOS behaviour is unchanged.

1. fix(export): resolve the audio codec against what the platform can encode

The bug that started this: every mp4 export fails on Linux. Video is fine — Chromium's bundled OpenH264 encodes AVC — but every template in export-templates.ts asks for audio: { codec: "aac" } and the encoder took that as a demand. In WebCodecs, AAC is the platform's encoder (AudioToolbox on macOS, Media Foundation on Windows), so Chromium on Linux has none, and the export dies after the video track has already started.

The requested codec is now a preference. New resolveAudioCodec(format, requested, options) in @diffusionstudio/encoder keeps it when the container takes it and the browser can encode it, otherwise picks the container's next encodable choice — Opus, which mp4 accepts and Chromium bundles everywhere. Resolved at all three places an export starts:

  • context/export.tsx (⌘E / the panel) — before the save picker, so the progress overlay names what is actually written
  • context/dapi/export.ts (dapi export) — before the render, so the echoed config is truthful
  • packages/encoder/src/encoder.ts — last line of defence, so a project authored on macOS with "codec": "aac" in package.json exports on Linux unedited

The audio picker now offers only codecs this machine can encode into the chosen container. createOutputFormat lost its TargetBuffer parameter and takes { fastStart }, so container support is queryable without a write target.

Verified in a real Chromium: canEncodeAudio('aac')false, resolveAudioCodec('mp4','aac')opus, and an mp4 muxed through the same AudioSampleSource/CanvasSource path reads back as avc + opus with decodable audio. Where AAC exists it is encodable, so it stays what gets used.

2. feat(desktop): package and run on Linux

makers had only MakerZIP(['darwin']) and the DMG, so a Linux checkout could be packaged but never maked. Adds MakerDeb and MakerRpm for ['linux'] plus a Linux ZIP, and scopes the DMG to ['darwin'].

The Linux build renames the executable to diffusion-studio: electron-packager keeps the space from name, fine inside a .app but not for the binary the packages symlink into /usr/bin. Gated to Linux, so macOS still produces Contents/MacOS/Diffusion Studio — the name the CLI wrapper and the signing pass both address.

setupAppMenu() returned early off darwin, so "Install dapi Command Line Tool…" was unreachable behind Electron's stock menu; there is now a non-macOS template carrying it under File, with no AppKit-only roles.

No ozone/Wayland flag, and that is deliberate. I tried --ozone-platform-hint=auto first, since the Wayland/Vulkan line in the log invites it. It makes the packaged app unusable here: the window comes up blank or with every glyph missing, while a DevTools capture of the same renderer is pixel-correct — the renderer paints and Wayland presentation drops it ('--ozone-platform=wayland' is not compatible with Vulkan). Chromium's XWayland default renders correctly. Electron already reads ELECTRON_OZONE_PLATFORM_HINT=auto from the environment, so anyone whose driver handles native Wayland can opt in; the code says why it is not the default.

release.yml gains a publish-linux job on ubuntu-latest (installing zip, dpkg-dev, fakeroot, rpm, squashfs-tools) so artifacts land on the same draft release. It carries no Apple secrets. Drop this hunk if you'd rather not build Linux in CI yet.

Left alone deliberately: build-native.mjs exiting 0 off darwin (AppKit addon), projects.ts's fileProviderDomains returning [] off darwin (File Provider is a macOS service), and update-electron-app, which already no-ops on Linux.

3. feat(cli): make dapi work on Linux

  • dapi open could not launch the applaunchApp shelled out to open -a behind a darwin gate. Linux now spawns the executable the packaged wrapper points at (DIFFUSION_APP_PATH), then one on PATH, then hands diffusion:// to xdg-open; failure still falls through to the socket and its existing error.
  • dapi fonts hard-failed off darwin — now enumerates through fontconfig, mapping fc-list's weight axis onto CSS weights and collapsing the one-line-per-file listing into families. 304 families here, filters intact, and the local() sources it emits load in Chromium via FontFace. Without fontconfig it says so and names the package.
  • The staged cli/bin/dapi wrapper hardcoded $DIR/../../../MacOS/Diffusion Studio — it now picks the layout it sits in: the bundle path on macOS, byte for byte as before, or the flat packager tree beside resources/. Tested against both simulated layouts, the symlink-on-PATH case, and the missing-executable error.
  • The in-app installer linked into /usr/local/bin behind an osascript admin prompt; on Linux it links ~/.local/bin, which the user owns, so nothing is elevated.

4. feat(desktop): build an AppImage as well

deb and rpm cover Debian/Ubuntu and Fedora; everywhere else there was nothing to hand a user. Adds @reforged/maker-appimage (Forge has no first-party AppImage maker) — one executable file, no root, no package manager. Both makers now set bin explicitly, because it defaults to the sanitized package name (diffusionstudio-desktop) rather than what electron-packager emits, and the AppImage build fails outright without it.

Flatpak is not here on purpose: the sandbox fights what this app does — editing arbitrary project folders, spawning agent CLIs and esbuild/Babel, putting dapi on PATH. Making it work means --filesystem=host plus no CLI install, a worse deal than the AppImage. Happy to add it if you disagree.

5. feat(desktop): package for Arch, and fix what a real package build hits

Dry-running the makers against the packaged tree — rather than trusting their config — turned up two builds that could never have succeeded, and comparing notes with #42 turned up a third:

Arch Linux support comes from #42. Arch has no Forge maker, so a script stages the forge output and runs makepkg, with a publish-arch container job. Adapted: the launcher shim that PR needed is gone, because the executable is renamed and /usr/bin can link straight to it; their ln -s … resources/cli/bin/dapi is kept, since a package can put dapi on PATH.

All four formats now install one desktop entry, in packaging/linux/, so the makers and the PKGBUILD cannot drift. It registers x-scheme-handler/diffusion — how the auth and checkout deep links come back to the app, the job packagerConfig.protocols does on macOS.

6. fix(cli): install a working dapi from an AppImage, and stop the app inheriting node mode

Two bugs found by installing the CLI from a real AppImage and then quitting it, which is what a user does:

  • The install wrote a symlink into the image's mount. That mount lives under /tmp only while the app runs, with a fresh random name each launch, so the link dangled the moment the app quit. The image file is stable and can run the CLI on its own Electron in node mode, so the install now writes a wrapper that does exactly that — the image mounts itself for the duration of each call. The path inside the mount is read from the running process, not assumed, since the maker decides the layout.
  • dapi open could never launch the app from any packaged install, deb and rpm included. The wrapper runs the CLI with ELECTRON_RUN_AS_NODE=1 and the spawned app inherited it, so it started with no main script and exited without a window. Earlier tests missed this because the launch target was a shell stub, which ignores the variable. spawnDetached now drops it, along with the AppImage variables.

Relationship to #42 (@Tsurgcom)

#42 landed first and covers packaging: deb, rpm, zip, AppImage and Arch. This PR overlaps it and goes wider (the export bug, the CLI, the app menu), so rather than leave two competing branches I folded the parts of #42 that are better than what I had into this one, credited on the commit (Co-authored-by: Tsurg). Maintainers: prefer #42's authorship where the work is theirs — I'm happy to rebase this onto it instead if that merges more easily.

Taken from #42: Arch support (adapted as above), the rpm ≥ 4.20 diagnosis — which would have broken my rpm build in CI and I had missed entirely — and StartupWMClass in the desktop entry.

Kept from this branch: makers over hand-written specs for deb/rpm/AppImage, so dependency lists, chrome-sandbox 4755, icons and lintian overrides come from the makers (only Arch, which has no maker, is a script); executableName: 'diffusion-studio' instead of working around the space per format; one desktop entry for all four formats (in #42 the rpm and Arch entries register diffusion:// and the deb's generated entry does not, which is the difference between deep links working and silently doing nothing on Debian/Ubuntu); and no ozone flag.

Two notes sent back to #42: the missing MimeType above, and that its publish-arch job runs as root in archlinux:latest while makepkg refuses to run as root — the version here creates a builder user and runs the step under sudo -u builder.

What was actually built and run

Artifact Result
Diffusion Studio-linux-x64 (electron-packager) Launches; renders correctly; new File/Edit/View/Window menu; executable is diffusion-studio
Shipped resources/cli/bin/dapi --version and fonts run on the packaged Electron in node mode
Diffusion Studio-0.204.1-x64.AppImage (128 MB) Launches from its mount, renders correctly, entry carries MimeType=x-scheme-handler/diffusion. Install-CLI driven through the real cli:install channel, then the app quit: the wrapper survives, dapi --version0.204.1, dapi fonts works, and dapi open relaunches the AppImage and opens the project
diffusion-studio-0.204.1-1-x86_64.pkg.tar.zst (Arch) Built with makepkg; root-owned, chrome-sandbox -rwsr-xr-x, both /usr/bin symlinks, shared desktop entry, dapi --version0.204.1 from the extracted tree
.deb / .rpm Not built here — no dpkg-deb/rpmbuild and no root to install them. DEBIAN/control and the .spec are verified by rendering them through the makers' own code; CI is the first archive build

Absent-dependency paths, tested rather than assumed: no FUSE/fusermount → the AppImage prints the FUSE hint and --appimage-extract-and-run starts the app; no fontconfigdapi fonts names the package to install, exit 1; no xdg-open with the app down → dapi open says "launch the app first", exit 1, no crash and no false success. GTK3/NSS/libcups are declared as deb Depends and Arch depends. The binary floor is Electron's, not this host's: the executable references at most GLIBC_2.25, so glibc ≥ 2.25 (Ubuntu 18.04+, Debian 10+, RHEL 8+). ~/.local/bin is on PATH on Debian and Fedora but not Arch, so the install dialog only claims dapi is ready when the link target is on the session PATH, and otherwise names the directory to add.

Checks

npm run check and npm run lint pass for every workspace touched (apps/web, apps/cli, apps/desktop, packages/encoder). Two pre-existing failures in this environment are unrelated and untouched: packages/assets/src/browser.ts showSaveFilePicker (present before these commits) and examples/ missing optional deps (three, redraw, @typegpu/noise).

Not covered: Windows. The AAC resolution helps it for free, but nothing else here was built or tested there.

…code

Every mp4 export template asks for AAC, and the encoder took that as a
demand. In WebCodecs, AAC is the platform's encoder — AudioToolbox on
macOS, Media Foundation on Windows — so Chromium on Linux has none, and
an export failed after the video track had already started encoding.

The codec asked for is now a preference: resolveAudioCodec keeps it when
the container takes it and the browser can encode it, and otherwise
picks the container's next encodable choice (Opus, which mp4 takes and
Chromium bundles everywhere). It is resolved at the three places an
export starts — the export panel, the dapi handler, and the encoder
itself — so the progress panel, the echoed dapi config, and the file all
name the same codec. The audio picker offers only codecs this machine
can actually encode.

Nothing changes where AAC exists: it is encodable, so it is what is
used.
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

@zbcoding is attempting to deploy a commit to the Diffusion Studio Team on Vercel.

A member of the Team first needs to authorize it.

@zbcoding
zbcoding marked this pull request as draft September 3, 2026 22:55
`npm run make` only had a darwin ZIP and the DMG, so a Linux checkout
could be packaged but never made into an installable artifact. Adds deb
and rpm makers plus a Linux ZIP, and scopes the DMG to darwin. The
FreeDesktop entry both packages share registers `x-scheme-handler/diffusion`,
which is what the auth and checkout deep links arrive on — the macOS
bundle gets that from packagerConfig.protocols.

The Linux build also renames the executable to `diffusion-studio`:
electron-packager keeps the space from `name`, which is fine inside a
.app bundle but not for the binary the packages symlink into /usr/bin.
The name stays untouched on macOS, where the CLI wrapper and the signing
pass both address `Contents/MacOS/Diffusion Studio`.

Two runtime gaps go with it: Chromium needs `--ozone-platform-hint=auto`
or it runs through XWayland on Wayland sessions, and `setupAppMenu` used
to return early off darwin, leaving the dapi installer unreachable
behind Electron's stock menu.

The release workflow gains a publish-linux job so the artifacts reach
the same draft release; it carries no Apple secrets.
Four macOS assumptions kept the CLI from working off darwin:

- `dapi open` launched the app with `open -a`, gated to darwin, so
  elsewhere it could only talk to an instance that was already up. Linux
  now starts the executable the packaged wrapper points at, then one on
  PATH, then hands `diffusion://` to xdg-open — the scheme the deb and
  rpm packages register. A failure still falls through to the socket and
  its existing error.
- `dapi fonts` hard-failed off darwin. It now enumerates through
  fontconfig, mapping fc-list's weight axis onto CSS weights and
  collapsing the one-line-per-file listing into families; the `local()`
  sources it emits are the same shape the macOS path builds, so <text>
  and the browser resolve them unchanged.
- The staged `cli/bin/dapi` wrapper hardcoded the .app bundle's Electron
  binary, so the shipped CLI could not run at all on Linux. It now picks
  the layout it sits in — the bundle path on macOS, byte for byte as
  before, or the flat packager tree beside `resources/`.
- The in-app installer linked into /usr/local/bin behind an osascript
  admin prompt. On Linux it links into ~/.local/bin, which the user owns,
  so nothing has to be elevated.
A deb and an rpm cover Debian/Ubuntu and Fedora; everywhere else there
was nothing to hand a user. An AppImage is one executable file that runs
without root or a package manager, so it is the artifact for the rest.
The maker needs `mksquashfs` on the build host, which the release
workflow now installs.

Both Linux packages and the AppImage share the FreeDesktop metadata, and
both now name the executable explicitly: `bin` defaults to the sanitized
package name (`diffusionstudio-desktop`), which is not what
electron-packager emits.

The in-app CLI installer refuses inside an AppImage rather than linking
into it: an AppImage runs from a mount under /tmp that disappears with
the process, so the link would dangle. The bundled dapi still works from
a running AppImage, and the message names the two options that persist —
the deb/rpm, or an unpacked AppImage.
@zbcoding zbcoding changed the title Linux support: fix mp4 export (no AAC encoder), package deb/rpm, and make dapi work Linux support: fix mp4 export (no AAC encoder), ship deb/rpm/AppImage, and make dapi work Sep 3, 2026
Dry-running the deb and rpm makers against the packaged tree, rather than
trusting their config, turned up two builds that could never have
succeeded, and comparing notes with diffusionstudio#42 turned up a third:

- The deb had no `Maintainer`, one of the five fields dpkg-deb requires.
  It defaults to `package.json`'s `author`, which this workspace does not
  set, and the control template silently omits the field when it is
  undefined - so the release job would have failed on its first run.
- `@electron-forge/maker-rpm` (electron-installer-redhat 3.4.0) writes a
  spec whose `%install` runs `cp -r usr/*` relative to the build
  directory. rpm 4.20 moved that directory, so the copy fails on Fedora
  41+ and on recent Debian and Ubuntu
  (electron-userland/electron-installer-redhat#343, electron/forge#3701).
  Diagnosed in diffusionstudio#42, which worked around it with a hand-written spec; this
  patches the template through the patch-package setup the repo already
  runs, so the maker keeps generating dependencies and metadata.
- MakerZIP shells out to `zip`, which is not installed everywhere. Named
  in the README with the other per-maker tools, and installed in CI.

Arch Linux support comes from diffusionstudio#42: it has no Electron Forge maker, so a
script stages the forge output and runs makepkg. The PKGBUILD drops the
launcher shim that PR needed, because the Linux executable is renamed to
`diffusion-studio` and /usr/bin can link straight to it, and it puts
`dapi` on PATH directly, which a package can do and the AppImage cannot.

All four formats now install one desktop entry, kept in packaging/linux
so the makers and the PKGBUILD cannot drift: two of the three formats in
diffusionstudio#42 registered `x-scheme-handler/diffusion` and the deb did not, which
is the difference between the auth and checkout deep links working and
silently doing nothing.

The CLI install dialog now only claims `dapi` is ready to run when the
link target is on the session PATH, and otherwise says which directory
to add - Debian and Fedora put ~/.local/bin there, Arch does not.

Co-authored-by: Tsurg <info@tsurg.com>
@zbcoding zbcoding changed the title Linux support: fix mp4 export (no AAC encoder), ship deb/rpm/AppImage, and make dapi work Linux support: fix mp4 export (no AAC encoder), ship deb/rpm/AppImage/Arch, and make dapi work Sep 4, 2026
…nheriting node mode

Two bugs, both found by installing the CLI from a real AppImage and then
quitting it - which is what a user does.

The install wrote a symlink into `resources` under the image's mount.
That mount lives under /tmp only while the app runs and its name changes
every launch, so the link dangled the moment the app quit. The image
file is stable and can run the CLI on its own Electron in node mode, so
the install now writes a wrapper that does that: the image mounts itself
for the duration of each call. The path inside the mount is read from
the running process rather than assumed, since the maker decides the
layout.

The wrapper runs the CLI with `ELECTRON_RUN_AS_NODE=1`, and a spawned
app inherited it - starting with no main script and exiting without a
window, so `dapi open` could never launch the app from any packaged
install, deb and rpm included. Earlier tests missed it because the
launch target was a shell stub, which ignores the variable.
`spawnDetached` now drops it, along with the AppImage variables, so a
launched image mounts itself afresh instead of reading this process's
mount.
@zbcoding

zbcoding commented Sep 4, 2026

Copy link
Copy Markdown
Author

#42 covers deb, rpm, zip, AppImage and Arch. This PR overlaps it and goes wider (the export bug, the CLI, the app menu). Credit on the commit (Co-authored-by: Tsurg).

Kept from this branch: makers over hand-written specs for deb/rpm/AppImage, so dependency lists, chrome-sandbox 4755, icons and lintian overrides come from the makers (only Arch, which has no maker, is a script); executableName: 'diffusion-studio' instead of working around the space per format; one desktop entry for all four formats (in #42 the rpm and Arch entries register diffusion:// and the deb's generated entry does not, which is the difference between deep links working and silently doing nothing on Debian/Ubuntu); and no ozone flag.

Two notes sent back to #42: the missing MimeType above, and that its publish-arch job runs as root in archlinux:latest while makepkg refuses to run as root — the version here creates a builder user and runs the step under sudo -u builder.

@zbcoding
zbcoding marked this pull request as ready for review September 5, 2026 06:04
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.

1 participant