Project overview

wrapper-manager-fds is foodogsquared’s reimagining of wrapper-manager.

wrapper-manager-fds is considered unstable and the author is still freely experimenting with various aspects of the project including the module options, library set API, and ease of use for third-partyuse. Expect constant breakages here and put on a hard hat before exploring this part of the town.

As a recap, the aim of wrapper-manager is to pave a way of configuring Nix environments through wrappers. There has been interest on this feature [1] and I sometimes configure programs through wrappers instead of the typical module system. It has its use cases such as quickly creating variants of the same program for specific reasons (for example, creating specialized versions of yt-dlp for downloading audio, video, and more). But why not create something more than that? And that’s how wrapper-manager-fds came to be.

Installation

Channels

You can install wrapper-manager-fds with Nix channels.

nix-channel --add https://github.com/foo-dogsquared/nix-module-wrapper-manager-fds/archive/master.tar.gz wrapper-manager-fds
nix-channel --update

Then in your environment configuration, you’ll have to import the user entrypoint. For more details, see User entrypoint.

Pinning tool

A recommended (non-flakes) way to install Nix dependencies is to use a pinning tool. There are a handful of pinning tool out there but in this case, we’ll use npins as our tool of choice. Assuming you have already initialized npins, you can simply add wrapper-manager-fds to your Nix project with the following command.

npins add --name wrapper-manager github foo-dogsquared nix-module-wrapper-manager-fds --at 0.1.0

Similar to channels installation, you’ll have to import the User entrypoint object.

Manual pinning

Though not recommended, you could manually pin the Nix library yourself and then import the user entrypoint like in the following code.

{ pkgs, ... }:

let
  wrapper-manager-fds-src = builtins.fetchTarball "https://github.com/foo-dogsquared/nix-module-wrapper-manager-fds/archive/0.1.0.tar.gz";
  wrapper-manager = import wrapper-manager-fds-src { };
  wrapperManagerLib = import wrapper-manager.wrapperManagerLib { inherit pkgs; }
in
wrapperManagerLib.env.build { }

Flakes

This project also has a flake object. In your flake definition, just import it as one of the inputs. Unlike the other methods, the flake output is the user entrypoint so no need to import it or anything.

{
  inputs.wrapper-manager-fds.url = "github:foo-dogsquared/nix-module-wrapper-manager-fds/0.1.0";

  outputs = { nixpkgs, ... }@inputs: {
    nixosConfigurations.desktop = nixpkgs.lib.nixosSystem {
      modules = [
        ./hosts/desktop.nix
        inputs.wrapper-manager-fds.nixosModules.default
      ];
    };
  };
}

Getting started

Now that you have wrapper-manager-fds on the go, let’s have a breakdown of what it is, exactly. The project itself is made of different parts which you can use for different purposes.

User entrypoint

Most of the things you need from the project are all retrieveable from the entrypoint of this project (default.nix in project root) which is an attribute set closely structured from a typical Nix flake that you see. For example, if you want the NixOS module for wrapper-manager, you could refer to nixosModules.default. Here’s the breakdown of the attrset entries in the entrypoint.

  • lib is an attribute set containing the main functions: eval and build. (More details to use them are in As a library.)

  • nixosModules contains a set of NixOS modules for wrapper-manager-fds integration. You could get the main module with the default attribute.

  • homeModules are for home-manager modules and similarly structured to nixosModules attribute.

  • wrapperManagerLib contains a path intended to be imported within your Nix code. It simply contains the library set that can be used outside of the wrapper-manager-fds module environment. To use it, it only requires a nixpkgs instance like in the following code.

    { pkgs, ... }:
    
    let
      wrapper-manager-fds-src = builtins.fetchTarball "https://github.com/foo-dogsquared/nix-module-wrapper-manager-fds/archive/master.tar.gz";
      wrapper-manager = import wrapper-manager-fds-src { };
      wrapperManagerLib = import wrapper-manager.wrapperManagerLib { inherit pkgs; }
    in
    wrapperManagerLib.env.build { }
  • overlays is a set of nixpkgs overlays to be applied by the user. So far, there’s only one overlay called default containing the wrapper-manager library set.

The module environment

One part of the project is the module environment. Just like home-manager and disko and the original source, wrapper-manager-fds comes with its own module environment. Instead of a home environment from home-manager, an entire operating system from NixOS, or an installation script from disko, wrapper-manager-fds module environment evaluates to a package similar to how certain environments treats them (e.g., environment.systemPackages for NixOS, home.packages for home-manager).

Much of the module environment relies on makeWrapper. In fact, this can be thought of as a declarative layer over makeWrapper with some other integrations.

If you want to view the module options, you can see it in wrapper-manager module options.

Here’s a very simple example of a wrapper for Neofetch.

{ lib, pkgs, ... }:

{
  wrappers.neofetch = {
    arg0 = lib.getExe' pkgs.neofetch "neofetch";
    appendArgs = [
      "--ascii_distro" "guix"
      "--title_fqdn" "off"
      "--os_arch" "off"
    ];
  };
}

Or if you want fastfetch…​

{ lib, pkgs, ... }:

{
  wrappers.fastfetch = {
    arg0 = lib.getExe' pkgs.fastfetch "fastfetch";
    appendArgs = [ "--logo" "Guix" ];
    env.NO_COLOR.value = "1";
  };
}

Or even both in the same configuration (which you can do). If evaluated, this should result in a single derivation that contains two executables in $out/bin/{fastfetch, neofetch}.

{
  imports = [
    ./fastfetch.nix
    ./neofetch.nix
  ];
}

You could even create XDG desktop entry files useful for the application to be launched through an application launcher/menu. For example, you could create an executable and a desktop entry to launch a custom Firefox profile in your home-manager configuration.

Creating a custom Firefox desktop entry launching a custom profile
{ config, lib, pkgs, ... }:

{
  programs.firefox.profiles.custom-profile = {
    # Put some profile-specific settings here.
  };

  wrapper-manager.packages.browsers = {
    wrappers.firefox-custom-profile = {
      arg0 = lib.getExe' config.programs.firefox.package "firefox";
      prependArgs = [
        "-P" "custom-profile"
      ];
      xdg.desktopEntry = {
        enable = true;
        settings = {
          desktopName = "Firefox (custom-profile)";
          startupNotify = true;
          startupWMClass = "firefox";
          icon = "firefox";
          mimeTypes = [
            "text/html"
            "application/xhtml+xml"
            "application/vnd.mozilla.xul+xml"
            "x-scheme-handler/http"
            "x-scheme-handler/https"
          ];
        };
      };
    };
  };
}

As a library

wrapper-manager also comes with a library set which you can use to evaluate and build wrapper-manager packages yourself. This is found in the wrapperManagerLib attribute from the user entrypoint where it needs an attribute set containing a nixpkgs instance in pkgs.

An example of importing wrapper-manager library
{ pkgs }:

let
  wrapper-manager = import (builtins.fetchgit { }) { };

  wmLib = import wrapper-manager.wrapperManagerLib { inherit pkgs; };
in
wmLib.env.build {
  inherit pkgs;
  modules = [ ./fastfetch.nix ];
  specialArgs.yourMomName = "Joe Mama";
}

Here’s a quick rundown of what you can do with the library.

  • Evaluate a wrapper-manager module with env.eval where it accepts an attrset similar to the previous code listing containing a list of additional modules, the nixpkgs instance to be used, and specialArgs to be passed on to the lib.evalModules from nixpkgs.

  • Build a wrapper through env.build returning a derivation of the wrapper. It accepts the same arguments as env.eval.

There is also lib attribute if all you want to do is to build and/or evaluate a wrapper-manager configuration. It only contains the function from env subset which contains build and eval.

As a composable module

The most user-friendly way of using wrapper-manager would be as a composable nixpkgs module of an existing environment. wrapper-manager provides a Nix module specifically for NixOS and home-manager environment. [2] You can import them through the {nixos,home}Modules.default from the user entrypoint of the project.

You can view the module options for each environment.

Most of the things set up here are implemented to make declaring wrappers ergonomic with the environment. For a start, wrapper-manager-fds sets up a module namespace in wrapper-manager. Here’s a quick breakdown of the features that the module has.

  • Passes the wrapper-manager library through wrapperManagerLib module argument. This is nice if you want to only use wrapper-manager to quickly create wrappers inside of the configuration without using the wrapper-manager NixOS/home-manager integration module.

  • You could declare wrappers through wrapper-manager.packages.<name> where each of the attribute value is expected to be a wrapper-manager configuration to be added in its respective wider-scope environment.

  • You could include other modules through wrapper-manager.sharedModules. This is useful for extending wrapper-manager inside of the configuration environment.

Here’s an example of adding wrappers through wrapper-manager inside of a home-manager configuration. The following configuration will create a wrapped package for yt-dlp with an additional wrapper script named yt-dlp-audio and yt-dlp-video.

Installing yt-dlp with custom variants of it inside of a home-manager configuration
{ config, lib, pkgs, ... }:

{
  home.packages = with pkgs; [
    flowtime
    blanket
  ];

  wrapper-manager.packages = {
    music-setup = {
      basePackages = [ pkgs.yt-dlp ];
      wrappers.yt-dlp-audio = {
        arg0 = lib.getExe' pkgs.yt-dlp "yt-dlp";
        prependArgs = [
          "--config-location" ./config/yt-dlp/audio.conf
        ];
      };
      wrappers.yt-dlp-video = {
        arg0 = lib.getExe' pkgs.yt-dlp "yt-dlp";
        prependArgs = [
          "--config-location" ./config/yt-dlp/video.conf
        ];
      };
    };
  };
}

Development

If you want to hack this hack, you’ll need either Nix with flakes enabled (experimental-features = nix-command flakes in nix.conf) or not. Either way, this should be enough to cater to both flake- and non-flake users.

This project supports the current stable and unstable version of NixOS. Specifically, we’re looking out for the nixpkgs instance both of these versions has. As an implementation detail, we pin these branches through npins which both flakes- and non-flake-based setups uses. Just be familiar with it and you’ll be fine for the most part. Most likely, you don’t even need to interact with it since handling update cadence is handled automatically through the remote CI.

Setting up the development environment should be easy enough.

  • For flake users, you can just reproduce the development environment with nix develop.

  • For non-flake users, you can do the same with nix-shell.

As an additional note, it is recommended to use something like direnv with use flake or use nix depending on your personal preferences to use flake or not.

Take note there is a Makefile full of commands intended for easily interacting with the project but it is heavily assumed you’re in the development environment of the project.

Library set and modules

This Nix project has a test infrastructure set up at test directory covering the library set and the wrapper-manager module environment. For its library set, it makes use of the nixpkgs library and a JSON schema to validate if it passes the whole test suite. To make use of it, just run the following commands.

  • For flake users, you can run nix flake check.

  • For non-flake users, you can do the same with nix-build tests/ -A lib or nix build -f tests/ lib.

There is also a test suite for different types of wrapper-manager configurations at test directory from the source code. You can check them the same way as before (e.g., it’s the same for flake-using contributors, nix-build tests/ -A configs for non-flake-using contributors).

The derivation output should be successfully built if all of the tests in the suite passes. Otherwise, it should fail and you’ll have to see the build log containing all of the tests that failed.

On another note, there is a quicker way of checking the test suite with nix eval -f tests lib (or nix-instantiate --eval --strict tests/ -A lib) where it contains the raw test data which is useful if you don’t want to essentially build a new derivation each time. It is also quicker to eyeball results in this way especially if you’re always working with the tests anyways.

Website

This project also has a website set up with Hugo. The files that you need to see are in docs/website directory.

  • For flake users, you can build the website with nix build .#devPackages.${SYSTEM}.website.

  • For non-flake users, you can do the same with nix-build docs/ -A website.

There is also a dedicated development environment placed in docs/website/shell.nix but this should be a part of the primary development environment already. You can enter it with nix develop .#devPackages.${SYSTEM}.website or nix-shell docs/website.

Just take note that the website also requires the NixOS options which comes in a JSON file. This should be already taken care of in the package definition of the website but otherwise it is something that you’ll have to be aware of.

The more important task to developing this part of the project is continuously getting feedback from it. You can do so simply with the following commands:

  • For flake users, nix develop --command hugo -s ./docs serve.

  • For non-flake users, nix-shell docs --command hugo -s ./docs serve.

  • If you’re using Makefile of this project, make docs-serve.

Nix environment

As for developing the environment with Nix itself, it is very much preferred to make wrapper-manager-fds work with non-flake setups. This also includes the workflow of the development itself for the purpose of easier time bootstrapping wrapper-manager-fds.

Due to the unfortunate situation with flakes as an experimental feature, it is more like a second-class citizen in terms of support. This is because it is pretty easy to make a flake with non-flake tools compared to vice versa. [3]

Here’s an exhaustive guidelines that you have to keep in mind when developing related files within the project:

  • This project uses calendar versioning.

  • Only the unstable branch of NixOS is currently supported. Support for the stable versions are unfortunately secondary and more incidental (at least at the moment).

  • There shouldn’t be any user consumables that requires anything from the npins sources.

Goals and non-goals

As a Nix project, wrapper-manager-fds aims for the following goals.

  • Create an ecosystem of creating them wrappers, mainly through its library set and the module environment.

  • Make creating wrappers ergonomic for its users. Not necessarily user-friendly but it should easy enough to get started while allowing some flexibility, yeah?

  • Make a nice environment for creating custom wrappers which is already quite possible thanks to the heavy lifting of the nixpkgs module system.

For now, wrapper-manager-fds does not focus on the following ideas; the main focus for now (as of 2024-07-31) is the core attributes needed to make wrapper-manager extensible for third-party module authors. Take note, these are all ideas that are considered but may or may not be out of the blacklisted ideas at some point in the future for a variety of reasons. Think of them as a list of possibilities for what may come within wrapper-manager-fds.

  • Create an environment similar to NixOS and home-manager. wrapper-manager-fds' endgoal is to create a derivation typically composed as part of an environment (e.g., mkShell for devshells, environment.systemPackages for NixOS, home.packages for home-manager). Otherwise, we’re creating a poor man’s version of them and it’ll quickly creep in scope.

  • Support for multiple nixpkgs releases. Up until I put some elbow grease for release engineering and to make testing between multiple branches easy, only the unstable branch of nixpkgs is officially supported for now.

  • Integrating with sandboxing frameworks such as Bubblewrap and Boxxy. [4] This is too big of a task so it isn’t considered for now. Plus, having this would now require creating additional support which the author does not have time for it.

  • Create an ecosystem of modules that would allow to create quick configurations for different programs similarly found on other module environments such as in NixOS and home-manager. Specifically, we’re talking about modules in programs namespace (e.g., programs.kitty, programs.alacritty, programs.nixvim). This would also require having a support cadence so not much is going to happen here. Instead, I would encourage to have a separately-maintained project containing those for now.

  • Focus on hardware-related configuration for the wrappers. For now, it isn’t possible within wrapper-manager (or Nix, really). Some possible ideas include creating our own version of nixpkgs' makeWrapper, creating a specialized launcher for it, or something in the middle. Would be a fun idea to make though. :)

Frequently asked questions (FAQ)

  1. Is this compatible with the original wrapper-manager?

    Nope. It is a reimagining with a completely different way of using it so it won’t be fully compatible with it from the start.

  2. Why reimplement this anyways?

    For funsies and also because there are some things I find not so great with using the project. As of this writing, using wrapper-manager to simply create wrappers anywhere is a pain.

    To put it in more details, here are my complaints from using the original version:

    • First and foremost, for whatever reason, the original version revolves around wrapProgram instead of the generalized makeWrapper. This decision bled into the module environment with unnecessary options such as renaming binaries from a given base package which hurts the user experience of configuring these wrappers.

    • There’s no option to create other types of wrappers, only shell-based wrappers is supported.

    • It is impossible to create a derivation consisting of only wrappers which is useful for installing wrappers in an environment alongside its original version. An example would be installing yt-dlp alongside its variants that are configured through wrapper-manager. In the original wrapper-manager, each of these wrapper would require a base package which will be included in the output.

    • No integration with NixOS/home-manager.

  3. Why not just incorporate the wanted changes into the original implementation?

    While it could be done, there will be some unwanted major changes into the project which would cause inconvenience to its users anyways so it isn’t a good idea. Plus it also justifies me implementing a bunch of features that would otherwise be deemed inconsistent with the project.

  4. Can’t you just create a wrapper with pkgs.makeWrapper and such from nixpkgs?

    Yeah, you can. There’s nobody stopping you from doing so and surely there’s no hitman preparing to assissinate right behind you as you about to deny wrapper-manager-fds and smugly type make in makeWrapper. In fact, wrapper-manager uses makeWrapper as the main ingredient. Just think of wrapper-manager as a declarative version of that among the bajillion ways of making wrappers in the Nix ecosystem plus some other integrations (e.g., XDG).

    As an additional point, there are still use cases for it even with a simple pkgs.writeShellScriptBin. In fact, if you have a situation like say having to create a one-off wrapper script to be added in a NixOS system, you can simply do the following:

    let
      ytdlpAudio = pkgs.writeScriptBin "yt-dlp-audio" ''
        ${pkgs.yt-dlp}/bin/yt-dlp --config-location "${../../config/yt-dlp/audio.conf}" $@
      '';
    in
    {
      environment.systemPackages = [ ytdlpAudio ];
    }

    BAM! No need for wrapper-manager!

  5. Why use the module system?

    Because screw you, that’s why!!! Am I stupid and lazy for basically using a battle-hardened configuration system library such as nixpkgs module system? [5]

    Seriously though, the main reason is pretty simple: it is quite established and a battle-hardened part in the Nix ecosystem. It has gone through the test of time and the numerous 339 users of the entire Nix ecosystem are quite adamant in the declarative aspect of the Nix thingy. So…​ why not use it.

  6. Any problems (and impending explosions) when using this project?

    As far as I can tell, not much (especially explosions) but there are a few caveats you need to know. Just know this is something the author is trying to resolve.

    • wrapper-manager-fds is not great at handling double wrappers. It just naively wraps a package and goes on its merry way.

    • wrapper-manager-fds is strongly biased towards Linux (and Unix-adjacent) ecosystem.

    • wrapper-manager-fds doesn’t handle any replacement for the related files very well. This is especially noticeable in large desktop-adjacent packages such as Inkscape, Firefox, and Blender with a bunch of plugins and whatnot where they have their own wrappers. This means you cannot set programs.NAME.package or something similar with it.

    • The build step isn’t enough to completely let the user replace the arguments found in programs.<name>.package (e.g., programs.kitty.package = wrapperManagerLib.env.build { }). Right now, wrapper-manager-fds can rebuild the package if the basePackage module value is a bare package instead of the typical list of package but at the cost of an entire rebuild of that package. Not a great experience to have especially if you’re making a wrapper for larger applications. There’s no in-betweensies for this unfortunately (at least until I can think of a solution).

      With all that said, the project exclusively (and currently) focuses on making a nice declarative environment allowing the user to create a wrapper meant to work without adding configuration files into arbitrary locations in the filesystem (e.g., $XDG_CONFIG_HOME).

Acknowledgements

I found a bunch of things for inspiration (READ: to steal ideas from). Here’s a list of resources I’ve found.

Aside from the listed resources, here’s more unrelated resources I’ve found that can/would’ve/doesn’t/you-get-what-I-mean-right influence the project.

  • Aux Tidepool seems to be interesting by applying the module system in package definitions.

This project is licensed under MIT License (SPDX identifier: MIT). Just see license file for full text and details and whatnot.

The documentation (except for the code examples), on the other hand, is licensed under GNU Free Documentation License v1.3 only with no "Invariants" section (SPDX identifier: GFDL-1.3-no-invariants-only) You can see either the link or license file for more info. The code examples, similar to the project codebase, are licensed under MIT with the same conditions apply and all that jazz.


1. I mean, a part of the nixpkgs package set has dedicated wrappers for some packages such as GIMP, Inkscape, and Blender.
2. Any other environments are basically unsupported and if you like to use it outside of NixOS and home-manager, you’re on your own.
3. flake-compat is great and all but it holds back wrapper-manager-fds in making it easy to bootstrap if we rely on it.
4. That said, the author does have custom wrapper-manager modules that does exactly that so this being ruled out may be ruled out in the future ;p
5. The answer is yes to both!