{ 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 builtins.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"}" ]; } ) )) } '' + lib.optionalString (partialSolution.interpreter != "none") '' ${partialSolution.interpreter} -n $out ''; }; writeScriptBin = name: partialSolution: text: writeTextFile rec { inherit name text; executable = true; destination = "/bin/${name}"; checkPhase = '' ${phraseContextForOut ( phraseInvocation name ( partialSolution // { scripts = [ "bin/${name}" ]; } ) )} '' + lib.optionalString (partialSolution.interpreter != "none") '' ${partialSolution.interpreter} -n $out/bin/${name} ''; }; mkDerivation = { pname, src, version, passthru ? { }, solutions, ... }@attrs: let inherit stdenv; /* Knock out our special solutions arg, but otherwise just build what the caller is giving us. We'll actually resholve it separately below (after we generate binlore for it). */ unresholved = ( stdenv.mkDerivation ( (removeAttrs attrs [ "solutions" ]) // { inherit version src; pname = "${pname}-unresholved"; } ) ); in /* resholve in a separate derivation; some concerns: - we aren't keeping many of the user's args, so they can't readily set LOGLEVEL and such... - not sure how this affects multiple outputs */ lib.extendDerivation true passthru ( stdenv.mkDerivation { src = unresholved; inherit version pname; buildInputs = [ resholve ]; disallowedReferences = [ resholve ]; # retain a reference to the base passthru = unresholved.passthru // { 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 solutions unresholved; # don't break the metadata... meta = unresholved.meta; } ); }