Merge origin/main into #246; dev desktop entry is opt-in, icons not embedded

- Nothing is written unless FILMCRAFT_DEV_DESKTOP_ENTRY=1 (debug builds no longer write into
  ~/.local/share on every cargo run).
- The 256/512 PNGs are copied from this checkout's assets/app-icon at runtime instead of
  include_bytes! into every Linux binary; a missing icon is logged and skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-10 06:22:16 -04:00
parent e2ebbe715e
commit e19eaa760b
2 changed files with 38 additions and 20 deletions

View File

@ -6,17 +6,17 @@
//! (`packaging/linux/ai.storyteller.filmcraft.desktop` and the hicolor PNGs) — but a source-tree
//! run has neither, so KDE/GNOME fall back to the generic Wayland icon. The fix is to ensure a
//! user-level entry for those runs: when no entry for the app id exists in any XDG data dir,
//! install one (plus the hicolor icon PNGs, embedded so it works whatever the working directory
//! is) into `$XDG_DATA_HOME`/`~/.local/share`. An existing packaged entry is never overridden;
//! the user-owned dev copy is rewritten when it points at another binary (a dev build moves
//! between checkouts). The shell picks the icon up at the *next* app start;
//! install one (plus the hicolor icon PNGs, read from this checkout's `assets/app-icon`, so
//! nothing is embedded in the binary) into `$XDG_DATA_HOME`/`~/.local/share`. An existing
//! packaged entry is never overridden; the user-owned dev copy is rewritten when it points at
//! another binary (a dev build moves between checkouts). The shell picks the icon up at the *next* app start;
//! `update-desktop-database`/`kbuildsycoca` is not needed for icon lookup.
//!
//! Only source-tree runs install anything: a debug build, or a release build with
//! `FILMCRAFT_DEV_DESKTOP_ENTRY` set (the docs' `cargo run --release` flow). Packaged runs are
//! excluded — AppImage (`APPIMAGE`) mounts at a fresh `/tmp/.mount_*` each launch, so its
//! `Exec` path changes every time and it ships its own entry, and a Flatpak (`FLATPAK_ID`)
//! installs its entry through the manifest. `packaging/linux/install.sh` writes the packaged
//! Opt-in only: nothing is written unless `FILMCRAFT_DEV_DESKTOP_ENTRY=1` is set, so ordinary
//! `cargo run`s (agents, worktrees, CI) never touch the user's profile. Packaged runs are
//! excluded even then — AppImage (`APPIMAGE`) mounts at a fresh `/tmp/.mount_*` each launch,
//! so its `Exec` path changes every time and it ships its own entry, and a Flatpak
//! (`FLATPAK_ID`) installs its entry through the manifest. `packaging/linux/install.sh` writes the packaged
//! `Exec=` the same two-layer way as here; keep the two in sync.
//!
//! Every failure is logged and skipped — a missing dev icon must never stop the app
@ -30,9 +30,10 @@ pub fn ensure_dev_desktop_entry() {
if std::env::var_os("APPIMAGE").is_some() || std::env::var_os("FLATPAK_ID").is_some() {
return;
}
// Dev builds only: a debug build, or a release run opted in — a packaged release must not
// touch the profile (the packages and install.sh install the real entry).
if !cfg!(debug_assertions) && std::env::var_os("FILMCRAFT_DEV_DESKTOP_ENTRY").is_none() {
// Opt-in only: a plain `cargo run` (agents, worktrees, other checkouts) must not write into
// the user's profile, and a packaged release never does (packages and install.sh install
// the real entry).
if std::env::var_os("FILMCRAFT_DEV_DESKTOP_ENTRY").is_none_or(|v| v != "1") {
return;
}
if let Err(e) = ensure() {
@ -71,13 +72,20 @@ fn ensure() -> Result<(), DevIconError> {
}
break; // a dev entry from another checkout: fall through and rewrite it
}
for (size, png) in ICONS {
let dest = user_dir.join("icons").join("hicolor").join(format!("{size}x{size}")).join("apps").join(format!("{}.png", crate::APP_ID));
for size in ICON_SIZES {
let rel = std::path::Path::new("hicolor").join(format!("{size}x{size}")).join("apps").join(format!("{}.png", crate::APP_ID));
let dest = user_dir.join("icons").join(&rel);
if !dest.is_file()
&& let Some(parent) = dest.parent()
{
// The checkout this binary was built from; a missing icon only costs the icon.
let src = std::path::Path::new(ICON_DIR).join(&rel);
if !src.is_file() {
log::warn!("dev desktop entry: icon {} not found; skipping it", src.display());
continue;
}
std::fs::create_dir_all(parent).map_err(DevIconError::Io)?;
std::fs::write(&dest, png).map_err(DevIconError::Io)?;
std::fs::copy(&src, &dest).map_err(DevIconError::Io)?;
}
}
let applications = user_dir.join("applications");
@ -89,10 +97,11 @@ fn ensure() -> Result<(), DevIconError> {
/// The hicolor icon sizes installed for the taskbar (a subset large enough for panel and topbar;
/// the shell picks the nearest size, so 256 and 512 cover both).
const ICONS: &[(u16, &[u8])] = &[
(256, include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.filmcraft.png")),
(512, include_bytes!("../../../assets/app-icon/hicolor/512x512/apps/ai.storyteller.filmcraft.png")),
];
const ICON_SIZES: [u16; 2] = [256, 512];
/// The app icons in the source tree this binary was built from (a path, not embedded bytes:
/// the entry is a dev-checkout convenience and release binaries must not carry the PNGs).
const ICON_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/app-icon");
/// The user data dir we install into: `$XDG_DATA_HOME`, else `$HOME/.local/share` (XDG base
/// directory spec). Untrusted environment: an empty or relative `XDG_DATA_HOME` means the
@ -250,6 +259,15 @@ mod tests {
/// The entry mirrors the packaged one, derived from the real file so the two cannot drift:
/// minus `TryExec` (a dev build isn't on `$PATH`), plus the quoted dev `Exec` path.
/// The icons the entry installs exist in the checkout.
#[test]
fn icon_sources_exist_in_the_checkout() {
for size in ICON_SIZES {
let p = std::path::Path::new(ICON_DIR).join(format!("hicolor/{size}x{size}/apps/ai.storyteller.filmcraft.png"));
assert!(p.is_file(), "{}", p.display());
}
}
#[test]
fn desktop_file_matches_the_packaged_fields() {
let packaged = include_str!("../../../packaging/linux/ai.storyteller.filmcraft.desktop");

View File

@ -40,7 +40,7 @@ cargo xtask web --serve 8765 # the web app on http://12
|---|---|
| `FILMCRAFT_CONTROL_PORT` | Same as `--control <port>` |
| `FILMCRAFT_CPU_COMPOSITE=1` | Disables the GPU compositor |
| `FILMCRAFT_DEV_DESKTOP_ENTRY=1` | A release run from a source tree installs the user-level desktop entry for the taskbar icon (a debug build always does; packaged runs never) |
| `FILMCRAFT_DEV_DESKTOP_ENTRY=1` | Linux, opt-in: a run from a source tree installs a user-level desktop entry (and the icons from `assets/app-icon`) so the taskbar shows the FilmCraft icon. Nothing is written without it; packaged runs never write one |
| `FILMCRAFT_DATA_DIR` | Same as `--data-dir <dir>`: auto-save, crash recovery, settings and logs |
| `RUST_LOG` | Log levels for standard error and the log file (see [Logs](#logs)) |