wrap.nix
1/**
2 Wraps a package with flags and env vars.
3
4 The wrapper derivation has each specified binary in the original package replaced by a wrapper with the specified flags and environment variables.
5 Every other file from the original derivation is symlinked to by the new derivation, except any file referencing the original $pkg will be cloned and have that reference replaced by the wrapper derivation's $out.
6
7 # Example
8
9 ```nix
10 mkWrapper = import ./wrap.nix;
11 editor = mkWrapper pkgs {
12 pkg = pkgs.helix;
13 hx = {
14 env.idk = "hi";
15 prependFlags = [
16 "--config"
17 "./helix.toml"
18 ];
19 };
20 };
21 ```
22
23 # Type
24
25 ```
26 mkWrapper :: Pkgs -> {
27 pkg : Derivation,
28 pname? : String,
29 additionalDependencies? : [Derivation],
30 <bin> : {
31 env? : AttrSet String,
32 prependFlags? : [String]
33 appendFlags? : [String]
34 }
35 } -> Derivation
36 ```
37
38 # Arguments
39
40 pkgs
41 : An instance of nixpkgs
42
43 pkg
44 : The derivation to wrap
45
46 pname
47 : An optional name for the wrapped package, defaults to `pkg.name`
48
49 additionalDependencies
50 : An optional list of additional derivations to include in the wrapped environment
51
52 <bin>
53 : Variadic number of attrsets in the form bin = { env?, flags? }, where `bin` is the name of the binary that `pkg` outputs that you want to wrap. `env` and `flags` are detailed below.
54
55 env
56 : An optional attrset specifying environment variables to set inside the wrapped environment, defaults to `{}`
57
58 prependFlags
59 : An optional list specifying flags to append to the wrapped binary, defaults to `[]`. These flags go before your arguments, in the form `bin --prepended $@`.
60
61 appendFlags
62 : An optional list specifying flags to append to the wrapped binary, defaults to `[]` These flags go after your arguments, in the form `bin $@ --appended`.
63*/
64pkgs:
65let
66 wrap =
67 {
68 pkg,
69 pname ? pkg.pname,
70 version ? pkg.version,
71 additionalDependencies ? [ ],
72 ...
73 }@args:
74
75 let
76 lib = pkgs.lib;
77 in
78
79 assert lib.isDerivation pkg || throw "'pkg' must be a derivation (e.g. pkgs.hello)";
80 assert lib.isString pname || throw "'name' must be a string";
81
82 let
83 # all keyword kwargs are bins
84 binaries = builtins.removeAttrs args [
85 "pkg"
86 "pname"
87 "version"
88 "additionalDependencies"
89 ];
90
91 wrapCommands = lib.mapAttrsToList (
92 bin:
93 {
94 prependFlags ? [ ],
95 appendFlags ? [ ],
96 env ? { },
97 }:
98 assert
99 (lib.isList prependFlags && lib.all lib.isString prependFlags)
100 || throw "'${bin}.addFlags' must be a list of strings";
101 assert
102 (lib.isList appendFlags && lib.all lib.isString appendFlags)
103 || throw "'${bin}.addFlags' must be a list of strings";
104 assert
105 (lib.isAttrs env && lib.all lib.isString (lib.attrValues env))
106 || throw "'${bin}.env' must be an attribute set of strings";
107 let
108 envArgs = lib.concatStringsSep " " (lib.mapAttrsToList (k: v: "--set '${k}' '${v}'") env);
109 in
110 ''
111 wrapProgram $out/bin/${bin} \
112 --add-flags "${builtins.toString prependFlags}" \
113 --append-flags "${builtins.toString appendFlags}" \
114 ${envArgs}
115 ''
116 ) binaries;
117
118 # nasty script to replace all text references to ${pkg} with ${out}
119 # this is necessary because some files that get symlinked into the new
120 # package directory ($out) will still reference ${pkg}, leading to
121 # unexpected behavior
122 fixReferences = ''
123 # iterate through every file/symlink in $out
124 find "$out" \( -type l -o -type f \) | while read -r item; do
125 # if item is a symlink, we want to (recursively) find the file it
126 # points to, read it, and if it contains $pkg, overwrite the symlink
127 # with a regular file to replace $pkg with $out
128 if [ -L "$item" ]; then
129 # redirection is to ignore broken symlinks
130 target=$(readlink -f "$item" 2>/dev/null || true)
131
132 # if our target is a nonempty file containing pkg, replace item with target
133 if [ -n "$target" ] && [ -f "$target" ] && grep -F -q "${pkg}" "$target"; then
134 rm "$item"
135 cp "$target" "$item"
136 # otherwise, skip the file replacement code
137 else
138 continue
139 fi
140 fi
141
142 # $item is guaranteed to be a regular file
143 if grep -F -q "${pkg}" "$item"; then
144 # ensure $item is writeable
145 [ -w "$item" ] || chmod +w "$item"
146 # replace all occurances of $pkg with $out
147 sed -i "s|${pkg}|$out|g" "$item" || true
148 fi
149 done
150 '';
151 in
152 pkgs.symlinkJoin {
153 inherit pname;
154 inherit version;
155 passthru = (pkg.passthru or { }) // {
156 unwrapped = pkg;
157
158 override =
159 overrideArgs:
160 let
161 newPkg = pkg.override overrideArgs;
162 newArgs = args // {
163 pkg = newPkg;
164 };
165 in
166 wrap newArgs;
167
168 overrideAttrs =
169 f:
170 let
171 newPkg = pkg.overrideAttrs f;
172 newArgs = args // {
173 pkg = newPkg;
174 };
175 in
176 wrap newArgs;
177 };
178 paths = [ pkg ];
179 buildInputs = [ pkgs.makeWrapper ] ++ additionalDependencies;
180 postBuild = lib.concatStringsSep "\n" (wrapCommands ++ [ fixReferences ]);
181 };
182in
183wrap