about summary refs log tree commit diff
path: root/pkgs/common-updater/combinators.nix
blob: 93fdac52f7c46d821b663879a6f3218d4b3bfe21 (plain) (blame)
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
{ 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);
}