Installing a Custom Package With Nix

Blue box with Christmas present and white snowflake.
BlogLink to blog
Blog
5 min read

A (relatively) quick guide to creating a custom package on Nix and NixOS. Image from https://www.pexels.com/photo/blue-box-with-christmas-present-and-white-snowflake-6102171/.


Contents


Introduction 

I’m writing this blog because I struggled with this seemingly simple problem for two days: how do I bundle up a custom package to install with Nix? Specifically: how do I download and “install” a binary file, plus a systemd service to manage it?

Context 

One of the most essential programs for my home server is Duplicacy 🔗, an application that backs up all my data to Backblaze B2. While Duplicacy’s command-line tool is open source and available in Nixpkgs 🔗, its web client is neither of those. I also couldn’t find any package definitions created by other Duplicacy users, so I thought “whatever, I’ll just make my own!”

Duplicacy’s web client is a single binary file. You simply need to download it 🔗, run chmod +x to make it executable, then run it. To manage it like a service on my current Debian installation, I moved the file to /usr/local/bin, created a systemd service file, and pointed it to the executable. This works fine on Debian, but Nix is a whole other beast.

Solution 

I’ll show what I did, then break it down later. There are two main files here:

  1. package.nix is where the package is defined.
  2. duplicacy-web.nix is where the service is defined, and where the package gets added to the system.

First, the package definition:

{ pkgs, lib }:

pkgs.stdenv.mkDerivation rec {
	pname = "duplicacy-web";
	version = "1.7.2";

	src = builtins.fetchurl {
		url = "https://acrosync.com/duplicacy-web/duplicacy_web_linux_x64_${version}";
		sha256 = "88383f7fea8462539cab7757dfa167bf42e37cbc19531b9de97373bc20efd317";
	};

	doCheck = false;

	dontUnpack = true;

	installPhase = ''
		install -D $src $out/duplicacy-web
		chmod a+x $out/duplicacy-web
	'';

	meta = with lib; {
		homepage = "https://duplicacy.com";
		description = "A new generation cloud backup tool";
		platforms = platforms.linux ++ platforms.darwin;
		#license = licenses.unfreeRedistributable;	# TODO: For some reason Nix refuses to install unfree packages despite being configured otherwise. Disable for now.
	};
}

Next, duplicacy-web.nix:

{ pkgs, config, lib, cfg, ... }:

let
	duplicacy-web = pkgs.callPackage ./package.nix { inherit pkgs lib; };
in
rec {
	environment.systemPackages = [
		duplicacy-web
	];

	# Install systemd service
	systemd.services."duplicacy-web" = {
		enable = true;
		wants = [ "network-online.target" ];
		after = [ "syslog.target" "network-online.target" ];
		description = "Start the Duplicacy backup service and web UI";
		serviceConfig = {
			Type = "simple";
			ExecStart = ''${duplicacy-web}/duplicacy-web''; 
			Restart = "on-failure";
			RestartSrc = 10;
			KillMode = "process";
			Environment = "HOME=${config.users.users.aires.home}";
		};
	};
}

Breakdown 

package.nix - defining the package itself 

Let’s start with package.nix. We create this as a derivation 🔗, which is sort of like a package blueprint. Once the blueprint is created, we can reference it in other Nix commands, for example, to install it to our system.

This derivation is simple: we name it “duplicacy-web,” pass a version number, then tell it how to fetch the file by providing a URL and SHA256 sum to verify the file’s integrity. Since this URL downloads a binary file, we don’t need to unpack anything, so we set dontUnpack = true;. Otherwise, Nix will complain that it doesn’t know how to unpack the file.

The installPhase tells Nix what to do with the file. All we need to do is tell Nix what to name the file and where to put it within the derivation, and then make it executable. These two lines do just that:

installPhase = ''
	install -D $src $out/duplicacy-web
	chmod a+x $out/duplicacy-web
'';

Lastly, we add some metadata about the package. This isn’t really necessary unless you’re looking to contribute this to nixpkgs. I ran into a problem here, though: duplicacy-web is proprietary software and has a nonfree license. Even though I supposedly enabled unfree licenses in my NixOS config 🔗, Nix still complained. So if you’re wondering why this is commented out, that’s why. Eventually I’ll figure it out.

meta = with lib; {
	homepage = "https://duplicacy.com";
	description = "A new generation cloud backup tool";
	platforms = platforms.linux ++ platforms.darwin;
	#license = licenses.unfreeRedistributable;	# TODO: For some reason Nix refuses to install unfree packages despite being configured otherwise. Disable for now.
};

duplicacy-web.nix - pulling the package into a module 

Here’s where we define what to do with the package. We’ll need to import this file into the rest of our configuration so it gets processed, but for now, let’s just focus on the contents.

First, we define duplicacy-web as the result of the derivation by calling our packaged.nix file. This step is what tripped me up the most, because I had absolutely no idea how to import or call a derivation from another file. I didn’t even know I needed to make a derivation to install a custom package. This single line is the result of many trial and error attempts. Don’t let its simplicity fool you!

duplicacy-web = pkgs.callPackage ../packages/duplicacy-web.nix { inherit pkgs lib; };

Next, we install duplicacy-web like any other package, then create a systemd service for it. Note that this systemd service doesn’t start automatically: this is deliberate due to how my server is set up. My server has a separate encrypted data partition that I manually unlock on each reboot. Since Duplicacy backs up that partition, I don’t want it mounted before Duplicacy starts, otherwise Very Bad Things™ could happen. So I have a startup script that mounts the partition first, and then starts the service. If you want the service to start automatically, you can add wantedBy = ["multi-user.target"] to start the service at boot, or wantedBy = ["default.target"] to have it start on login. See NixOS options 🔗 for details.

Also, the files that Duplicacy requires are stored in my user’s home directory, so within the service, I included an environment variable pointing to my home. If you want to reuse this service definition for yourself, make sure to replace ${config.users.users.aires.home} with the path to the directory containing your .duplicacy-web folder.

Importing the module into your system configuration 

That’s pretty much it. Just import duplicacy-web.nix in your main configuration file like so, and you’re done.

{ ... }: {
	imports = [
	    ./duplicacy-web.nix
	];

	# The rest of your configuration
}

When you run nixos-rebuild, it’ll automatically build the duplicacy-web derivation and the systemd service, and include them both in your system. The service will have the right path to the derivation and will start just fine (assuming you start it yourself or via script, like I do).

Good luck on your NixOS journey!

Previous: "NixOS: What is it, how does it work, and should you use it?"Next: "The Antidepressant That Changed My Life"
art atmospheric breaks breakbeat buddhism chicago code disco fediverse fiction funk furry house house music kubernetes lgbt linux logseq mastodon mental health movies music nixos obsidian personal philosophy pkm poetry prompt second life social software soul technology writing