{ lib, }: /* This is a set of tools to manipulate update scripts as recognized by update.nix. It is still very experimental with **instability** almost guaranteed so any use outside Nixpkgs is discouraged. update.nix currently accepts the following type: type UpdateScript // Simple path to script to execute script = FilePath // Path to execute plus arguments to pass it | [ (FilePath | String) ] // Advanced attribue set (experimental) | { // Script to execute (same as basic update script above) command : (FilePath | [ (FilePath | String) ]) // Features that the script supports // - commit: (experimental) returns commit message in stdout // - silent: (experimental) returns no stdout supportedFeatures : ?[ ("commit" | "silent") ] // Override attribute path detected by update.nix attrPath : ?String } */ let # type ShellArg = String | { __rawShell : String } /* Quotes all arguments to be safely passed to the Bourne shell. escapeShellArgs' : [ShellArg] -> String */ escapeShellArgs' = lib.concatMapStringsSep " " ( arg: if arg ? __rawShell then arg.__rawShell else lib.escapeShellArg arg ); /* processArg : { maxArgIndex : Int, args : [ShellArg], paths : [FilePath] } → (String|FilePath) → { maxArgIndex : Int, args : [ShellArg], paths : [FilePath] } Helper reducer function for building a command arguments where file paths are replaced with argv[x] reference. */ processArg = { maxArgIndex, args, paths, }: arg: if builtins.isPath arg then { args = args ++ [ { __rawShell = "\"\$${builtins.toString maxArgIndex}\""; } ]; maxArgIndex = maxArgIndex + 1; paths = paths ++ [ arg ]; } else { args = args ++ [ arg ]; inherit maxArgIndex paths; }; /* extractPaths : Int → [ (String|FilePath) ] → { maxArgIndex : Int, args : [ShellArg], paths : [FilePath] } Helper function that extracts file paths from command arguments and replaces them with argv[x] references. */ extractPaths = maxArgIndex: command: builtins.foldl' processArg { inherit maxArgIndex; args = [ ]; paths = [ ]; } command; /* processCommand : { maxArgIndex : Int, commands : [[ShellArg]], paths : [FilePath] } → [ (String|FilePath) ] → { maxArgIndex : Int, commands : [[ShellArg]], paths : [FilePath] } Helper reducer function for extracting file paths from individual commands. */ processCommand = { maxArgIndex, commands, paths, }: command: let new = extractPaths maxArgIndex command; in { commands = commands ++ [ new.args ]; paths = paths ++ new.paths; maxArgIndex = new.maxArgIndex; }; /* extractCommands : Int → [[ (String|FilePath) ]] → { maxArgIndex : Int, commands : [[ShellArg]], paths : [FilePath] } Helper function for extracting file paths from a list of commands and replacing them with argv[x] references. */ extractCommands = maxArgIndex: commands: builtins.foldl' processCommand { inherit maxArgIndex; commands = [ ]; paths = [ ]; } commands; /* commandsToShellInvocation : [[ (String|FilePath) ]] → [ (String|FilePath) ] Converts a list of commands into a single command by turning them into a shell script and passing them to `sh -c`. */ commandsToShellInvocation = commands: let extracted = extractCommands 0 commands; in [ "sh" "-ec" (lib.concatMapStringsSep ";" escapeShellArgs' extracted.commands) # We need paths as separate arguments so that update.nix can ensure they refer to the local directory # rather than a store path. ] ++ extracted.paths; in rec { /* normalize : UpdateScript → UpdateScript EXPERIMENTAL! Converts a basic update script to the experimental attribute set form. */ normalize = updateScript: { command = lib.toList (updateScript.command or updateScript); supportedFeatures = updateScript.supportedFeatures or [ ]; } // lib.optionalAttrs (updateScript ? attrPath) { inherit (updateScript) attrPath; }; /* sequence : [UpdateScript] → UpdateScript EXPERIMENTAL! Combines multiple update scripts to run in sequence. */ sequence = scripts: let scriptsNormalized = builtins.map normalize scripts; in let scripts = scriptsNormalized; hasCommitSupport = lib.findSingle ({ supportedFeatures, ... }: supportedFeatures == [ "commit" ]) null null scripts != null; hasSilentSupport = lib.findFirst ({ supportedFeatures, ... }: supportedFeatures == [ "silent" ]) null scripts != null; # Supported features currently only describe the format of the standard output of the update script. # Here we ensure that the standard output of the combined update script is well formed. validateFeatures = if hasCommitSupport then # Exactly one update script declares only “commit” feature and all the rest declare only “silent” feature. ({ supportedFeatures, ... }: supportedFeatures == [ "commit" ] || supportedFeatures == [ "silent" ]) else if hasSilentSupport then # All update scripts declare only “silent” feature. ({ supportedFeatures, ... }: supportedFeatures == [ "silent" ]) else # No update script declares any supported feature to fail loudly on unknown features rather than silently discard them. ({ supportedFeatures, ... }: supportedFeatures == [ ]); in assert lib.assertMsg (lib.all validateFeatures scripts) "Combining update scripts with features enabled (other than “silent” scripts and an optional single script with “commit”) is currently unsupported."; assert lib.assertMsg ( builtins.length ( lib.unique ( builtins.map ( { attrPath ? null, ... }: attrPath ) scripts ) ) == 1 ) "Combining update scripts with different attr paths is currently unsupported."; { command = commandsToShellInvocation (builtins.map ({ command, ... }: command) scripts); supportedFeatures = if hasCommitSupport then [ "commit" ] else if hasSilentSupport then [ "silent" ] else [ ]; }; /* copyAttrOutputToFile : String → FilePath → UpdateScript EXPERIMENTAL! Simple update script that copies the output of Nix derivation built by `attr` to `path`. */ copyAttrOutputToFile = attr: path: { command = [ "sh" "-c" "cp --no-preserve=all \"$(nix-build -A ${attr})\" \"$0\" > /dev/null" path ]; supportedFeatures = [ "silent" ]; }; }