{ 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"}" ]; } ) ) )} ${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}" ]; } ) ) } ${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 ]; # 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; }); }