fix(desktop): use desktop.app.name for Linux .desktop Name=

`desktop.app.name` only filled in the output path when none was set, so
with an explicit output (e.g. `./dist/aiuse.rpm`) the configured name was
dropped and every Linux `.desktop` entry used the output file stem for
`Name=`.

Keep the configured name as a separate display name and use it for
`Name=` in the app dir, AppImage, `.deb` and `.rpm` desktop entries. The
package name, launcher, `Exec=` and icon names still come from the output
path.

Fixes #36776
This commit is contained in:
dualfroz committed 2026-09-29 11:59:56 +02:00
1 parent 3fda863616
commit 951237cce7
3 files changed
+216 -20

No files matched your search

+211 -20
View File
@@ -140,10 +140,13 @@ fn apply_desktop_config_to_flags(
});
}
if let Some(name) = app_config.name
&& desktop_flags.output.is_none()
{
desktop_flags.output = Some(name);
if let Some(name) = app_config.name {
if desktop_flags.output.is_none() {
desktop_flags.output = Some(name.clone());
}
if desktop_flags.display_name.is_none() {
desktop_flags.display_name = Some(name);
}
}
if let Some(identifier) = app_config.identifier
@@ -588,6 +591,7 @@ async fn compile_desktop(
create_linux_appimage(
&bundle_path,
&appimage_abs,
desktop_flags.display_name.as_deref(),
desktop_flags.target.as_deref(),
)?;
appimage_abs
@@ -1875,18 +1879,11 @@ async fn package_linux_app_dir(
"skipping .desktop file: {e} (desktop file IDs follow the same reverse-DNS rules as macOS bundle IDs)"
);
} else {
let desktop_entry = format!(
"[Desktop Entry]\n\
Type=Application\n\
Name={app_name}\n\
Exec={app_name}\n\
Icon=AppIcon\n\
StartupWMClass={desktop_id}\n\
Categories=Utility;\n",
);
let display_name =
desktop_flags.display_name.as_deref().unwrap_or(&app_name);
std::fs::write(
app_dir.join(format!("{desktop_id}.desktop")),
desktop_entry,
app_dir_desktop_entry(display_name, &app_name, &desktop_id),
)?;
}
@@ -1899,6 +1896,25 @@ async fn package_linux_app_dir(
Ok(app_dir)
}
/// `.desktop` entry written inside the Linux app dir. `display_name` is the
/// human-readable `Name=` (`desktop.app.name`), while `launcher` is the
/// executable/app dir name derived from the output path.
fn app_dir_desktop_entry(
display_name: &str,
launcher: &str,
desktop_id: &str,
) -> String {
format!(
"[Desktop Entry]\n\
Type=Application\n\
Name={display_name}\n\
Exec={launcher}\n\
Icon=AppIcon\n\
StartupWMClass={desktop_id}\n\
Categories=Utility;\n",
)
}
/// Environment variable pointing at a local laufey checkout, used to bypass the
/// download path during development. Build-tree subpaths under this directory
/// are searched the same way the old sibling-checkout heuristic searched.
@@ -3645,6 +3661,7 @@ fn push_dir_contents_to_squashfs(
fn create_linux_appimage(
app_dir: &Path,
appimage_path: &Path,
display_name: Option<&str>,
target: Option<&str>,
) -> Result<(), AnyError> {
use std::io::Cursor;
@@ -3654,6 +3671,7 @@ fn create_linux_appimage(
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "App".to_string());
let display_name = display_name.unwrap_or(&app_name);
let runtime_elf = appimage_runtime_for_target(target)?;
@@ -3686,7 +3704,7 @@ fn create_linux_appimage(
let desktop_entry = format!(
"[Desktop Entry]\n\
Type=Application\n\
Name={app_name}\n\
Name={display_name}\n\
Exec={app_name}\n\
Icon={app_name}\n\
Categories=Utility;\n",
@@ -3787,9 +3805,12 @@ struct LinuxPackageMeta {
/// Used as the package name, the `/usr/bin` symlink, and the icon/`.desktop`
/// basenames.
package: String,
/// Human display name (the staged app dir's name), used for `Name=` in the
/// `.desktop` entry and the package summary.
/// The staged app dir's name, which is also the launcher's file name
/// inside it. Used for the package summary and the `/usr/bin` symlink.
app_name: String,
/// Human-readable name for `Name=` in the `.desktop` entry: the configured
/// `desktop.app.name`, falling back to `app_name`.
display_name: String,
version: String,
maintainer: String,
summary: String,
@@ -3853,6 +3874,10 @@ fn linux_package_meta(
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "App".to_string());
let package = debian_package_name(&app_name);
let display_name = desktop_flags
.display_name
.clone()
.unwrap_or_else(|| app_name.clone());
let identifier = desktop_flags
.identifier
.clone()
@@ -3863,6 +3888,7 @@ fn linux_package_meta(
version: linux_package_version(config_version)?,
package,
app_name,
display_name,
identifier,
})
}
@@ -3945,12 +3971,12 @@ fn system_desktop_entry(meta: &LinuxPackageMeta) -> String {
format!(
"[Desktop Entry]\n\
Type=Application\n\
Name={app_name}\n\
Name={display_name}\n\
Exec={package}\n\
Icon={package}\n\
StartupWMClass={identifier}\n\
Categories=Utility;\n",
app_name = meta.app_name,
display_name = meta.display_name,
package = meta.package,
identifier = meta.identifier,
)
@@ -7387,6 +7413,7 @@ def456 other.zip
engine: Default::default(),
all_targets: false,
identifier: None,
display_name: None,
deep_links: Vec::new(),
codesign_identity: None,
inspect_renderer: None,
@@ -7409,7 +7436,7 @@ def456 other.zip
let app_dir = fake_linux_app_dir(tmp.path(), "MyApp");
let appimage_path = tmp.path().join("MyApp.AppImage");
let target = Some("x86_64-unknown-linux-gnu");
create_linux_appimage(&app_dir, &appimage_path, target).unwrap();
create_linux_appimage(&app_dir, &appimage_path, None, target).unwrap();
let runtime_offset =
appimage_runtime_for_target(target).unwrap().len() as u64;
@@ -8360,4 +8387,168 @@ def456 other.zip
// Left unset; callers fall back to "webview" via unwrap_or("webview").
assert_eq!(flags.backend.as_deref(), None);
}
// --- desktop.app.name as the Linux `.desktop` display name (#36776) ---
fn app_name_config(name: &str) -> DesktopConfig {
DesktopConfig {
app: Some(deno_config::deno_json::DesktopAppConfig {
name: Some(name.to_string()),
..Default::default()
}),
..Default::default()
}
}
#[test]
fn app_name_is_kept_as_display_name_with_explicit_output() {
let mut flags = DesktopFlags {
source_file: "report.ts".to_string(),
output: Some("./dist/aiuse.rpm".to_string()),
..Default::default()
};
apply_desktop_config_to_flags(&mut flags, app_name_config("AI Usage"));
assert_eq!(flags.output.as_deref(), Some("./dist/aiuse.rpm"));
assert_eq!(flags.display_name.as_deref(), Some("AI Usage"));
}
#[test]
fn app_name_fills_output_and_display_name_without_output() {
let mut flags = DesktopFlags {
source_file: "report.ts".to_string(),
..Default::default()
};
apply_desktop_config_to_flags(&mut flags, app_name_config("AI Usage"));
assert_eq!(flags.output.as_deref(), Some("AI Usage"));
assert_eq!(flags.display_name.as_deref(), Some("AI Usage"));
}
#[test]
fn system_desktop_entry_uses_display_name_for_name_only() {
let mut flags = empty_desktop_flags();
flags.display_name = Some("AI Usage".to_string());
flags.identifier = Some("io.github.sigmasd.aiuse".to_string());
let meta =
linux_package_meta(Path::new("/tmp/dist/aiuse"), &flags, None).unwrap();
assert_eq!(
system_desktop_entry(&meta),
"[Desktop Entry]\n\
Type=Application\n\
Name=AI Usage\n\
Exec=aiuse\n\
Icon=aiuse\n\
StartupWMClass=io.github.sigmasd.aiuse\n\
Categories=Utility;\n",
);
}
#[test]
fn app_dir_desktop_entry_uses_display_name_for_name_only() {
assert_eq!(
app_dir_desktop_entry("AI Usage", "aiuse", "io.github.sigmasd.aiuse"),
"[Desktop Entry]\n\
Type=Application\n\
Name=AI Usage\n\
Exec=aiuse\n\
Icon=AppIcon\n\
StartupWMClass=io.github.sigmasd.aiuse\n\
Categories=Utility;\n",
);
}
#[test]
fn linux_desktop_entry_name_falls_back_to_app_dir_name() {
let meta = linux_package_meta(
Path::new("/tmp/dist/aiuse"),
&empty_desktop_flags(),
None,
)
.unwrap();
assert!(system_desktop_entry(&meta).contains("\nName=aiuse\n"));
}
#[test]
fn appimage_desktop_entry_uses_display_name() {
let tmp = tempfile::tempdir().unwrap();
let app_dir = fake_linux_app_dir(tmp.path(), "aiuse");
let appimage_path = tmp.path().join("aiuse.AppImage");
let target = Some("x86_64-unknown-linux-gnu");
create_linux_appimage(&app_dir, &appimage_path, Some("AI Usage"), target)
.unwrap();
let runtime_offset =
appimage_runtime_for_target(target).unwrap().len() as u64;
let appimage =
std::io::BufReader::new(std::fs::File::open(&appimage_path).unwrap());
let filesystem = backhand::FilesystemReader::from_reader_with_offset(
appimage,
runtime_offset,
)
.unwrap();
let mut desktop_entry = String::new();
for node in filesystem.files() {
if node.fullpath == Path::new("/aiuse.desktop")
&& let backhand::InnerNode::File(file) = &node.inner
{
filesystem
.file(file)
.reader()
.read_to_string(&mut desktop_entry)
.unwrap();
}
}
assert!(
desktop_entry.contains("\nName=AI Usage\nExec=aiuse\nIcon=aiuse\n"),
"desktop entry:\n{desktop_entry}"
);
}
#[test]
fn deb_desktop_entry_uses_display_name() {
let tmp = tempfile::tempdir().unwrap();
let app_dir = fake_linux_app_dir(tmp.path(), "aiuse");
let deb = tmp.path().join("aiuse.deb");
let mut flags = empty_desktop_flags();
flags.display_name = Some("AI Usage".to_string());
create_linux_deb(
&app_dir,
&deb,
&flags,
Some("x86_64-unknown-linux-gnu"),
None,
)
.unwrap();
let bytes = std::fs::read(&deb).unwrap();
let data_tar_gz = {
let mut pos = 8;
let mut last = Vec::new();
while pos + 60 <= bytes.len() {
let header = &bytes[pos..pos + 60];
let size: usize = String::from_utf8_lossy(&header[48..58])
.trim()
.parse()
.unwrap();
last = bytes[pos + 60..pos + 60 + size].to_vec();
pos += 60 + size + (size % 2);
}
last
};
let data_tar = gunzip(&data_tar_gz);
let mut archive = tar::Archive::new(&data_tar[..]);
let mut desktop_entry = String::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
if entry.path().unwrap().to_string_lossy()
== "usr/share/applications/aiuse.desktop"
{
entry.read_to_string(&mut desktop_entry).unwrap();
}
}
assert!(
desktop_entry.contains("\nName=AI Usage\nExec=aiuse\nIcon=aiuse\n"),
"desktop entry:\n{desktop_entry}"
);
}
}
+1
View File
@@ -2712,6 +2712,7 @@ fn desktop_parse(
backend: result.get_one("backend").map(|s| s.to_string()),
all_targets: result.get_bool("all-targets"),
identifier: None,
display_name: None,
deep_links: Vec::new(),
codesign_identity: None,
inspect_renderer,
+4
View File
@@ -258,6 +258,10 @@ pub struct DesktopFlags {
/// identifier, and (eventually) the Windows AppUserModelID. When unset
/// a synthetic `com.deno.desktop.<app-slug>` is generated.
pub identifier: Option<String>,
/// Human-readable app name from `desktop.app.name` in `deno.json`. Used for
/// the Linux `.desktop` `Name=` entry. Kept separate from `output`, which
/// only falls back to this name when no output path is configured.
pub display_name: Option<String>,
/// Custom URL schemes (deep links) to register with the OS, e.g. `["acme"]`
/// for `acme://...` links. Sourced from `desktop.app.deepLinks` in
/// `deno.json`. Each entry is a bare scheme with no `://`.