G
YouTube TikTok Games
Skip to content

Add desktop entries for AppImage casks - #24343

Merged
MikeMcQuaid merged 1 commit into
Homebrew:mainfrom
syntactic-spice:appimage-desktop-entries
Oct 27, 2026
Merged

MikeMcQuaid merged 1 commit into
Homebrew:mainfrom
syntactic-spice:appimage-desktop-entries

Conversation

@syntactic-spice

@syntactic-spice syntactic-spice commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Please also see this discussion.

Currently, AppImage casks (Linux) copy the executable, but do not extract their embedded desktop entries. That means the applications never appear in the launcher/menu.

  • To make AppImage casks on Linux appear in the desktop application menu, install each root-level embedded desktop entry into ${HOMEBREW_XDG_DATA_HOME:-~/.local/share}/applications.
  • Keep the embedded names so URL handlers, dock pins and notifications that look up the app's desktop ID keep working.
  • Point every Exec at the AppImage through env DESKTOPINTEGRATION=1, keeping its variables and arguments.
  • Point Icon at the icon copied into the versioned Caskroom, falling back to .DirIcon.
  • Track file ownership with symlinks from the Caskroom: fail on an existing entry unless --force or --adopt, and rewrite it on upgrade.

There was an earlier attempt (#23674), where an objection was raised about writing to the user's home directory. But I think that's moot now, since after #23771 AppImages themselves are written to the home dir and fonts have always been written to the home dir on Linux too.

More details

  • Which entries: we run the AppImage's own --appimage-extract '*.desktop' and install every top-level desktop entry. A candidate counts only if it is a regular file of at most 64 KiB whose first non-blank, non-comment line is [Desktop Entry]. Some *.desktop files are not entries: opencode-desktop's ai.opencode.desktop is its 228 MB executable, and its entry is ai.opencode.desktop.desktop.
  • File names: the embedded names are kept, since URL handlers, dock pins and Wayland window matching look apps up by desktop ID. This was investigated here.
  • Exec: every Exec line, including [Desktop Action] groups, is rewritten to run the AppImage in ~/Applications. The entry's own variables and arguments are kept and DESKTOPINTEGRATION=1 is set, because some AppRun scripts integrate themselves into the desktop otherwise. For example:
    • Exec=env GDK_BACKEND=x11 UBUNTU_MENUPROXY=0 audacity %F becomes Exec=env DESKTOPINTEGRATION=1 GDK_BACKEND=x11 UBUNTU_MENUPROXY=0 "<AppImage>" %F
  • TryExec is also pointed to the AppImage.
  • Icons: the icon named by Icon= is extracted from the top of the image, following symlinks into the image. If it isn't found, we retry without a known extension, then falls back to .DirIcon. The icon is copied into the versioned Caskroom folder and Icon= points at it by absolute path.

File ownership

  • Tracking: The filenames are not predictable, but each installed entry is linked from the versioned Caskroom folder. Uninstall removes the files those links point to.
  • Conflicts: if an entry with the same name already exists, the install fails before the AppImage is moved, unless --force or --adopt is passed. If replacing an existing entry, that entry's ownership is transferred to the new cask by deleting any existing links to it from another cask (can be the case
  • Upgrades rewrite the entries.

  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include brew benchmark results.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?
  • Have you written new tests (excluding integration tests)? Here's an example.

  • I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

Claude Opus 5.5 was used for the initial implementation and iterations after manual review. It was also used to gather empirical evidence from the current ~250 available casks about edge cases and common non-spec-compliant patterns. It was used to analyze appimaged (reference implementation) and Gear Lever (popular AppImage manager) code to check how they handle those patterns. And to write an app to let me manually test behavior of different desktop environments to make some decisions.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Summary

Summary by CodeRabbit

  • New Features
    • AppImage installation now adds embedded desktop entries and supported icons to the user’s applications directory, with launch commands pointing to the installed app.
    • Installation checks for conflicts with existing desktop entries and handles them during forced installs and upgrades.
    • MIME caches are refreshed when affected entries declare MIME types.
    • Uninstalling an AppImage removes its associated desktop entries and links.
  • Documentation
    • Updated the Cask Cookbook with details about AppImage installation and desktop entries.

AppImage installation now extracts embedded desktop entries and icons, rewrites launch commands, and installs entries in the user applications directory. Uninstall removes installed desktop entries and refreshes MIME caches when applicable.

Changes: AppImage desktop integration

Layer / File(s) Summary
Parse and edit desktop entries
Library/Homebrew/cask/desktop_file_parser.rb, Library/Homebrew/test/cask/desktop_file_parser_spec.rb
Cask::DesktopFileParser reads and updates desktop-entry groups and keys. It rewrites Exec and TryExec values. Specs cover parsing, updates, serialisation, and errors.
Prepare and install desktop entries
Library/Homebrew/cask/artifact/appimage.rb, Library/Homebrew/test/support/fixtures/cask/naked.AppImage, Library/Homebrew/test/support/fixtures/cask/Casks/with-appimage.rb, Library/Homebrew/test/cask/artifact/appimage_spec.rb, docs/Cask-Cookbook.md
Installation extracts desktop entries and icons, rewrites launch commands, checks for host conflicts, and installs entries in the user applications directory. Fixtures and specs cover installation cases. The cookbook describes the installation path and embedded entries.
Remove desktop entries
Library/Homebrew/cask/artifact/appimage.rb, Library/Homebrew/test/cask/artifact/appimage_spec.rb
Uninstall removes staged links and host desktop entries. It refreshes MIME caches when applicable. Specs cover removal and cache handling.

Priority: Normal

Merge Risk: Low · up to 18960

Users who configure the documented desktop-entry location may find entries installed elsewhere. Correct the documentation or configuration behavior; the mismatch is bounded and does not otherwise block merging.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: adding desktop entries for AppImage casks.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description directly explains the AppImage desktop-entry extraction, rewriting, icon handling, ownership tracking, conflict handling, and test coverage implemented by the changeset.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f09aa3c3-bcc3-4794-99fd-ab43b368eed3
📥 Commits

Reviewing files that changed from the base of the PR and between 7a056ff and b08e705.

📒 Files selected for processing: 5
  • Library/Homebrew/cask/artifact/appimage.rb
  • Library/Homebrew/test/cask/artifact/appimage_spec.rb
  • Library/Homebrew/test/support/fixtures/cask/Casks/with-appimage.rb
  • Library/Homebrew/test/support/fixtures/cask/naked.AppImage
  • docs/Cask-Cookbook.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread docs/Cask-Cookbook.md Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (2)

Minor · Preserve env options in rewritten Exec values. · appimage.rb:203-222

Library/Homebrew/cask/artifact/appimage.rb:203-222
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve env options in rewritten Exec values.

For Exec=env --unset=FOO AppRun %U, desktop_entry_exec treats --unset=FOO as the old executable. The rewritten command drops the option and passes AppRun as an argument to the AppImage. This violates the requirement to retain environment settings and arguments.

Parse leading env options before selecting the original executable. Keep them before DESKTOPINTEGRATION=1.

Suggested fix
         words = exec.scan(/(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s"])+/).map(&:to_s)
         words.shift if words.first == "env"
+        env_options = words.take_while { |word| word.start_with?("-") }
+        words = words.drop(env_options.length)
         variables = words.take_while { |word| word.match?(/\A[A-Za-z_]\w*=/) }
         arguments = words.drop(variables.length + 1)
         variables.reject! { |variable| variable.start_with?("DESKTOPINTEGRATION=") }
-        [exec_environment, *variables, exec_path, *arguments].join(" ")
+        environment = if env_options.empty?
+          exec_environment
+        else
+          exec_environment.sub(/\Aenv\b/, "env #{env_options.join(" ")}")
+        end
+        [environment, *variables, exec_path, *arguments].join(" ")
Minor · Rebuild the MIME cache after removing desktop entries. · appimage.rb:163-172

Library/Homebrew/cask/artifact/appimage.rb:163-172
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Rebuild the MIME cache after removing desktop entries.

When an installed MIME-bearing entry is removed, applications/mimeinfo.cache can still list its desktop file. Applications that use this cache can offer the uninstalled AppImage as a MIME handler, then fail because its desktop entry no longer exists.

Suggested fix
         cask.staged_path.glob("*.desktop").select(&:symlink?).each do |desktop_entry_link|
           desktop_entry = desktop_entry_link.readlink
           if desktop_entry.exist? || desktop_entry.symlink?
             ohai "Removing desktop entry '#{desktop_entry}'"
             desktop_entry.unlink
           end
           desktop_entry_link.unlink
         end
+
+        return unless (desktop_entry_dir/"mimeinfo.cache").exist?
+        return unless (update_desktop_database = which("update-desktop-database"))
+
+        command.run(update_desktop_database, args: [desktop_entry_dir], print_stderr: false)

Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 45cd8d6e-1053-42f9-8365-ef00b34023b5
Commits

Reviewing files that changed from the base of the PR and between b08e705 and cc43583.

Files selected for processing: 3
  • Library/Homebrew/cask/artifact/appimage.rb
  • Library/Homebrew/test/cask/artifact/appimage_spec.rb
  • docs/Cask-Cookbook.md
Files skipped from review as they are similar to previous changes: 2
  • docs/Cask-Cookbook.md
  • Library/Homebrew/cask/artifact/appimage.rb

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I like the idea but unfortunately the logic here is way too brittle as it's relying on a variety of hard-to-follow regex to actually parse and make changes to a file format.

Instead, look for libraries and/or tools, provided by ideally the XDG system and if not Homebrew, we can rely on here instead to actually parse and read these files.

@syntactic-spice

syntactic-spice commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the PR! I like the idea but unfortunately the logic here is way too brittle as it's relying on a variety of hard-to-follow regex to actually parse and make changes to a file format.

Instead, look for libraries and/or tools, provided by ideally the XDG system and if not Homebrew, we can rely on here instead to actually parse and read these files.

Thanks, that's a fair point. We can use desktop-file-utils to do the parsing, but the format itself is quite simple:

[Desktop Entry]
Exec=<launch command>
Icon=<filename>
...

The complexity is really in two regexes

  1. Quoting the launch path (one you identified above)
  2. Parsing the <launch command> itself (E.g. Exec=env FOO=bar BAZ="foo bar" executable --arg=foo ...) and identifying the executable name. This is the most complex one: words = exec.scan(/(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s"])+/).map(&:to_s)
  3. Then there are simple ones that just check if the line starts with Icon= etc.

Second can be replaced entirely with Ruby's Shellwords. Third can be replaced with a parser from desktop-file-utils etc. But there doesn't seem to be a way to replace the first one. Does that sound reasonable?

Fwiw, I did test with all 250 current appimages before submitting :)

Edit: Maybe we could replace the first too if we pointed the desktop entry to a symlink from the caskroom composed only of safe characters. That's assuming the user didn't use a custom brew prefix.

@MikeMcQuaid

Copy link
Copy Markdown
Member

@syntactic-spice leaning on shellwords sounds good. I think a dedicated desktop_file_parser.rb would make this easier to follow? Also looks close enough to git config I wonder if we could use that? desktop-file-utils is appealing but a bit heavyweight. Thanks!

@syntactic-spice

Copy link
Copy Markdown
Contributor Author

@MikeMcQuaid Please take a look. Unfortunately git config and TOML are different enough that their parsers won't work without substitution hacks.

Added separate desktop_file_parser.rb which is hopefully much easier to follow. Used Shellwords to replace the large regex. Tested parser behavior against desktop-file-utils, GLib (used by GTK-based desktops) and kreadconfig (used by KDE) on the 250 AppImage casks, and it's identical as far as I can tell.

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much easier to follow now thanks! A few more requests:

  • please inline any methods only used once
  • try to move any more desktop-file-specific logic into desktop_file_parser
  • in general let's see if you can slim the diff here a bit more just to the specific case(s) you specifically need for now

@syntactic-spice
syntactic-spice force-pushed the appimage-desktop-entries branch from 9074704 to 1c5d543 Compare October 26, 2026 16:06
@syntactic-spice

syntactic-spice commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks. Inlined methods and moved all desktop-file-specific logic to desktop_file_parser.rb. All appimage-specific logic is still in the main file. Removed the fallback to a minimal desktop entry, since no current cask needs it. Tried to remove the .DirIcon fallback too, but about 10 casks need it, including vscodium which I imagine is popular.

Edit: removed some overlapping tests

- To make AppImage casks on Linux appear in the desktop application
  menu, install each root-level embedded desktop entry into
  `${HOMEBREW_XDG_DATA_HOME:-~/.local/share}/applications`.
- Keep the entry's filename so URL handlers, dock pins and notifications
  that look up the app's desktop ID keep working.
- Add `Cask::DesktopFileParser` to parse and manipulate desktop files.
- Point every `Exec` at the AppImage through `env DESKTOPINTEGRATION=1`,
  keeping its variables and arguments.
- Point `Icon` at the named icon copied into the versioned Caskroom,
  falling back to `.DirIcon`.
- Track ownership with Caskroom symlinks, fail on an existing entry
  unless `--force` or `--adopt`, and rewrite it on upgrade.
@syntactic-spice
syntactic-spice force-pushed the appimage-desktop-entries branch from 1c5d543 to 18960f3 Compare October 26, 2026 18:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔇 Additional comments: 2
Library/Homebrew/test/cask/artifact/appimage_spec.rb: 1

89-89: 🎯 Functional Correctness

No per-example ENV cleanup is required.

Library/Homebrew/test/spec_helper.rb snapshots ENV before each example and restores it in the example teardown. The assignments in this spec therefore do not affect later examples.

Library/Homebrew/test/cask/desktop_file_parser_spec.rb-135-135: 1

135-135: Minor · Functional Correctness · Quick win

⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Assert that TryExec is removed.

The PR objective requires removal of TryExec, but this assertion requires replace_program to retain it. The parser also rewrites the key rather than removing it. Update the parser and assert that get("TryExec") returns nil.


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a745c787-8652-4bca-b7de-123bb3d52941
Commits

Reviewing files that changed from the base of the PR and between 1c5d543 and 18960f3.

Files selected for processing: 2
  • Library/Homebrew/test/cask/artifact/appimage_spec.rb
  • Library/Homebrew/test/cask/desktop_file_parser_spec.rb

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@MikeMcQuaid
MikeMcQuaid added this pull request to the merge queue Oct 27, 2026
Merged via the queue into Homebrew:main with commit 9999947 Oct 27, 2026
54 checks passed
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