Back Original

Humbly Maintaining a NixOS Package

- 4 mins read

I’ve got a small handful of packages on Nixpkgs that I maintain. When I was starting out, I didn’t really know how to do any of that, and was fortunate enough to have some friends onboard me. In the spirit of giving back, I’ll walk through how I do an update pass today on a simple package, and give some background information on how to make it easy.

Workflow of updating a NixOS package

I’m a maintainer for the lmstudio package, which wraps the binaries from LM Studio (a friendly GUI/IDE for AI model development work). This workflow is for that package; other packages (especially those used by others or built from source) will probably have different details.

  1. Find out that my package needs updating.
  2. I sync my nixpkgs fork with upstream.
  3. I create a branch in my fork of the form lmstudio-X.Y.Z.W and switch to it.
  4. I run our update script (from the root nixpkgs directory) via nix-shell maintainers/scripts/update.nix --argstr package lmstudio.
  5. I commit the resulting changes, which in my case are a changed hash and version number, using the format lmstudio: X.Y.Z.W -> Q.R.S.T. You ideally only want one commit or other maintainers get mad at you.
  6. I test the changes via nix-shell -I nixpkgs=~/projects/nix/nixpkgs -p lmstudio.
  7. I open a PR against upstream master branch with the title lmstudio: X.Y.Z.W -> Q.R.S.T and link the changelog. Example here.
  8. Some kind souls will review the PR using nixpkgs-review: nixpkgs-review pr --post-result 452412. This will (assuming they’re setup with gh) check out the PR in a sandbox, build it, and give them a chance to run it for manual testing. After, it’ll comment with a summary saying that ye, verily, did the PR build on their machine.
  9. Eventually, if approved, it’ll get merged into unstable by somebody with a commit bit. Backporting may or may not also occur.

nix-shell vs nix-build

Instead of…

$ nix-shell -I ~/projects/nix/nixpkgs -p lmstudio

…you can also use…

$ nix-build -I ~/projects/nix/nixpkgs -A lmstudio

After that, you’d have a result directory symlinked into the nix store:

[crertel@host:~/projects/nix/nixpkgs]$ ls -la result
lrwxrwxrwx 1 crertel users 61 Nov 18 12:31 result -> /nix/store/0rifw0s3frxqk6j14gpjag90wd9d28hn-lmstudio-0.3.31-7

[crertel@host:~/projects/nix/nixpkgs]$ ls -la result/
bin/   share/

[crertel@host:~/projects/nix/nixpkgs]$ ls -la result/bin/
total 100372
dr-xr-xr-x 2 root root      4096 Dec 31  1969 .
dr-xr-xr-x 4 root root      4096 Dec 31  1969 ..
-r-xr-xr-x 1 root root 102767744 Dec 31  1969 lms
lrwxrwxrwx 1 root root        67 Dec 31  1969 lm-studio -> /nix/store/gp0qyqzb4kp8j892sdnjyd8b891mmvck-lmstudio-0.3.31-7-bwrap

Neat, huh? I find that the nix-shell version is a bit easier for iterating and poking around (and it mimics how I normally use nix anyways), but nix-build will be closer to the thing that actually gets the final artifact out.

How the update script works

Before we had a good update script (I think @deftdawg was the one who got this working), we had to manually download and hash releases and update the version numbers; with the script, though, we can just use that one-liner above.

The script is called from packages.nix:

{
  lib,
  stdenv,
  callPackage,
  ...
}@args:
let
  pname = "lmstudio";

  version_aarch64-darwin = "0.3.31-7";
  hash_aarch64-darwin = "sha256-OyPHKUmZsIU0kc3JxwxdxACSN7hR6gFWjMnGp1x5diQ=";
  version_x86_64-linux = "0.3.31-7";
  hash_x86_64-linux = "sha256-NeVteQlzygHkwwgLkB5n7meH02WhFE68KkbGRulDiC0=";

  meta = {
    description = "LM Studio is an easy to use desktop app for experimenting with local and open-source Large Language Models (LLMs)";
    homepage = "https://lmstudio.ai/";
    license = lib.licenses.unfree;
    mainProgram = "lm-studio";
    maintainers = with lib.maintainers; [ crertel ];
    platforms = [
      "x86_64-linux"
      "aarch64-darwin"
    ];
    sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ];
    broken = stdenv.hostPlatform.isDarwin; # Upstream issue: https://github.com/lmstudio-ai/lmstudio-bug-tracker/issues/347
  };
in
if stdenv.hostPlatform.isDarwin then
  callPackage ./darwin.nix {
    inherit pname meta;
    passthru.updateScript = ./update.sh;
    version = version_aarch64-darwin;
    url =
      args.url
        or "https://installers.lmstudio.ai/darwin/arm64/${version_aarch64-darwin}/LM-Studio-${version_aarch64-darwin}-arm64.dmg";
    hash = args.hash or hash_aarch64-darwin;
  }
else
  callPackage ./linux.nix {
    inherit pname meta;
    passthru.updateScript = ./update.sh;
    version = version_x86_64-linux;
    url =
      args.url
        or "https://installers.lmstudio.ai/linux/x64/${version_x86_64-linux}/LM-Studio-${version_x86_64-linux}-x64.AppImage";
    hash = args.hash or hash_x86_64-linux;
  }

Note the passthru.updateScript attribute, which refers to an update.sh. That script looks like:

#!/usr/bin/env nix-shell
#!nix-shell -i bash -p curl common-updater-scripts

set -euo pipefail

for system in "aarch64-darwin darwin/arm64" "x86_64-linux linux/x64"; do
  # shellcheck disable=SC2086
  set -- ${system} # split string into variables $1 and $2

  arch="${1}"
  platform="${2}"

  url=$(curl -ILs -o /dev/null -w %{url_effective} "https://lmstudio.ai/download/latest/${platform}")
  version="$(echo "${url}" | cut -d/ -f6)"
  hash=$(nix --extra-experimental-features nix-command hash convert --hash-algo sha256 "$(nix-prefetch-url "${url}")")

  update-source-version lmstudio "${version}" "${hash}" --system="${arch}" --version-key="version_${arch}" \
    2> >(tee /dev/stderr) | grep -q "nothing to do" && exit
done

What that script does (and your script could do something similar!) is work out the version and hash for the most recent version of this software using some curl magic, and then invoke the update-source-version script. That script will fixup the old package.nix and propose the changes (which is why we have to commit it after).

Further learning

If you want to learn more about this, some places to check: