Blogger, Writer, Web Developer

Porting rhine into nixpkgs

I've recently made the switch back to using a pure Wayland tiling window manager on my Linux machine, which has been great for personal organization and performance. I've been a big fan of the sway window manager for some time (and swayfx fork for the fun eye candy).

However, after visiting the source code repository lately and seeing that some of the last few commits have had help from Claude and other AI tools, I've been less than enthused about it.

This all led me to wonder if there were other options out there?

At around the same time I switched to sway full time, I discovered this list (now one of many) of AI-free (or pledged free) software and realized that river, the former tiling wayland window-manager, had taken a no-AI/LLM pledge. Which seemed great! However, the project had also just recently shifted towards being a generic extensible Wayland compositor framework instead of a window manager. That was unfortunate, because it meant I couldn't really use river on its own.

I could have just used the river-classic package (which was the legacy river-with-a-window-manager) that most repos seemed to package, but I was worried that it may not have much of a future since the bulk of development effort had shifted away from that strategy.

On the upside, that shift allowed for hobbyists to just make a new window manager whenever they wanted and never have to worry about implementing wl_roots, Wayland, or the compositor layer. River would act as the backend for all of that boring stuff and the window manager could just manage windows. Simple!

This meant I could just grab any one of a number of off the shelf window managers that were compatible with river and have a no-AI wayland compositor. However, I wasn't just satisfied with just one layer. The river-compatible WM I had my eye on had, likewise, just taken a no-AI/LLM pledge. It was called rhine.

Like sway, rhine was also a tiling window manager. But unlike sway it had access to some really cool animations, layout options, and window management strategies, which all seemed they would make a fun new addition to my current set up.

Unfortunately, I couldn't just test out rhine from the get-go because it hadn't been packaged for nix and NixOS yet. Or, for that fact, it hadn't been packaged for much of anything yet-- it was still in beta!

So I decided to take this opportunity to try my hand at packaging up rhine, a program written in zig, and getting it to work with NixOS.

The Current State of Zig in Nixpkgs

There are a bunch of different conventions in nixpkgs for packaging up programs in languages that have their own package manager or fetch their own dependencies, like zig. The basic strategy has stayed the same, but it has shifted through a few different patterns.

If you look through nixpkgs, you'll find a few different ways of doing this.

The old pattern would have been to use a build tool like zon2nix, which converts the build dependencies in the build.zig.zon file into a nix expression, like so:

# generated by zon2nix (https://github.com/nix-community/zon2nix)

{ linkFarm, fetchzip }:

linkFarm "zig-packages" [
  {
    name = "1220363c7e27b2d3f39de6ff6e90f9537a0634199860fea237a55ddb1e1717f5d6a5";
    path = fetchzip {
      url = "https://gist.github.com/antlilja/8372900fcc09e38d7b0b6bbaddad3904/archive/6c3321e0969ff2463f8335da5601986cf2108690.tar.gz";
      hash = "sha256-m/kr4kmkG2rLkAj5YwvM0HmXTd+chAiQHzYK6ozpWlw=";
    };
  }
  {
    name = "122048992ca58a78318b6eba4f65c692564be5af3b30fbef50cd4abeda981b2e7fa5";
    path = fetchzip {
      url = "https://github.com/ziglibs/known-folders/archive/fa75e1bc672952efa0cf06160bbd942b47f6d59b.tar.gz";
      hash = "sha256-U/h4bVarq8CFKbFyNXKl3vBRPubYooLxA1xUz3qMGPE=";
    };
  }
  {
    name = "122089a8247a693cad53beb161bde6c30f71376cd4298798d45b32740c3581405864";
    path = fetchzip {
      url = "https://github.com/ziglibs/diffz/archive/90353d401c59e2ca5ed0abe5444c29ad3d7489aa.tar.gz";
      hash = "sha256-3CdYo6WevT0alRwKmbABahjhFKz7V9rdkDUZ43VtDeU=";
    };
  }
]

This file would then just be imported into your nix package. The traditional way of doing it would be as a build flag.

# from the 26.05 release of zigscient
...

  zigBuildFlags = [
    "--system"
    "${finalAttrs.deps}"
    "-Doptimize=ReleaseFast"
  ];
  preBuild = ''
    export ZIG_DEBUG_CMD=1;
    export ZIG_LIB_DIR=./lib;
  '';

  deps = callPackage ./deps_0_17.nix { };
  nativeBuildInputs = [ zig_0_17 ];
...

The problem with this method is that sometimes zon2nix builds the fetchers incorrectly and it can cause some build errors.

The newer way to package dependencies is a method that river actually models quite well, which is to use the zig.fetchDeps function that comes with all modern zig packages in nixpkgs.

zigDeps = zig.fetchDeps {
    inherit (finalAttrs) src pname version;
    fetchAll = true;
    hash = "sha256-u7lnnqxrTdaiqNOeFrqvesDM6B4YUjYkmP7CtGOBDIs=";
  };

This attribute can be referenced in a postConfigure hook (using the finalAttrs pattern, to avoid using the performance heavy rec keyword).

postConfigure = ''
    ln -s ${finalAttrs.zigDeps} "$ZIG_GLOBAL_CACHE_DIR/p"
'';

Not only is this pattern much cleaner and allow zig dependencies to be atomic, but it builds perfectly without any strange build time failures.

Launching a Window Manager With River

Since rhine requires river to run, river actually needs to be the program that gets launched first and calls the window manager.

You can either pass the name of the window manager as a command line argument, or by writing a init script at ~/.config/river/init that does this for you.

# CLI method
river -c rhine

# init script
rhine

Likewise, you also need to store your rhine configuration in the river configuration directory in a config.rh file. The configuration is written a bespoke configuration language that is shared among a few different tools. Rhine ships with a minimal configuration which is useful for testing, but you can also use this batteries-included configuration which is more similar to what you'd get with sway.

An interesting convention in rhine is that the configuration file is shared among a few different ecosystem tools. Not only does rhine use the configuration file to define workspaces and keybinds, but another tool called channel (made by the same developer behind rhine) also reads the same file to set up input interfaces and input profiles.

On my first attempt to package river, I avoided setting up any external tools and just focused on porting the window manager.

After a few days of testing, I was able to get a successful rhine build that I could launch with river in a terminal. Fantastic!

Enter New Co-Maintainers

Once I was able to get the package to build, I cleaned up my code and posted it on nixpkgs as a new pull request, which I was surprised to find actually attracted a fair bit of attention and interest.

One person had even started trying to go through the exact same process of packaging up rhine in nix, but hadn't landed on a working build yet. This person did, however, try to get a working version of channel in the same PR, and once they discovered that I was working on the same thing, expressed interest in joining as a co-maintainer.

We merged together our work and (after several rounds of revisions) we ended up landing on a decent package implementation that we were able to add to nixpkgs.

Cool beans!

Next Steps

Some things that we're looking to add later on include an upgrade channel to the newest release (a contributor pointed out that there had been a new release in the time we were waiting to merge the first PR), creating a functional desktop session for rhine that any generic display managers could detect (rhine comes with a desktop file, but I've had mixed results with it being detected), and keeping up with any future updates.

I've toyed with the idea of packaging up some manpages for rhine, but I've not had enough time to sit down and really experiment with the configuration interface to the extent that I would need in order to write docs for it.

In the meantime, I'm looking forward to setting up a new desktop session and getting rhine established as a secondary window manager.

I'll have to lean on things like swayidle, swaylock, and rofi to get full feature parity with my current sway setup, but I think that's totally fine.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang rel class> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <p class>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
CAPTCHA
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.

Webmention notify form

CAPTCHA
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.

Author Information

Drupal developer, blogger, NixOS package maintainer, Linux user, and experimental literature enthusiast.