A Nix library to create wrapped executables via the module system
Nix
345
293 commits
updated Sep 16, 2026
A Nix library to create wrapped executables via the module system.
Are you annoyed by rewriting modules for every platform? nixos, home-manager, nix-darwin, devenv?
Then this library is for you!
Watch this excellent Video by Vimjoyer for an explanation:
This library provides two main components:
lib.wrapPackage: Low-level function to wrap packages with additional flags, environment variables, and runtime dependencieslib.wrapModule: High-level function to create reusable wrapper modules with type-safe configuration optionswrapperModules: Pre-built wrapper modules for common packages (mpv, notmuch, etc.){
inputs.wrappers.url = "github:lassulus/wrappers";
outputs = { self, nixpkgs, wrappers }: {
packages.x86_64-linux.default =
(wrappers.wrapperModules.mpv.apply {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
scripts = [ pkgs.mpvScripts.mpris ];
"mpv.conf".content = ''
vo=gpu
hwdec=auto
'';
"input.conf".content = ''
WHEEL_UP seek 10
WHEEL_DOWN seek -10
'';
}).wrapper;
};
}
{ pkgs, wrappers, ... }:
wrappers.lib.wrapPackage {
inherit pkgs;
package = pkgs.curl;
runtimeInputs = [ pkgs.jq ];
env = {
CURL_CA_BUNDLE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
};
flags = {
"--silent" = true;
"--connect-timeout" = "30";
};
# Or use args directly for more control:
# args = [ "--silent" "--connect-timeout" "30" ];
flagSeparator = "="; # Use --flag=value instead of --flag value (default is " ")
preHook = ''
echo "Making request..." >&2
'';
}
You can also wrap a specific executable from a package with a custom name:
wrappers.lib.wrapPackage {
inherit pkgs;
package = pkgs.coreutils;
exePath = "${pkgs.coreutils}/bin/ls";
binName = "my-ls";
flags = {
"--color" = "auto";
"-l" = true;
};
}
{ wlib, lib }:
wlib.wrapModule ({ config, wlib, ... }: {
options = {
profile = lib.mkOption {
type = lib.types.enum [ "fast" "quality" ];
default = "fast";
description = "Encoding profile to use";
};
outputDir = lib.mkOption {
type = lib.types.str;
default = "./output";
description = "Directory for output files";
};
};
config.package = config.pkgs.ffmpeg;
config.flags = {
"-preset" = if config.profile == "fast" then "veryfast" else "slow";
};
config.env = {
FFMPEG_OUTPUT_DIR = config.outputDir;
};
})
Arguments:
pkgs: nixpkgs instancepackage: Base package to wrapexePath: Path to the executable to wrap (default: lib.getExe package)binName: Name for the wrapped binary (default: baseNameOf exePath)runtimeInputs: List of packages added to PATH (default: [])env: Attribute set of environment variables (default: {})flags: Attribute set of command-line flags (default: {})
true: Flag without argument (e.g., --verbose)"string": Flag with argument (e.g., --output "file.txt")false or null: Flag omittedflagSeparator: Separator between flag name and value when generating args from flags (default: " ", can be "=")args: List of command-line arguments like argv in execve (default: auto-generated from flags)
[ "--silent" "--connect-timeout" "30" ]flagspreHook: Shell script executed before the command (default: "")postHook: Shell script executed after the command. This will leave a bash process running, use with caution (default: "")passthru: Additional attributes for the derivation's passthru (default: {})aliases: List of additional symlink names for the executable (default: [])filesToPatch: List of file paths (glob patterns) relative to package root to patch for self-references (default: ["share/applications/*.desktop"])
["bin/*", "lib/*.sh"] to replace original package paths with wrapped package pathsfilesToExclude: List of file paths (glob patterns) to exclude from the wrapped package (default: [])patchHook: Shell script that runs after patchPhase to modify the wrapper package files (default: "")wrapper: Custom wrapper function (optional, overrides default exec wrapper)The function:
lndir for symlinking to maintain directory structureCreates a reusable wrapper module with:
options: Exposed options for documentation generationapply: Function to instantiate the wrapper with settings, returning a config object
wrapper attribute of the returned configBuilt-in options (always available):
pkgs: nixpkgs instance (required)package: Base package to wrapextraPackages: Additional runtime dependenciesflags: Command-line flags (attribute set)flagSeparator: Separator between flag name and value (default: " ")args: Command-line arguments list (auto-generated from flags if not provided)env: Environment variablespreHook: Shell script executed before the command (default: "")postHook: Shell script executed after the command. This will leave a bash process running, use with caution (default: "")passthru: Additional passthru attributesfilesToPatch: List of file paths (glob patterns) to patch for self-references (default: ["share/applications/*.desktop"])filesToExclude: List of file paths (glob patterns) to exclude from the wrapped package (default: [])patchHook: Shell script that runs after patchPhase to modify the wrapper package files (default "")wrapper: The resulting wrapped package (read-only, auto-generated from other options)apply: Function to extend the configuration with additional modules (read-only)Optional modules (import via wlib.modules.<name>):
systemd: Generates systemd service files (user and/or system), options are passed through from NixOSCustom types:
wlib.types.file: File type with content and path options
content: File contents as stringpath: Derived path using pkgs.writeTextThe wrapper module system integrates with NixOS module evaluation:
lib.evalModules for configuration evaluationconfig for accessing evaluated configurationoptions for introspection and documentationThe apply function allows you to extend an already-applied configuration with additional modules, similar to extendModules in NixOS:
# Apply initial configuration
initialConfig = wrappers.wrapperModules.mpv.apply {
pkgs = pkgs;
scripts = [ pkgs.mpvScripts.mpris ];
"mpv.conf".content = ''
vo=gpu
'';
};
# Extend with additional configuration
extendedConfig = initialConfig.apply {
scripts = [ pkgs.mpvScripts.thumbnail ];
"mpv.conf".content = ''
profile=gpu-hq
'';
};
# Access the wrapper
package = extendedConfig.wrapper;
The apply function re-evaluates the module with both the original settings and the new module, allowing you to override or add to the existing configuration.
Wraps mpv with configuration file support and script management:
(wrappers.wrapperModules.mpv.apply {
pkgs = pkgs;
scripts = [ pkgs.mpvScripts.mpris pkgs.mpvScripts.thumbnail ];
"mpv.conf".content = ''
vo=gpu
profile=gpu-hq
'';
"input.conf".content = ''
RIGHT seek 5
LEFT seek -5
'';
flags = {
"--save-position-on-quit" = true;
};
}).wrapper
Wraps notmuch with INI-based configuration:
(wrappers.wrapperModules.notmuch.apply {
pkgs = pkgs;
config = {
database = {
path = "/home/user/Mail";
mail_root = "/home/user/Mail";
};
user = {
name = "John Doe";
primary_email = "john@example.com";
};
};
}).wrapper
Import wlib.modules.systemd to generate systemd service files for your wrapper.
The options under systemd are the same as systemd.services.<name> in NixOS,
passed through directly.
ExecStart (including args), Environment, PATH, preStart and postStop
are picked up from the wrapper automatically, so you only need to set what's
specific to the service.
The same config produces both a user and system service file, available at
config.outputs.systemd-user and config.outputs.systemd-system. Use
whichever fits your deployment.
wlib.wrapModule ({ config, wlib, ... }: {
imports = [ wlib.modules.systemd ];
config = {
package = config.pkgs.hello;
flags."--greeting" = "world";
env.HELLO_LANG = "en";
systemd = {
description = "Hello service";
serviceConfig.Type = "simple";
serviceConfig.Restart = "on-failure";
};
};
})
Settings merge when using apply:
extended = myWrapper.apply {
systemd.serviceConfig.Restart = "always";
systemd.environment.EXTRA = "value";
};
You need both systemd.packages for the unit file and the corresponding
wantedBy to actually activate it. NixOS does not read the [Install] section
from unit files, it creates the .wants symlinks from the module option instead.
As a user service (for all users):
# configuration.nix
{ pkgs, wrappers, ... }:
let
myHello = wrappers.wrapperModules.hello.apply {
inherit pkgs;
systemd.serviceConfig.Restart = "always";
};
in {
systemd.packages = [ myHello.outputs.systemd-user ];
# NixOS needs this to create the .wants symlink, the [Install]
# section in the unit file alone is not enough
systemd.user.services.hello.wantedBy = [ "default.target" ];
}
As a system service:
# configuration.nix
{ pkgs, wrappers, ... }:
let
myHello = wrappers.wrapperModules.hello.apply {
inherit pkgs;
systemd.serviceConfig.Restart = "always";
};
in {
systemd.packages = [ myHello.outputs.systemd-system ];
systemd.services.hello.wantedBy = [ "multi-user.target" ];
}
For per-user services, link via xdg.dataFile:
# home.nix
{ pkgs, wrappers, ... }:
let
myHello = wrappers.wrapperModules.hello.apply {
inherit pkgs;
systemd.wantedBy = [ "default.target" ];
systemd.serviceConfig.Restart = "always";
};
in {
xdg.dataFile."systemd/user/hello.service".source =
"${myHello.outputs.systemd-user}/systemd/user/hello.service";
}
Upstream this schema into nixpkgs with an optional module.nix for every package. NixOS modules could then reuse these wrapper modules for consistent configuration across platforms.
Nix
100.0%
A Nix library to create wrapped executables via the module system
Nix
345
293 commits
updated Sep 16, 2026
A Nix library to create wrapped executables via the module system.
Are you annoyed by rewriting modules for every platform? nixos, home-manager, nix-darwin, devenv?
Then this library is for you!
Watch this excellent Video by Vimjoyer for an explanation:
This library provides two main components:
lib.wrapPackage: Low-level function to wrap packages with additional flags, environment variables, and runtime dependencieslib.wrapModule: High-level function to create reusable wrapper modules with type-safe configuration optionswrapperModules: Pre-built wrapper modules for common packages (mpv, notmuch, etc.){
inputs.wrappers.url = "github:lassulus/wrappers";
outputs = { self, nixpkgs, wrappers }: {
packages.x86_64-linux.default =
(wrappers.wrapperModules.mpv.apply {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
scripts = [ pkgs.mpvScripts.mpris ];
"mpv.conf".content = ''
vo=gpu
hwdec=auto
'';
"input.conf".content = ''
WHEEL_UP seek 10
WHEEL_DOWN seek -10
'';
}).wrapper;
};
}
{ pkgs, wrappers, ... }:
wrappers.lib.wrapPackage {
inherit pkgs;
package = pkgs.curl;
runtimeInputs = [ pkgs.jq ];
env = {
CURL_CA_BUNDLE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
};
flags = {
"--silent" = true;
"--connect-timeout" = "30";
};
# Or use args directly for more control:
# args = [ "--silent" "--connect-timeout" "30" ];
flagSeparator = "="; # Use --flag=value instead of --flag value (default is " ")
preHook = ''
echo "Making request..." >&2
'';
}
You can also wrap a specific executable from a package with a custom name:
wrappers.lib.wrapPackage {
inherit pkgs;
package = pkgs.coreutils;
exePath = "${pkgs.coreutils}/bin/ls";
binName = "my-ls";
flags = {
"--color" = "auto";
"-l" = true;
};
}
{ wlib, lib }:
wlib.wrapModule ({ config, wlib, ... }: {
options = {
profile = lib.mkOption {
type = lib.types.enum [ "fast" "quality" ];
default = "fast";
description = "Encoding profile to use";
};
outputDir = lib.mkOption {
type = lib.types.str;
default = "./output";
description = "Directory for output files";
};
};
config.package = config.pkgs.ffmpeg;
config.flags = {
"-preset" = if config.profile == "fast" then "veryfast" else "slow";
};
config.env = {
FFMPEG_OUTPUT_DIR = config.outputDir;
};
})
Arguments:
pkgs: nixpkgs instancepackage: Base package to wrapexePath: Path to the executable to wrap (default: lib.getExe package)binName: Name for the wrapped binary (default: baseNameOf exePath)runtimeInputs: List of packages added to PATH (default: [])env: Attribute set of environment variables (default: {})flags: Attribute set of command-line flags (default: {})
true: Flag without argument (e.g., --verbose)"string": Flag with argument (e.g., --output "file.txt")false or null: Flag omittedflagSeparator: Separator between flag name and value when generating args from flags (default: " ", can be "=")args: List of command-line arguments like argv in execve (default: auto-generated from flags)
[ "--silent" "--connect-timeout" "30" ]flagspreHook: Shell script executed before the command (default: "")postHook: Shell script executed after the command. This will leave a bash process running, use with caution (default: "")passthru: Additional attributes for the derivation's passthru (default: {})aliases: List of additional symlink names for the executable (default: [])filesToPatch: List of file paths (glob patterns) relative to package root to patch for self-references (default: ["share/applications/*.desktop"])
["bin/*", "lib/*.sh"] to replace original package paths with wrapped package pathsfilesToExclude: List of file paths (glob patterns) to exclude from the wrapped package (default: [])patchHook: Shell script that runs after patchPhase to modify the wrapper package files (default: "")wrapper: Custom wrapper function (optional, overrides default exec wrapper)The function:
lndir for symlinking to maintain directory structureCreates a reusable wrapper module with:
options: Exposed options for documentation generationapply: Function to instantiate the wrapper with settings, returning a config object
wrapper attribute of the returned configBuilt-in options (always available):
pkgs: nixpkgs instance (required)package: Base package to wrapextraPackages: Additional runtime dependenciesflags: Command-line flags (attribute set)flagSeparator: Separator between flag name and value (default: " ")args: Command-line arguments list (auto-generated from flags if not provided)env: Environment variablespreHook: Shell script executed before the command (default: "")postHook: Shell script executed after the command. This will leave a bash process running, use with caution (default: "")passthru: Additional passthru attributesfilesToPatch: List of file paths (glob patterns) to patch for self-references (default: ["share/applications/*.desktop"])filesToExclude: List of file paths (glob patterns) to exclude from the wrapped package (default: [])patchHook: Shell script that runs after patchPhase to modify the wrapper package files (default "")wrapper: The resulting wrapped package (read-only, auto-generated from other options)apply: Function to extend the configuration with additional modules (read-only)Optional modules (import via wlib.modules.<name>):
systemd: Generates systemd service files (user and/or system), options are passed through from NixOSCustom types:
wlib.types.file: File type with content and path options
content: File contents as stringpath: Derived path using pkgs.writeTextThe wrapper module system integrates with NixOS module evaluation:
lib.evalModules for configuration evaluationconfig for accessing evaluated configurationoptions for introspection and documentationThe apply function allows you to extend an already-applied configuration with additional modules, similar to extendModules in NixOS:
# Apply initial configuration
initialConfig = wrappers.wrapperModules.mpv.apply {
pkgs = pkgs;
scripts = [ pkgs.mpvScripts.mpris ];
"mpv.conf".content = ''
vo=gpu
'';
};
# Extend with additional configuration
extendedConfig = initialConfig.apply {
scripts = [ pkgs.mpvScripts.thumbnail ];
"mpv.conf".content = ''
profile=gpu-hq
'';
};
# Access the wrapper
package = extendedConfig.wrapper;
The apply function re-evaluates the module with both the original settings and the new module, allowing you to override or add to the existing configuration.
Wraps mpv with configuration file support and script management:
(wrappers.wrapperModules.mpv.apply {
pkgs = pkgs;
scripts = [ pkgs.mpvScripts.mpris pkgs.mpvScripts.thumbnail ];
"mpv.conf".content = ''
vo=gpu
profile=gpu-hq
'';
"input.conf".content = ''
RIGHT seek 5
LEFT seek -5
'';
flags = {
"--save-position-on-quit" = true;
};
}).wrapper
Wraps notmuch with INI-based configuration:
(wrappers.wrapperModules.notmuch.apply {
pkgs = pkgs;
config = {
database = {
path = "/home/user/Mail";
mail_root = "/home/user/Mail";
};
user = {
name = "John Doe";
primary_email = "john@example.com";
};
};
}).wrapper
Import wlib.modules.systemd to generate systemd service files for your wrapper.
The options under systemd are the same as systemd.services.<name> in NixOS,
passed through directly.
ExecStart (including args), Environment, PATH, preStart and postStop
are picked up from the wrapper automatically, so you only need to set what's
specific to the service.
The same config produces both a user and system service file, available at
config.outputs.systemd-user and config.outputs.systemd-system. Use
whichever fits your deployment.
wlib.wrapModule ({ config, wlib, ... }: {
imports = [ wlib.modules.systemd ];
config = {
package = config.pkgs.hello;
flags."--greeting" = "world";
env.HELLO_LANG = "en";
systemd = {
description = "Hello service";
serviceConfig.Type = "simple";
serviceConfig.Restart = "on-failure";
};
};
})
Settings merge when using apply:
extended = myWrapper.apply {
systemd.serviceConfig.Restart = "always";
systemd.environment.EXTRA = "value";
};
You need both systemd.packages for the unit file and the corresponding
wantedBy to actually activate it. NixOS does not read the [Install] section
from unit files, it creates the .wants symlinks from the module option instead.
As a user service (for all users):
# configuration.nix
{ pkgs, wrappers, ... }:
let
myHello = wrappers.wrapperModules.hello.apply {
inherit pkgs;
systemd.serviceConfig.Restart = "always";
};
in {
systemd.packages = [ myHello.outputs.systemd-user ];
# NixOS needs this to create the .wants symlink, the [Install]
# section in the unit file alone is not enough
systemd.user.services.hello.wantedBy = [ "default.target" ];
}
As a system service:
# configuration.nix
{ pkgs, wrappers, ... }:
let
myHello = wrappers.wrapperModules.hello.apply {
inherit pkgs;
systemd.serviceConfig.Restart = "always";
};
in {
systemd.packages = [ myHello.outputs.systemd-system ];
systemd.services.hello.wantedBy = [ "multi-user.target" ];
}
For per-user services, link via xdg.dataFile:
# home.nix
{ pkgs, wrappers, ... }:
let
myHello = wrappers.wrapperModules.hello.apply {
inherit pkgs;
systemd.wantedBy = [ "default.target" ];
systemd.serviceConfig.Restart = "always";
};
in {
xdg.dataFile."systemd/user/hello.service".source =
"${myHello.outputs.systemd-user}/systemd/user/hello.service";
}
Upstream this schema into nixpkgs with an optional module.nix for every package. NixOS modules could then reuse these wrapper modules for consistent configuration across platforms.
Nix
100.0%