summaryrefslogtreecommitdiffstats
path: root/pkgs/development/misc/resholve/resholve-utils.nix
blob: 648de4bddea7dfd9876868f1d1d3348fada11d43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
{
  lib,
  stdenv,
  resholve,
  binlore,
  writeTextFile,
}:

rec {
  /*
    These functions break up the work of partially validating the
    'solutions' attrset and massaging it into env/cli args.

    Note: some of the left-most args do not *have* to be passed as
    deep as they are, but I've done so to provide more error context
  */

  # for brevity / line length
  spaces = l: builtins.concatStringsSep " " l;
  colons = l: builtins.concatStringsSep ":" l;
  semicolons = l: builtins.concatStringsSep ";" l;

  # Throw a fit with dotted attr path context
  nope = path: msg: throw "${builtins.concatStringsSep "." path}: ${msg}";

  # Special-case directive value representations by type
  phraseDirective =
    solution: env: name: val:
    if builtins.isInt val then
      toString val
    else if builtins.isString val then
      name
    else if true == val then
      name
    else if false == val then
      "" # omit!
    else if null == val then
      "" # omit!
    else if builtins.isList val then
      "${name}:${semicolons (map lib.escapeShellArg val)}"
    else
      nope [
        solution
        env
        name
      ] "unexpected type: ${builtins.typeOf val}";

  # Build fake/fix/keep directives from Nix types
  phraseDirectives =
    solution: env: val:
    lib.mapAttrsToList (phraseDirective solution env) val;

  # Custom ~search-path routine to handle relative path strings
  relSafeBinPath =
    input:
    if lib.isDerivation input then
      ((lib.getOutput "bin" input) + "/bin")
    else if builtins.isString input then
      input
    else
      throw "unexpected type for input: ${builtins.typeOf input}";

  # Special-case value representation by type/name
  phraseEnvVal =
    solution: env: val:
    if env == "inputs" then
      (colons (map relSafeBinPath val))
    else if builtins.isString val then
      val
    else if builtins.isList val then
      spaces val
    else if builtins.isAttrs val then
      spaces (phraseDirectives solution env val)
    else
      nope [
        solution
        env
      ] "unexpected type: ${builtins.typeOf val}";

  # Shell-format each env value
  shellEnv =
    solution: env: value:
    lib.escapeShellArg (phraseEnvVal solution env value);

  # Build a single ENV=val pair
  phraseEnv =
    solution: env: value:
    "RESHOLVE_${lib.toUpper env}=${shellEnv solution env value}";

  /*
    Discard attrs:
    - claimed by phraseArgs
    - only needed for binlore.collect
  */
  removeUnneededArgs =
    value:
    removeAttrs value [
      "scripts"
      "flags"
      "unresholved"
    ];

  # Verify required arguments are present
  validateSolution =
    {
      scripts,
      inputs,
      interpreter,
      ...
    }:
    true;

  # Pull out specific solution keys to build ENV=val pairs
  phraseEnvs =
    solution: value: spaces (lib.mapAttrsToList (phraseEnv solution) (removeUnneededArgs value));

  # Pull out specific solution keys to build CLI argstring
  phraseArgs =
    {
      flags ? [ ],
      scripts,
      ...
    }:
    spaces (flags ++ scripts);

  phraseBinloreArgs =
    value:
    let
      hasUnresholved = builtins.hasAttr "unresholved" value;
    in
    {
      drvs = value.inputs ++ lib.optionals hasUnresholved [ value.unresholved ];
      strip = if hasUnresholved then [ value.unresholved ] else [ ];
    };

  # Build a single resholve invocation
  phraseInvocation =
    solution: value:
    if validateSolution value then
      # we pass resholve a directory
      "RESHOLVE_LORE=${binlore.collect (phraseBinloreArgs value)} ${phraseEnvs solution value} ${resholve}/bin/resholve --overwrite ${phraseArgs value}"
    else
      throw "invalid solution"; # shouldn't trigger for now

  injectUnresholved =
    solutions: unresholved:
    (builtins.mapAttrs (name: value: value // { inherit unresholved; }) solutions);

  # Build resholve invocation for each solution.
  phraseCommands =
    solutions: unresholved:
    builtins.concatStringsSep "\n" (
      lib.mapAttrsToList phraseInvocation (injectUnresholved solutions unresholved)
    );

  /*
    subshell/PS4/set -x and : command to output resholve envs
    and invocation. Extra context makes it clearer what the
    Nix API is doing, makes nix-shell debugging easier, etc.
  */
  phraseContext =
    {
      invokable,
      prep ? ''cd "$out"'',
    }:
    ''
      (
        ${prep}
        PS4=$'\x1f'"\033[33m[resholve context]\033[0m "
        set -x
        : invoking resholve with PWD=$PWD
        ${invokable}
      )
    '';
  phraseContextForPWD =
    invokable:
    phraseContext {
      inherit invokable;
      prep = "";
    };
  phraseContextForOut = invokable: phraseContext { inherit invokable; };

  phraseSolution = name: solution: (phraseContextForOut (phraseInvocation name solution));
  phraseSolutions =
    solutions: unresholved: phraseContextForOut (phraseCommands solutions unresholved);

  writeScript =
    name: partialSolution: text:
    writeTextFile {
      inherit name text;
      executable = true;
      checkPhase = ''
        ${
          (phraseContextForPWD (
            phraseInvocation name (
              partialSolution
              // {
                scripts = [ "${placeholder "out"}" ];
              }
            )
          ))
        }
      '';
    };
  writeScriptBin =
    name: partialSolution: text:
    writeTextFile rec {
      inherit name text;
      executable = true;
      destination = "/bin/${name}";
      checkPhase = ''
        ${phraseContextForOut (
          phraseInvocation name (
            partialSolution
            // {
              scripts = [ "bin/${name}" ];
            }
          )
        )}
      '';
    };
  /*
    This uses nested derivations so we can:
    - let project build however it needs in inner "unresholved" drv
    - pass this drv through binlore to collect info on exec behavior
      in any scripts and binaries in its output
    - resholve with lore unresholved source and lore as inputs

    some concerns:
    - resholve drv isn't keeping many of the caller's args,
      (to avoid re-triggering phases and such in outer build)
      so they can't readily set LOGLEVEL and such...
    - not sure how this affects multiple outputs
  */
  mkDerivation = lib.extendMkDerivation {
    constructDrv = stdenv.mkDerivation;
    excludeDrvArgNames = [
      "postResholve"
      "solutions"
    ];
    extendDrvArgs =
      finalAttrs:
      {
        pname,
        src,
        version,
        passthru ? { },
        solutions,
        postResholve ? "",
        ...
      }@args:
      {
        pname = "${pname}-unresholved";
        passthru = passthru // {
          # needed to resholve in outer drv
          inherit postResholve solutions;
        };
      };
    transformDrv =
      unresholved:
      stdenv.mkDerivation {
        src = unresholved;
        pname = lib.removeSuffix "-unresholved" unresholved.pname;
        inherit (unresholved) version;
        buildInputs = [ resholve ];
        disallowedReferences = [ resholve ];

        passthru = unresholved.passthru // {
          # retain a reference to the base
          unresholved = unresholved;
          # fallback attr for update bot to query our src
          originalSrc = unresholved.src;
        };

        # do these imply that we should use NoCC or something?
        dontConfigure = true;
        dontBuild = true;

        installPhase = ''
          cp -R $src $out
        '';

        # enable below for verbose debug info if needed
        # supports default python.logging levels
        # LOGLEVEL="INFO";
        preFixup = phraseSolutions unresholved.solutions unresholved;

        postFixup = unresholved.postResholve;

        inherit (unresholved) meta strictDeps __structuredAttrs;
      };
  };
}