Skip to content

Commit d8fe9f4

Browse files
authored
nixos-rebuild-ng: allow deploying closures (#482430)
2 parents a8c93ba + 978a08c commit d8fe9f4

File tree

7 files changed

+334
-3
lines changed

7 files changed

+334
-3
lines changed

nixos/tests/all-tests.nix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,9 @@ in
11071107
nixos-rebuild-specialisations = runTestOn [ "x86_64-linux" ] {
11081108
imports = [ ./nixos-rebuild-specialisations.nix ];
11091109
};
1110+
nixos-rebuild-store-path = runTestOn [ "x86_64-linux" ] {
1111+
imports = [ ./nixos-rebuild-store-path.nix ];
1112+
};
11101113
nixos-rebuild-target-host = runTest {
11111114
imports = [ ./nixos-rebuild-target-host.nix ];
11121115
};
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{ hostPkgs, ... }:
2+
{
3+
name = "nixos-rebuild-store-path";
4+
5+
# TODO: remove overlay from nixos/modules/profiles/installation-device.nix
6+
# make it a _small package instead, then remove pkgsReadOnly = false;.
7+
node.pkgsReadOnly = false;
8+
9+
nodes = {
10+
machine =
11+
{ lib, pkgs, ... }:
12+
{
13+
imports = [
14+
../modules/profiles/installation-device.nix
15+
../modules/profiles/base.nix
16+
];
17+
18+
nix.settings = {
19+
substituters = lib.mkForce [ ];
20+
hashed-mirrors = null;
21+
connect-timeout = 1;
22+
};
23+
24+
system.includeBuildDependencies = true;
25+
26+
system.extraDependencies = [
27+
# Not part of the initial build apparently?
28+
pkgs.grub2
29+
];
30+
31+
system.switch.enable = true;
32+
33+
virtualisation = {
34+
cores = 2;
35+
memorySize = 4096;
36+
};
37+
};
38+
};
39+
40+
testScript =
41+
let
42+
configFile =
43+
hostname:
44+
hostPkgs.writeText "configuration.nix" # nix
45+
''
46+
{ lib, pkgs, ... }: {
47+
imports = [
48+
./hardware-configuration.nix
49+
<nixpkgs/nixos/modules/testing/test-instrumentation.nix>
50+
];
51+
52+
boot.loader.grub = {
53+
enable = true;
54+
device = "/dev/vda";
55+
forceInstall = true;
56+
};
57+
58+
documentation.enable = false;
59+
60+
networking.hostName = "${hostname}";
61+
}
62+
'';
63+
64+
in
65+
# python
66+
''
67+
machine.start()
68+
machine.succeed("udevadm settle")
69+
machine.wait_for_unit("multi-user.target")
70+
71+
machine.succeed("nixos-generate-config")
72+
73+
with subtest("Build configuration without switching"):
74+
machine.copy_from_host(
75+
"${configFile "store-path-test"}",
76+
"/etc/nixos/configuration.nix",
77+
)
78+
store_path = machine.succeed("nix-build '<nixpkgs/nixos>' -A system --no-out-link").strip()
79+
machine.succeed(f"test -f {store_path}/nixos-version")
80+
81+
with subtest("Switch using --store-path"):
82+
machine.succeed(f"nixos-rebuild switch --store-path {store_path}")
83+
hostname = machine.succeed("cat /etc/hostname").strip()
84+
assert hostname == "store-path-test", f"Expected hostname 'store-path-test', got '{hostname}'"
85+
86+
with subtest("Test using --store-path"):
87+
machine.copy_from_host(
88+
"${configFile "store-path-test-2"}",
89+
"/etc/nixos/configuration.nix",
90+
)
91+
store_path_2 = machine.succeed("nix-build '<nixpkgs/nixos>' -A system --no-out-link").strip()
92+
machine.succeed(f"nixos-rebuild test --store-path {store_path_2}")
93+
hostname = machine.succeed("cat /etc/hostname").strip()
94+
assert hostname == "store-path-test-2", f"Expected hostname 'store-path-test-2', got '{hostname}'"
95+
96+
with subtest("Ensure --store-path rejects invalid combinations"):
97+
machine.fail(f"nixos-rebuild switch --store-path {store_path} --rollback")
98+
machine.fail(f"nixos-rebuild switch --store-path {store_path} --flake .")
99+
machine.fail(f"nixos-rebuild build --store-path {store_path}")
100+
'';
101+
}

pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ nixos-rebuild - reconfigure a NixOS machine
2121
_nixos-rebuild_ \[--verbose] [--quiet] [--max-jobs MAX_JOBS] [--cores CORES] [--log-format LOG_FORMAT] [--keep-going] [--keep-failed] [--fallback] [--repair] [--option OPTION OPTION] [--builders BUILDERS] [--include INCLUDE]++
2222
\[--print-build-logs] [--show-trace] [--accept-flake-config] [--refresh] [--impure] [--offline] [--no-net] [--recreate-lock-file] [--no-update-lock-file] [--no-write-lock-file] [--no-registries] [--commit-lock-file]++
2323
\[--update-input UPDATE_INPUT] [--override-input OVERRIDE_INPUT OVERRIDE_INPUT] [--no-build-output] [--use-substitutes] [--help] [--debug] [--file FILE] [--attr ATTR] [--flake [FLAKE]] [--no-flake] [--install-bootloader]++
24-
\[--profile-name PROFILE_NAME] [--specialisation SPECIALISATION] [--rollback] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--no-reexec]++
24+
\[--profile-name PROFILE_NAME] [--specialisation SPECIALISATION] [--rollback] [--store-path STORE_PATH] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--no-reexec]++
2525
\[--build-host BUILD_HOST] [--target-host TARGET_HOST] [--no-build-nix] [--image-variant IMAGE_VARIANT]++
2626
\[{switch,boot,test,build,edit,repl,dry-build,dry-run,dry-activate,build-image,build-vm,build-vm-with-bootloader,list-generations}]
2727

@@ -182,6 +182,20 @@ It must be one of the following:
182182
(The previous configuration is defined as the one before the “current”
183183
generation of the Nix profile _/nix/var/nix/profiles/system_.)
184184

185+
*--store-path* _path_
186+
Use a pre-built NixOS system store path at _path_ instead of evaluating
187+
and building from the configuration. This skips the evaluation and build
188+
phases entirely. The path must be a valid NixOS system closure
189+
(containing _nixos-version_ and _bin/switch-to-configuration_).
190+
191+
This is useful for deploying closures that were built elsewhere, such as
192+
in CI systems or on remote build machines.
193+
194+
Can only be used with *switch*, *boot*, *test*, and *dry-activate*
195+
actions. Mutually exclusive with *--rollback*, *--flake*, *--file*, and
196+
*--attr*. The *--build-host* option is ignored when *--store-path* is
197+
specified.
198+
185199
*--builders* _builder-spec_
186200
Allow ad-hoc remote builders for building the new system. This requires
187201
the user executing *nixos-rebuild* (usually root) to be configured as a

pkgs/by-name/ni/nixos-rebuild-ng/package.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ python3Packages.buildPythonApplication rec {
9898
# FIXME: this test is disabled since it times out in @ofborg
9999
# nixos-rebuild-install-bootloader
100100
nixos-rebuild-specialisations
101+
nixos-rebuild-store-path
101102
nixos-rebuild-target-host
102103
;
103104
repl = callPackage ./tests/repl.nix { };

pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
136136
action="store_true",
137137
help="Roll back to the previous configuration",
138138
)
139+
main_parser.add_argument(
140+
"--store-path",
141+
metavar="PATH",
142+
help="Use a pre-built NixOS system store path instead of building",
143+
)
139144
main_parser.add_argument(
140145
"--upgrade",
141146
action="store_true",
@@ -269,6 +274,22 @@ def parser_warn(msg: str) -> None:
269274
if args.flake and (args.file or args.attr):
270275
parser.error("--flake cannot be used with --file or --attr")
271276

277+
if args.store_path:
278+
if args.rollback:
279+
parser.error("--store-path and --rollback are mutually exclusive")
280+
if args.flake or args.file or args.attr:
281+
parser.error("--store-path cannot be used with --flake, --file, or --attr")
282+
if args.action not in (
283+
Action.SWITCH.value,
284+
Action.BOOT.value,
285+
Action.TEST.value,
286+
Action.DRY_ACTIVATE.value,
287+
):
288+
parser.error(f"--store-path cannot be used with '{args.action}'")
289+
if args.flake is None:
290+
# Disable flake auto-detection since we're using a pre-built store path
291+
args.flake = False
292+
272293
return args, grouped_nix_args
273294

274295

@@ -297,7 +318,7 @@ def execute(argv: list[str]) -> None:
297318
build_attr = BuildAttr.from_arg(args.attr, args.file)
298319
flake = Flake.from_arg(args.flake, target_host)
299320

300-
if can_run and not flake:
321+
if can_run and not flake and not args.store_path:
301322
services.write_version_suffix(grouped_nix_args)
302323

303324
match action:

pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,14 @@ def build_and_activate_system(
290290
grouped_nix_args=grouped_nix_args,
291291
)
292292

293-
if args.rollback:
293+
if args.store_path:
294+
path_to_config = Path(args.store_path)
295+
nix.copy_closure(
296+
path_to_config,
297+
to_host=target_host,
298+
copy_flags=grouped_nix_args.copy_flags,
299+
)
300+
elif args.rollback:
294301
path_to_config = _rollback_system(
295302
action=action,
296303
args=args,

0 commit comments

Comments
 (0)