The vim
and neovim
packages in Nixpkgs allow users to set custom configuration, including their customRC
and any plugins they might want.
How do they accomplish it?
In this article, we'll explore how to create packages with similar behavior. We'll create a simple Rust app that consumes a configuration file, then create a Nix flake containing both an unwrapped binary package as well as a configurable package.
Observing our goal
My neovimConfigured
derivation demonstrates the desired experience for our users:
{ neovim, vimPlugins }:
neovim.override {
vimAlias = true;
viAlias = true;
configure = {
customRC = ''
luafile ../config/nvim/init.lua
'';
packages.myVimPackage = with vimPlugins; {
start = [
LanguageClient-neovim
# ...
];
};
};
}
We're able to override the neovim
package with some custom attributes which are then loaded when the nvim
from that package is invoked. In theory, virtually any Nix user could run the following and get nearly my exact configuration:
In order to accomplish this, we'll make use of nixpkgs.lib.makeOverridable
.
Interacting with makeOverridable
The makeOverridable
(source) function allows us to attach an override
behavior to a given function.
Let's open our nix repl
and try it:
nix-repl> makeOverridable = (builtins.getFlake "nixpkgs").lib.makeOverridable
nix-repl> pretendPkg = args@{ ... }: args
nix-repl> pretendPkg { foo = 1; }
{ foo = 1; }
nix-repl> overriddenPretendPkg = makeOverridable pretendPkg { default = true; }
nix-repl> overriddenPretendPkg
{ default = true; override = { ... }; overrideDerivation = «lambda @ /nix/store/31jbhzh2pj5zsr5ip983qbknv23kf7d4-source/lib/customisation.nix:84:32»; }
When we used makeOverridable
with the pretendPkg
function it creates a result (overriddenPretendPkg
) as if the original function was called with the given attribute set ({ default = true; }
), as well as two additional attributes: override
and overrideDerivation
.
Let's check out override
:
nix-repl> overriddenPretendPkg.override
{ __functionArgs = { ... }; __functor = «lambda @ /nix/store/31jbhzh2pj5zsr5ip983qbknv23kf7d4-source/lib/trivial.nix:346:19»; }
nix-repl> overriddenPretendPkg.override { }
{ default = true; override = { ... }; overrideDerivation = «lambda @ /nix/store/31jbhzh2pj5zsr5ip983qbknv23kf7d4-source/lib/customisation.nix:84:32»; }
nix-repl> overriddenPretendPkg.override { default = false; }
{ default = false; override = { ... }; overrideDerivation = «lambda @ /nix/store/31jbhzh2pj5zsr5ip983qbknv23kf7d4-source/lib/customisation.nix:84:32»; }
So override
is a function which accepts an attribute set which it merges with the attribute set the makeOverride
function is invoked with.
Note:
overrideDerivation
is deprecated, so we won't discuss it.
We can use makeOverridable
on any function that accepts an attribute set, namely, a Nix package created through something like mkDerivation
or mkShell
.
Let's make a quick Rust application, after, we can make a Nix flake, package, and overridable package for it!
A test application
First, let's quickly bootstrap a Rust package to play with. Make a demo
directory and create the following two files:
# demo/Cargo.toml
[]
= "demo"
= "0.1.0"
= "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[]
= { = "0.3", = false }
= { = "0.5", = false }
= { = "1", = false, = [ "derive" ] }
= { = "0.5", = false }
= { = "0.6", = false }
// demo/src/main.rs
use WrapErr;
use Deserialize;
use ;
use StructOpt;
This will create a program which accepts a given config, loads it, and prints it out to stdout
. That should be sufficient to tell if we've succeeded in our task later!
If you already have a global Rust environment set up, you can give this a test with cargo run
or DEMO_CONFIG=/some/path cargo run
. If you don't have one, you'll be able to do this after the next step.
Flexing Nix
Now our little crate needs a Nix package. We'll use github:oxalica/rust-overlay
and github:nix-community/naersk
today, so our demo
crate is built like so:
# Chunk of a Flake.nix
overlay = final: prev: {
demo-unwrapped = naersk.lib."final.hostPlatform.system".buildPackage rec {
pname = "demo";
version = "0.0.1";
src = gitignore.lib.gitignoreSource ./.;
};
# ...
Note: We suffix it with
-unwrapped
so the configurable package can bedemo
. We call it 'unwrapped' since we will be 'wrapping' the package to make it configurable.
Then we'll define a wrapDemo
function which takes some given non-overridable demo
package and makes it overridable.
wrapDemo = demo-unwrapped: final.lib.makeOverridable ({ configuration ? null }:
if configuration == null then
demo-unwrapped
else
let
configurationFile = final.writeTextFile {
name = "demo-config";
text = configuration;
}; in
final.symlinkJoin {
name = "demo-final.lib.getVersion final.demo-unwrapped";
paths = [ ];
nativeBuildInputs = with final; [ makeWrapper ];
postBuild = ''
makeWrapper demo-unwrapped/bin/demo \
placeholder "out"/bin/demo \
--set-default DEMO_CONFIG configurationFile
'';
});
The { configuration ? null }
(on the first line) is the available options to override, here you could add, remove, or change what users can manipulate.
symlinkJoin
(source) is a builder like mkDerivation
, and produces what nix run
(and the rest) expect.
The placeholder
function, a Nix builtin, returns a placeholder for one of the outputs of a package.
In the postBuild
step of the derivation we'll make use of makeWrapper
(source), which allows us to do things like set arguments and populate environment variables.
With that, we can define the 'wrapped' package:
demo = final.wrapDemo final.demo-unwrapped { };
So whole flake looks like so:
# demo/flake.nix
{
description = "An example of a configurable Nix package.";
inputs = {
nixpkgs.url = "nixpkgs";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
naersk.url = "github:nix-community/naersk";
naersk.inputs.nixpkgs.follows = "nixpkgs";
gitignore.url = "github:hercules-ci/gitignore.nix";
gitignore.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, rust-overlay, naersk, gitignore }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
pkgsForSystem = system: (import nixpkgs {
inherit system;
overlays = [
self.overlay
rust-overlay.overlay
(self: super: { inherit (self.rust-bin.stable.latest) rustc cargo rustdoc; })
];
});
in
{
overlay = final: prev: {
demo-unwrapped = naersk.lib."final.hostPlatform.system".buildPackage rec {
pname = "demo";
version = "0.0.1";
src = gitignore.lib.gitignoreSource ./.;
};
wrapDemo = demo-unwrapped: final.lib.makeOverridable ({ configuration ? null }:
if configuration == null then
demo-unwrapped
else
let
configurationFile = final.writeTextFile {
name = "demo-config";
text = configuration;
}; in
final.symlinkJoin {
name = "demo-final.lib.getVersion final.demo-unwrapped";
paths = [ ];
nativeBuildInputs = with final; [ makeWrapper ];
postBuild = ''
makeWrapper demo-unwrapped/bin/demo \
placeholder "out"/bin/demo \
--set-default DEMO_CONFIG configurationFile
'';
});
demo = final.wrapDemo final.demo-unwrapped { };
};
defaultPackage = forAllSystems (system:
(pkgsForSystem system).demo
);
packages = forAllSystems (system:
let
pkgs = pkgsForSystem system;
in
{
inherit (pkgs) demo demo-unwrapped;
demoConfigured = pkgs.demo.override {
configuration = ''
switch = true
'';
};
});
devShell = forAllSystems (system:
let
pkgs = pkgsForSystem system;
in
pkgs.mkShell {
inputsFrom = [ pkgs.demo ];
buildInputs = with pkgs; [ nixpkgs-fmt cargo rust-bin.stable.latest.default ];
});
checks = forAllSystems (system:
let
pkgs = pkgsForSystem system;
in
{
format = pkgs.runCommand "check-format"
{ } ''
pkgs.nixpkgs-fmt/bin/nixpkgs-fmt --check ./.
touch $out # it worked!`
'';
inherit (pkgs) demo;
demoConfigured = self.packages."system".demoConfigured;
});
};
}
If you use direnv
you can add this too:
# demo/.envrc
# reload when these files change
# load the flake devShell
Experimenting
Let's validate that this works!
In our Flake.nix
we also defined a demoConfigured
package which uses override
and sets a custom configuration
:
packages = forAllSystems (system:
let
pkgs = pkgsForSystem system;
in
{
inherit (pkgs) demo demo-unwrapped;
demoConfigured = pkgs.demo.override {
configuration = ''
switch = true
'';
};
});
We can run it as well as the default demo
and validate the configuration file does get overriden:
)
Now the configured one:
Fantastic! We can see switch
got set in our demoConfigured
package, and the config file it reads from is part of our Nix store.
As you can see, Nix lets us make configurable wrappers around our packages quite easily, allowing your users to take their configuration with them.