diff --git a/pkgs/common-updater/combinators.nix b/pkgs/common-updater/combinators.nix new file mode 100644 index 000000000000..93fdac52f7c4 --- /dev/null +++ b/pkgs/common-updater/combinators.nix @@ -0,0 +1,128 @@ +{ 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 + supportedFeatures : ?[ "commit" ] + // 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" + "-c" + (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; + validateFeatures = ({ supportedFeatures, ... }: supportedFeatures == [ ]); + in + + assert lib.assertMsg (lib.all validateFeatures scripts) "Combining update scripts with features enabled 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."; + + commandsToShellInvocation (builtins.map ({ command, ... }: command) scripts); +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 6db187b74fd5..00a1836c806f 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -124,6 +124,8 @@ with pkgs; genericUpdater = callPackage ../common-updater/generic-updater.nix { }; + update-script-combinators = callPackage ../common-updater/combinators.nix { }; + gitUpdater = callPackage ../common-updater/git-updater.nix { }; httpTwoLevelsUpdater = callPackage ../common-updater/http-two-levels-updater.nix { };