1{ lib, stdenv, resholve, binlore, writeTextFile }:
2
3rec {
4 /* These functions break up the work of partially validating the
5 'solutions' attrset and massaging it into env/cli args.
6
7 Note: some of the left-most args do not *have* to be passed as
8 deep as they are, but I've done so to provide more error context
9 */
10
11 # for brevity / line length
12 spaces = l: builtins.concatStringsSep " " l;
13 colons = l: builtins.concatStringsSep ":" l;
14 semicolons = l: builtins.concatStringsSep ";" l;
15
16 /* Throw a fit with dotted attr path context */
17 nope = path: msg:
18 throw "${builtins.concatStringsSep "." path}: ${msg}";
19
20 /* Special-case directive value representations by type */
21 phraseDirective = solution: env: name: val:
22 if builtins.isInt val then builtins.toString val
23 else if builtins.isString val then name
24 else if true == val then name
25 else if false == val then "" # omit!
26 else if null == val then "" # omit!
27 else if builtins.isList val then "${name}:${semicolons (map lib.escapeShellArg val)}"
28 else nope [ solution env name ] "unexpected type: ${builtins.typeOf val}";
29
30 /* Build fake/fix/keep directives from Nix types */
31 phraseDirectives = solution: env: val:
32 lib.mapAttrsToList (phraseDirective solution env) val;
33
34 /* Custom ~search-path routine to handle relative path strings */
35 relSafeBinPath = input:
36 if lib.isDerivation input then ((lib.getOutput "bin" input) + "/bin")
37 else if builtins.isString input then input
38 else throw "unexpected type for input: ${builtins.typeOf input}";
39
40 /* Special-case value representation by type/name */
41 phraseEnvVal = solution: env: val:
42 if env == "inputs" then (colons (map relSafeBinPath val))
43 else if builtins.isString val then val
44 else if builtins.isList val then spaces val
45 else if builtins.isAttrs val then spaces (phraseDirectives solution env val)
46 else nope [ solution env ] "unexpected type: ${builtins.typeOf val}";
47
48 /* Shell-format each env value */
49 shellEnv = solution: env: value:
50 lib.escapeShellArg (phraseEnvVal solution env value);
51
52 /* Build a single ENV=val pair */
53 phraseEnv = solution: env: value:
54 "RESHOLVE_${lib.toUpper env}=${shellEnv solution env value}";
55
56 /* Discard attrs:
57 - claimed by phraseArgs
58 - only needed for binlore.collect
59 */
60 removeUnneededArgs = value:
61 removeAttrs value [ "scripts" "flags" "unresholved" ];
62
63 /* Verify required arguments are present */
64 validateSolution = { scripts, inputs, interpreter, ... }: true;
65
66 /* Pull out specific solution keys to build ENV=val pairs */
67 phraseEnvs = solution: value:
68 spaces (lib.mapAttrsToList (phraseEnv solution) (removeUnneededArgs value));
69
70 /* Pull out specific solution keys to build CLI argstring */
71 phraseArgs = { flags ? [ ], scripts, ... }:
72 spaces (flags ++ scripts);
73
74 phraseBinloreArgs = value:
75 let
76 hasUnresholved = builtins.hasAttr "unresholved" value;
77 in {
78 drvs = value.inputs ++
79 lib.optionals hasUnresholved [ value.unresholved ];
80 strip = if hasUnresholved then [ value.unresholved ] else [ ];
81 };
82
83 /* Build a single resholve invocation */
84 phraseInvocation = solution: value:
85 if validateSolution value then
86 # we pass resholve a directory
87 "RESHOLVE_LORE=${binlore.collect (phraseBinloreArgs value) } ${phraseEnvs solution value} ${resholve}/bin/resholve --overwrite ${phraseArgs value}"
88 else throw "invalid solution"; # shouldn't trigger for now
89
90 injectUnresholved = solutions: unresholved: (builtins.mapAttrs (name: value: value // { inherit unresholved; } ) solutions);
91
92 /* Build resholve invocation for each solution. */
93 phraseCommands = solutions: unresholved:
94 builtins.concatStringsSep "\n" (
95 lib.mapAttrsToList phraseInvocation (injectUnresholved solutions unresholved)
96 );
97
98 /*
99 subshell/PS4/set -x and : command to output resholve envs
100 and invocation. Extra context makes it clearer what the
101 Nix API is doing, makes nix-shell debugging easier, etc.
102 */
103 phraseContext = { invokable, prep ? ''cd "$out"'' }: ''
104 (
105 ${prep}
106 PS4=$'\x1f'"\033[33m[resholve context]\033[0m "
107 set -x
108 : invoking resholve with PWD=$PWD
109 ${invokable}
110 )
111 '';
112 phraseContextForPWD = invokable: phraseContext { inherit invokable; prep = ""; };
113 phraseContextForOut = invokable: phraseContext { inherit invokable; };
114
115 phraseSolution = name: solution: (phraseContextForOut (phraseInvocation name solution));
116 phraseSolutions = solutions: unresholved:
117 phraseContextForOut (phraseCommands solutions unresholved);
118
119 writeScript = name: partialSolution: text:
120 writeTextFile {
121 inherit name text;
122 executable = true;
123 checkPhase = ''
124 ${(phraseContextForPWD (
125 phraseInvocation name (
126 partialSolution // {
127 scripts = [ "${placeholder "out"}" ];
128 }
129 )
130 )
131 )}
132 ${partialSolution.interpreter} -n $out
133 '';
134 };
135 writeScriptBin = name: partialSolution: text:
136 writeTextFile rec {
137 inherit name text;
138 executable = true;
139 destination = "/bin/${name}";
140 checkPhase = ''
141 ${phraseContextForOut (
142 phraseInvocation name (
143 partialSolution // {
144 scripts = [ "bin/${name}" ];
145 }
146 )
147 )
148 }
149 ${partialSolution.interpreter} -n $out/bin/${name}
150 '';
151 };
152 mkDerivation = { pname
153 , src
154 , version
155 , passthru ? { }
156 , solutions
157 , ...
158 }@attrs:
159 let
160 inherit stdenv;
161
162 /*
163 Knock out our special solutions arg, but otherwise
164 just build what the caller is giving us. We'll
165 actually resholve it separately below (after we
166 generate binlore for it).
167 */
168 unresholved = (stdenv.mkDerivation ((removeAttrs attrs [ "solutions" ])
169 // {
170 inherit version src;
171 pname = "${pname}-unresholved";
172 }));
173 in
174 /*
175 resholve in a separate derivation; some concerns:
176 - we aren't keeping many of the user's args, so they
177 can't readily set LOGLEVEL and such...
178 - not sure how this affects multiple outputs
179 */
180 lib.extendDerivation true passthru (stdenv.mkDerivation {
181 src = unresholved;
182 inherit version pname;
183 buildInputs = [ resholve ];
184
185 # retain a reference to the base
186 passthru = unresholved.passthru // {
187 unresholved = unresholved;
188 # fallback attr for update bot to query our src
189 originalSrc = unresholved.src;
190 };
191
192 # do these imply that we should use NoCC or something?
193 dontConfigure = true;
194 dontBuild = true;
195
196 installPhase = ''
197 cp -R $src $out
198 '';
199
200 # enable below for verbose debug info if needed
201 # supports default python.logging levels
202 # LOGLEVEL="INFO";
203 preFixup = phraseSolutions solutions unresholved;
204
205 # don't break the metadata...
206 meta = unresholved.meta;
207 });
208}