Installing App Images as First Class Citizens in NixOS

NixOS is a great package manager, and it has a ton of packages available. I love installing new apps, because it’s just so darn easy.

Search for a package, find the name, add it to your config, rebuild… profit?

But what do you do if you don’t find a package? Shrug shoulders and find an alternative?

I find often times that the apps that don’t exist on Nix are usually AppImages. I don’t know why—perhaps because AppImages are an easy way to support virtually all Linux distros? Whatever the case, I have found several apps that don’t exist in Nix yet, but they do have app images.

But how does one install AppImages on Nix?

What I Used to Do with AppImages #

When I used to run Pop_OS!, I would use the App Image Integrator for app images. This made it pretty easy. Any time you manually run an appimage, the Integrator tool would ask if you want to install it permanently, or just run it once.

But, somewhat ironically, I couldn’t find this app in Nix. And more importantly, for me, it’s not declarative. I use Nix primarily [to keep my computers synced together](file:///posts/2024/my-experience-with-nixos/), and this app would break that. Even if I had it installed, any time I wanted to install an app image I would have to do it on both computers, which is no bueno.

So I did some digging, and discovered a solution that is both harder and easier than I would have expected.

Prerequisite: Enable AppImages #

NixOS doesn’t enable App Images by default, so if you want to use them, you need to add this to your config:

# enable appimage support
programs.appimage.enable = true;
programs.appimage.binfmt = true;

After rebuilding, you should then be able to download and run app images like you would on any other computer.

But that doesn’t help us integrate them. Downloaded app images won’t show up in your application launcher. So let’s fix that.

Turning an AppImage into a Derivation #

Behind the scenes, every Nix package is formatted as something called a “derivation”. Simply put, derivations are instructions on how to install a given package.

Every package needs these instructions, but they’re generally hidden from you and me. When you add a new package to your NixOS config, behind the scenes it is pulling in a new derivation.

Derivations are hidden right in front of our faces: when you search for a package on search.nixos.org, every package has a “source” link. If you click that, it will take you directly to the derivation for that package.

So if we want to integrate a random app image into our system, the best way that I’ve found is to write our own derivation.

Writing a derivation sounds hard and it looks intimidating. But fortunately you don’t have to fully understand what that means: I will give you the template that I found, which makes it as easy as possible to do this.

Here’s my basic AppImage derivation for NixOS:

{ pkgs, ... }: let
  pname = "cursor";
  version = "0.40.3";

  src = pkgs.fetchurl {
    url = "https://downloader.cursor.sh/linux/appImage/x64";
    hash = "sha256-CD6bQ4T8DhJidiOxNRgRDL4obfEZx7hnO0VotVb6lDc=";
  };
  appimageContents = pkgs.appimageTools.extract {inherit pname version src;};
in
    pkgs.appimageTools.wrapType2 {
      inherit pname version src;
      pkgs = pkgs;
      extraInstallCommands = ''
        install -m 444 -D ${appimageContents}/${pname}.desktop -t $out/share/applications
        substituteInPlace $out/share/applications/${pname}.desktop \
          --replace 'Exec=AppRun' 'Exec=${pname}'
        cp -r ${appimageContents}/usr/share/icons $out/share

        # unless linked, the binary is placed in $out/bin/cursor-someVersion
        # ln -s $out/bin/${pname}-${version} $out/bin/${pname}
      '';

      extraBwrapArgs = [
        "--bind-try /etc/nixos/ /etc/nixos/"
      ];

      # vscode likes to kill the parent so that the
      # gui application isn't attached to the terminal session
      dieWithParent = false;

      extraPkgs = pkgs: with pkgs; [
        unzip
        autoPatchelfHook
        asar
        # override doesn't preserve splicing https://github.com/NixOS/nixpkgs/issues/132651
        (buildPackages.wrapGAppsHook.override {inherit (buildPackages) makeWrapper;})
      ];
    }

To get a better idea of what this code is doing, check out the official docs on wrapping an AppImage.

This is a derivation that I created in order to install an AI powered text editor called Cursor. Most of this code will work regardless of the AppImage you want to use, you’ll just have to replace the Cursor specific bits. Those are these bits:

# update to the name of your app
pname = "cursor";

# update to the version of the app you are installing
version = "0.40.3"

# update to the url for downloading the appimage
# the hash will need to be updated too. I would leave this one in place and rebuild your system: Nix will give you an error and tell you the new hash you should be using
src = pkgs.fetchurl {
	url = "https://downloader.cursor.sh/linux/appImage/x64";
	hash = "sha256-CD6bQ4T8DhJidiOxNRgRDL4obfEZx7hnO0VotVb6lDc=";
};

Those three things might be all you need to update. If it doesn’t work, then take a look at the extraInstallCommands. This script assumes that the appimage has a appname.desktop file, and you might need to download the appimage and verify that this file exists in your package.

This script (with minor modifications) has worked for the three appimages that I have tried. Even if it doesn’t work for you, it should at least give you a good starting point.

Installing a Custom Derivation #

One more note about using these custom derivations. When I first tried this, I created a file called cursor.nix, pasted this code, and then tried to import it into my config like this:

imports = [
	./system/environment.nix
];

This method works for basic configuration, but not for derivations. Derivations need to be imported, like this:

{ pkgs, lib, inputs, ... }:

let 
	cursorApp = import ./apps/cursor.nix { inherit pkgs; };
in
{
	# ...

	environment.systemPackages = with pkgs; [
		cursorApp
	];
}

Note the path: I have my cursor config file in an apps folder. You can do that too, or put it somewhere else and update that path.

Conclusion #

Funny enough, in preparing for this article, I discovered that the Cursor app now exists in Nix packages, it’s called code-cursor. It has a more up-to-date version than the one I use, so I will likely update to that, and delete my custom solution.

But it’s a great thing to have in my back pocket, and it also gives me an easy way to downgrade packages, if I need to do that.

Derivations are still a bit mysterious and complicated to me, but I’m enjoying learning more about such an integral part of the Nix ecosystem.

← Home

Changelog
  • Fix typo
  • Publish new nixos article