diff --git a/modules/misc/nix-remote-build.nix b/modules/misc/nix-remote-build.nix new file mode 100644 index 000000000..d11bc2d68 --- /dev/null +++ b/modules/misc/nix-remote-build.nix @@ -0,0 +1,207 @@ +{ config, lib, ... }: + +let + inherit (lib) + concatStringsSep + mkIf + mkOption + optionalString + types + ; + + cfg = config.nix; +in +{ + options = { + nix = { + buildMachines = mkOption { + type = types.listOf ( + types.submodule { + options = { + hostName = mkOption { + type = types.str; + example = "nixbuilder.example.org"; + description = '' + The hostname of the build machine. + ''; + }; + protocol = mkOption { + type = types.enum [ + null + "ssh" + "ssh-ng" + ]; + default = "ssh"; + example = "ssh-ng"; + description = '' + The protocol used for communicating with the build machine. + Use `ssh-ng` if your remote builder and your + local Nix version support that improved protocol. + + Use `null` when trying to change the special localhost builder + without a protocol which is for example used by hydra. + ''; + }; + systems = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "x86_64-linux" + "aarch64-linux" + ]; + description = '' + The system types the build machine can execute derivations on. + ''; + }; + sshUser = mkOption { + type = types.nullOr types.str; + default = null; + example = "builder"; + description = '' + The username to log in as on the remote host. This user must be + able to log in and run nix commands non-interactively. It must + also be privileged to build derivations, so must be included in + {option}`nix.settings.trusted-users`. + ''; + }; + sshKey = mkOption { + type = types.nullOr types.str; + default = null; + example = "/root/.ssh/id_buildhost_builduser"; + description = '' + The path to the SSH private key with which to authenticate on + the build machine. The private key must not have a passphrase. + If null, the building user (root on NixOS machines) must have an + appropriate ssh configuration to log in non-interactively. + + Note that for security reasons, this path must point to a file + in the local filesystem, *not* to the nix store. + ''; + }; + maxJobs = mkOption { + type = types.int; + default = 1; + description = '' + The number of concurrent jobs the build machine supports. The + build machine will enforce its own limits, but this allows hydra + to schedule better since there is no work-stealing between build + machines. + ''; + }; + speedFactor = mkOption { + type = types.int; + default = 1; + description = '' + The relative speed of this builder. This is an arbitrary integer + that indicates the speed of this builder, relative to other + builders. Higher is faster. + ''; + }; + mandatoryFeatures = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "big-parallel" ]; + description = '' + A list of features mandatory for this builder. The builder will + be ignored for derivations that don't require all features in + this list. All mandatory features are automatically included in + {var}`supportedFeatures`. + ''; + }; + supportedFeatures = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "kvm" + "big-parallel" + ]; + description = '' + A list of features supported by this builder. The builder will + be ignored for derivations that require features not in this + list. + ''; + }; + publicHostKey = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The (base64-encoded) public host key of this builder. The field + is calculated via {command}`base64 -w0 /etc/ssh/ssh_host_type_key.pub`. + If null, SSH will use its regular known-hosts file when connecting. + ''; + }; + }; + } + ); + default = [ ]; + description = '' + This option lists the machines to be used if distributed builds are + enabled (see {option}`nix.distributedBuilds`). + Nix will perform derivations on those machines via SSH by copying the + inputs to the Nix store on the remote machine, starting the build, + then copying the output back to the local Nix store. + ''; + }; + + distributedBuilds = mkOption { + type = types.bool; + default = false; + description = '' + Whether to distribute builds to the machines listed in + {option}`nix.buildMachines`. + ''; + }; + }; + }; + + # distributedBuilds does *not* inhibit $XDG_CONFIG_HOME/nix/machines generation; caller may + # override that nix option. + config = mkIf cfg.enable ( + let + machineFilePath = "nix/machines"; + + isNixAtLeast = lib.versionAtLeast (lib.getVersion cfg.package); + in + { + # List of machines for distributed Nix builds + xdg.configFile.${machineFilePath} = mkIf (cfg.buildMachines != [ ]) { + text = lib.concatMapStrings ( + machine: + (concatStringsSep " " ( + [ + "${optionalString (machine.protocol != null) "${machine.protocol}://"}${ + optionalString (machine.sshUser != null) "${machine.sshUser}@" + }${machine.hostName}" + (concatStringsSep "," machine.systems) + (if machine.sshKey != null then machine.sshKey else "-") + (toString machine.maxJobs) + (toString machine.speedFactor) + ( + let + res = (machine.supportedFeatures ++ machine.mandatoryFeatures); + in + if (res == [ ]) then "-" else (concatStringsSep "," res) + ) + ( + let + res = machine.mandatoryFeatures; + in + if (res == [ ]) then "-" else (concatStringsSep "," machine.mandatoryFeatures) + ) + ] + ++ lib.optional (isNixAtLeast "2.4pre") ( + if machine.publicHostKey != null then machine.publicHostKey else "-" + ) + )) + + "\n" + ) cfg.buildMachines; + }; + + nix.settings = mkIf cfg.distributedBuilds { + builders = "@${config.xdg.configHome}/${machineFilePath}"; + }; + } + ); + + meta.maintainers = [ lib.maintainers.GaetanLepage ]; +} diff --git a/modules/modules.nix b/modules/modules.nix index 42e6a4a6d..52fbc5ce9 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -34,6 +34,7 @@ let ./misc/lib.nix ./misc/mozilla-messaging-hosts.nix ./misc/news.nix + ./misc/nix-remote-build.nix ./misc/nix.nix ./misc/nixgl.nix ./misc/numlock.nix diff --git a/tests/default.nix b/tests/default.nix index 73f485f2b..67563a1fa 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -181,6 +181,7 @@ import nmtSrc { ./modules/misc/manual ./modules/misc/news ./modules/misc/nix + ./modules/misc/nix-remote-build ./modules/misc/specialisation ./modules/misc/xdg ./modules/xresources diff --git a/tests/modules/misc/nix-remote-build/default.nix b/tests/modules/misc/nix-remote-build/default.nix new file mode 100644 index 000000000..7eb59cd45 --- /dev/null +++ b/tests/modules/misc/nix-remote-build/default.nix @@ -0,0 +1,4 @@ +{ + nix-remote-build-empty = ./empty-settings.nix; + nix-remote-build-example = ./example-settings.nix; +} diff --git a/tests/modules/misc/nix-remote-build/empty-settings.nix b/tests/modules/misc/nix-remote-build/empty-settings.nix new file mode 100644 index 000000000..a2b33787e --- /dev/null +++ b/tests/modules/misc/nix-remote-build/empty-settings.nix @@ -0,0 +1,11 @@ +{ config, ... }: + +{ + nix = { + package = config.lib.test.mkStubPackage { }; + }; + + nmt.script = '' + assertPathNotExists home-files/.config/nix + ''; +} diff --git a/tests/modules/misc/nix-remote-build/example-settings-expected b/tests/modules/misc/nix-remote-build/example-settings-expected new file mode 100644 index 000000000..243f730eb --- /dev/null +++ b/tests/modules/misc/nix-remote-build/example-settings-expected @@ -0,0 +1,2 @@ +ssh-ng://bob@foo.example.com aarch64-linux /path/to/ssh-key 2 4 benchmark,big-parallel,kvm,nixos-test,big-parallel big-parallel PUBLIC_HOST_KEY +ssh://alice@192.168.1.42 aarch64-darwin,x86_64-darwin ~/.ssh/id_rsa 1 1 apple-virt,big-parallel,nixos-test - PUBLIC_HOST_KEY_2 diff --git a/tests/modules/misc/nix-remote-build/example-settings.nix b/tests/modules/misc/nix-remote-build/example-settings.nix new file mode 100644 index 000000000..14733f48a --- /dev/null +++ b/tests/modules/misc/nix-remote-build/example-settings.nix @@ -0,0 +1,64 @@ +{ + config, + lib, + pkgs, + ... +}: + +{ + nix = { + package = config.lib.test.mkStubPackage { + version = lib.getVersion pkgs.nixVersions.stable; + }; + + distributedBuilds = true; + + buildMachines = [ + { + hostName = "foo.example.com"; + sshUser = "bob"; + sshKey = "/path/to/ssh-key"; + publicHostKey = "PUBLIC_HOST_KEY"; + systems = [ "aarch64-linux" ]; + speedFactor = 4; + protocol = "ssh-ng"; + maxJobs = 2; + supportedFeatures = [ + "benchmark" + "big-parallel" + "kvm" + "nixos-test" + ]; + mandatoryFeatures = [ + "big-parallel" + ]; + } + { + hostName = "192.168.1.42"; + sshUser = "alice"; + sshKey = "~/.ssh/id_rsa"; + publicHostKey = "PUBLIC_HOST_KEY_2"; + systems = [ + "aarch64-darwin" + "x86_64-darwin" + ]; + supportedFeatures = [ + "apple-virt" + "big-parallel" + "nixos-test" + ]; + } + ]; + }; + + nmt.script = '' + assertFileExists "home-files/.config/nix/machines" + + assertFileContent \ + home-files/.config/nix/machines \ + ${./example-settings-expected} + + assertFileContains home-files/.config/nix/nix.conf \ + 'builders = @${config.xdg.configHome}/nix/machines' + ''; +}