nixos-rebuild-ng: implement build-image (#371142)
This commit is contained in:
@@ -22,8 +22,9 @@ _nixos-rebuild_ \[--verbose] [--max-jobs MAX_JOBS] [--cores CORES] [--log-format
|
||||
\[--no-update-lock-file] [--no-write-lock-file] [--no-registries] [--commit-lock-file] [--update-input UPDATE_INPUT] [--override-input OVERRIDE_INPUT OVERRIDE_INPUT]++
|
||||
\[--no-build-output] [--use-substitutes] [--help] [--file FILE] [--attr ATTR] [--flake [FLAKE]] [--no-flake] [--install-bootloader] [--profile-name PROFILE_NAME]++
|
||||
\[--specialisation SPECIALISATION] [--rollback] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--fast]++
|
||||
\[--image-variant VARIANT]++
|
||||
\[--build-host BUILD_HOST] [--target-host TARGET_HOST]++
|
||||
\[{switch,boot,test,build,edit,repl,dry-build,dry-run,dry-activate,build-vm,build-vm-with-bootloader,list-generations}]
|
||||
\[{switch,boot,test,build,edit,repl,dry-build,dry-run,dry-activate,build-image,build-vm,build-vm-with-bootloader,list-generations}]
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
@@ -106,6 +107,13 @@ It must be one of the following:
|
||||
*repl*
|
||||
Opens the configuration in *nix repl*.
|
||||
|
||||
*build-image*
|
||||
Build a disk-image variant, pre-configured for the given
|
||||
platform/provider. Select a variant with the *--image-variant* option
|
||||
or run without any options to get a list of available variants.
|
||||
|
||||
$ nixos-rebuild build-image --image-variant proxmox
|
||||
|
||||
*build-vm*
|
||||
Build a script that starts a NixOS virtual machine with the desired
|
||||
configuration. It leaves a symlink _result_ in the current directory that
|
||||
@@ -206,6 +214,11 @@ It must be one of the following:
|
||||
Activates given specialisation; when not specified, switching and testing
|
||||
will activate the base, unspecialised system.
|
||||
|
||||
*--image-variant* _variant_
|
||||
Selects an image variant to build from the _config.system.build.images_
|
||||
attribute of the given configuration. A list of variants is printed if
|
||||
this option remains unset.
|
||||
|
||||
*--build-host* _host_
|
||||
Instead of building the new configuration locally, use the specified host
|
||||
to perform the build. The host needs to be accessible with ssh, and must
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import assert_never
|
||||
|
||||
from . import nix, tmpdir
|
||||
from .constants import EXECUTABLE, WITH_NIX_2_18, WITH_REEXEC, WITH_SHELL_FILES
|
||||
from .models import Action, BuildAttr, Flake, NRError, Profile
|
||||
from .models import Action, BuildAttr, Flake, ImageVariants, NRError, Profile
|
||||
from .process import Remote, cleanup_ssh
|
||||
from .utils import Args, LogFormatter, tabulate
|
||||
|
||||
@@ -176,6 +176,11 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
|
||||
"--target-host", help="Specifies host to activate the configuration"
|
||||
)
|
||||
main_parser.add_argument("--no-build-nix", action="store_true", help="Deprecated")
|
||||
main_parser.add_argument(
|
||||
"--image-variant",
|
||||
help="Selects an image variant to build from the "
|
||||
+ "config.system.build.images attribute of the given configuration",
|
||||
)
|
||||
main_parser.add_argument("action", choices=Action.values(), nargs="?")
|
||||
|
||||
return main_parser, sub_parsers
|
||||
@@ -285,8 +290,7 @@ def reexec(
|
||||
)
|
||||
except CalledProcessError:
|
||||
logger.warning(
|
||||
"could not build a newer version of nixos-rebuild, "
|
||||
+ "using current version"
|
||||
"could not build a newer version of nixos-rebuild, using current version"
|
||||
)
|
||||
|
||||
if drv:
|
||||
@@ -372,6 +376,7 @@ def execute(argv: list[str]) -> None:
|
||||
| Action.BUILD
|
||||
| Action.DRY_BUILD
|
||||
| Action.DRY_ACTIVATE
|
||||
| Action.BUILD_IMAGE
|
||||
| Action.BUILD_VM
|
||||
| Action.BUILD_VM_WITH_BOOTLOADER
|
||||
):
|
||||
@@ -383,7 +388,29 @@ def execute(argv: list[str]) -> None:
|
||||
flake_build_flags |= {"no_link": no_link, "dry_run": dry_run}
|
||||
rollback = bool(args.rollback)
|
||||
|
||||
def validate_image_variant(variants: ImageVariants) -> None:
|
||||
if args.image_variant not in variants:
|
||||
raise NRError(
|
||||
"please specify one of the following "
|
||||
+ "supported image variants via --image-variant:\n"
|
||||
+ "\n".join(f"- {v}" for v in variants.keys())
|
||||
)
|
||||
|
||||
match action:
|
||||
case Action.BUILD_IMAGE if flake:
|
||||
variants = nix.get_build_image_variants_flake(
|
||||
flake,
|
||||
eval_flags=flake_common_flags,
|
||||
)
|
||||
validate_image_variant(variants)
|
||||
attr = f"config.system.build.images.{args.image_variant}"
|
||||
case Action.BUILD_IMAGE:
|
||||
variants = nix.get_build_image_variants(
|
||||
build_attr,
|
||||
instantiate_flags=common_flags,
|
||||
)
|
||||
validate_image_variant(variants)
|
||||
attr = f"config.system.build.images.{args.image_variant}"
|
||||
case Action.BUILD_VM:
|
||||
attr = "config.system.build.vm"
|
||||
case Action.BUILD_VM_WITH_BOOTLOADER:
|
||||
@@ -460,25 +487,37 @@ def execute(argv: list[str]) -> None:
|
||||
sudo=args.sudo,
|
||||
)
|
||||
|
||||
if action in (Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE):
|
||||
nix.switch_to_configuration(
|
||||
path_to_config,
|
||||
action,
|
||||
target_host=target_host,
|
||||
sudo=args.sudo,
|
||||
specialisation=args.specialisation,
|
||||
install_bootloader=args.install_bootloader,
|
||||
)
|
||||
elif action in (Action.BUILD_VM, Action.BUILD_VM_WITH_BOOTLOADER):
|
||||
# If you get `not-found`, please open an issue
|
||||
vm_path = next(path_to_config.glob("bin/run-*-vm"), "not-found")
|
||||
print(
|
||||
f"Done. The virtual machine can be started by running '{vm_path}'"
|
||||
)
|
||||
# Print only the result to stdout to make it easier to script
|
||||
def print_result(msg: str, result: str | Path) -> None:
|
||||
print(msg, end=" ", file=sys.stderr, flush=True)
|
||||
print(result, flush=True)
|
||||
|
||||
match action:
|
||||
case Action.SWITCH | Action.BOOT | Action.TEST | Action.DRY_ACTIVATE:
|
||||
nix.switch_to_configuration(
|
||||
path_to_config,
|
||||
action,
|
||||
target_host=target_host,
|
||||
sudo=args.sudo,
|
||||
specialisation=args.specialisation,
|
||||
install_bootloader=args.install_bootloader,
|
||||
)
|
||||
case Action.BUILD_VM | Action.BUILD_VM_WITH_BOOTLOADER:
|
||||
# If you get `not-found`, please open an issue
|
||||
vm_path = next(path_to_config.glob("bin/run-*-vm"), "not-found")
|
||||
print_result(
|
||||
"Done. The virtual machine can be started by running", vm_path
|
||||
)
|
||||
case Action.BUILD_IMAGE:
|
||||
disk_path = path_to_config / variants[args.image_variant]
|
||||
print_result("Done. The disk image can be found in", disk_path)
|
||||
|
||||
case Action.EDIT:
|
||||
nix.edit(flake, flake_build_flags)
|
||||
|
||||
case Action.DRY_RUN:
|
||||
assert False, "DRY_RUN should be a DRY_BUILD alias"
|
||||
raise AssertionError("DRY_RUN should be a DRY_BUILD alias")
|
||||
|
||||
case Action.LIST_GENERATIONS:
|
||||
generations = nix.list_generations(profile)
|
||||
if args.json:
|
||||
@@ -494,11 +533,13 @@ def execute(argv: list[str]) -> None:
|
||||
"current": "Current",
|
||||
}
|
||||
print(tabulate(generations, headers=headers))
|
||||
|
||||
case Action.REPL:
|
||||
if flake:
|
||||
nix.repl_flake("toplevel", flake, flake_build_flags)
|
||||
else:
|
||||
nix.repl("system", build_attr, build_flags)
|
||||
|
||||
case _:
|
||||
assert_never(action)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ from typing import Any, Callable, ClassVar, Self, TypedDict, override
|
||||
|
||||
from .process import Remote, run_wrapper
|
||||
|
||||
type ImageVariants = dict[str, str]
|
||||
|
||||
|
||||
class NRError(Exception):
|
||||
"nixos-rebuild general error."
|
||||
@@ -30,6 +32,7 @@ class Action(Enum):
|
||||
DRY_BUILD = "dry-build"
|
||||
DRY_RUN = "dry-run"
|
||||
DRY_ACTIVATE = "dry-activate"
|
||||
BUILD_IMAGE = "build-image"
|
||||
BUILD_VM = "build-vm"
|
||||
BUILD_VM_WITH_BOOTLOADER = "build-vm-with-bootloader"
|
||||
LIST_GENERATIONS = "list-generations"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from importlib.resources import files
|
||||
@@ -17,6 +19,7 @@ from .models import (
|
||||
Flake,
|
||||
Generation,
|
||||
GenerationJson,
|
||||
ImageVariants,
|
||||
NRError,
|
||||
Profile,
|
||||
Remote,
|
||||
@@ -263,6 +266,57 @@ def find_file(file: str, nix_flags: Args | None = None) -> Path | None:
|
||||
return Path(r.stdout.strip())
|
||||
|
||||
|
||||
def get_build_image_variants(
|
||||
build_attr: BuildAttr,
|
||||
instantiate_flags: Args | None = None,
|
||||
) -> ImageVariants:
|
||||
path = (
|
||||
f'"{build_attr.path.resolve()}"'
|
||||
if isinstance(build_attr.path, Path)
|
||||
else build_attr.path
|
||||
)
|
||||
r = run_wrapper(
|
||||
[
|
||||
"nix-instantiate",
|
||||
"--eval",
|
||||
"--strict",
|
||||
"--json",
|
||||
"--expr",
|
||||
textwrap.dedent(f"""
|
||||
let
|
||||
value = import {path};
|
||||
set = if builtins.isFunction value then value {{}} else value;
|
||||
in
|
||||
builtins.mapAttrs (n: v: v.passthru.filePath) set.{build_attr.to_attr("config.system.build.images")}
|
||||
"""),
|
||||
*dict_to_flags(instantiate_flags),
|
||||
],
|
||||
stdout=PIPE,
|
||||
)
|
||||
j: ImageVariants = json.loads(r.stdout.strip())
|
||||
return j
|
||||
|
||||
|
||||
def get_build_image_variants_flake(
|
||||
flake: Flake,
|
||||
eval_flags: Args | None = None,
|
||||
) -> ImageVariants:
|
||||
r = run_wrapper(
|
||||
[
|
||||
"nix",
|
||||
"eval",
|
||||
"--json",
|
||||
flake.to_attr("config.system.build.images"),
|
||||
"--apply",
|
||||
"builtins.mapAttrs (n: v: v.passthru.filePath)",
|
||||
*dict_to_flags(eval_flags),
|
||||
],
|
||||
stdout=PIPE,
|
||||
)
|
||||
j: ImageVariants = json.loads(r.stdout.strip())
|
||||
return j
|
||||
|
||||
|
||||
def get_nixpkgs_rev(nixpkgs_path: Path | None) -> str | None:
|
||||
"""Get Nixpkgs path as a Git revision.
|
||||
|
||||
|
||||
@@ -52,4 +52,4 @@ extend-select = [
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
addopts = ["--import-mode=importlib"]
|
||||
addopts = "--import-mode=importlib"
|
||||
|
||||
@@ -150,6 +150,75 @@ def test_execute_nix_boot(mock_run: Any, tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(nr.process.os.environ, {}, clear=True)
|
||||
@patch(get_qualified_name(nr.process.subprocess.run), autospec=True)
|
||||
def test_execute_nix_build_image_flake(mock_run: Any, tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "test"
|
||||
config_path.touch()
|
||||
|
||||
def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]:
|
||||
if args[0] == "nix" and "eval" in args:
|
||||
return CompletedProcess(
|
||||
[],
|
||||
0,
|
||||
"""
|
||||
{
|
||||
"azure": "nixos-image-azure-25.05.20250102.6df2492-x86_64-linux.vhd",
|
||||
"vmware": "nixos-image-vmware-25.05.20250102.6df2492-x86_64-linux.vmdk"
|
||||
}
|
||||
""",
|
||||
)
|
||||
elif args[0] == "nix":
|
||||
return CompletedProcess([], 0, str(config_path))
|
||||
else:
|
||||
return CompletedProcess([], 0)
|
||||
|
||||
mock_run.side_effect = run_side_effect
|
||||
|
||||
nr.execute(
|
||||
[
|
||||
"nixos-rebuild",
|
||||
"build-image",
|
||||
"--image-variant",
|
||||
"azure",
|
||||
"--flake",
|
||||
"/path/to/config#hostname",
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_run.call_count == 2
|
||||
mock_run.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
[
|
||||
"nix",
|
||||
"eval",
|
||||
"--json",
|
||||
"/path/to/config#nixosConfigurations.hostname.config.system.build.images",
|
||||
"--apply",
|
||||
"builtins.mapAttrs (n: v: v.passthru.filePath)",
|
||||
],
|
||||
check=True,
|
||||
stdout=PIPE,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
call(
|
||||
[
|
||||
"nix",
|
||||
"--extra-experimental-features",
|
||||
"nix-command flakes",
|
||||
"build",
|
||||
"--print-out-paths",
|
||||
"/path/to/config#nixosConfigurations.hostname.config.system.build.images.azure",
|
||||
],
|
||||
check=True,
|
||||
stdout=PIPE,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(nr.process.os.environ, {}, clear=True)
|
||||
@patch(get_qualified_name(nr.process.subprocess.run), autospec=True)
|
||||
def test_execute_nix_switch_flake(mock_run: Any, tmp_path: Path) -> None:
|
||||
|
||||
@@ -314,6 +314,103 @@ def test_edit(mock_run: Any, monkeypatch: Any, tmpdir: Any) -> None:
|
||||
mock_run.assert_called_with(["editor", default_nix], check=False)
|
||||
|
||||
|
||||
@patch(
|
||||
get_qualified_name(n.run_wrapper, n),
|
||||
autospec=True,
|
||||
return_value=CompletedProcess(
|
||||
[],
|
||||
0,
|
||||
"""
|
||||
{
|
||||
"azure": "nixos-image-azure-25.05.20250102.6df2492-x86_64-linux.vhd",
|
||||
"vmware": "nixos-image-vmware-25.05.20250102.6df2492-x86_64-linux.vmdk"
|
||||
}
|
||||
""",
|
||||
),
|
||||
)
|
||||
def test_get_build_image_variants(mock_run: Any) -> None:
|
||||
build_attr = m.BuildAttr("<nixpkgs/nixos>", None)
|
||||
assert n.get_build_image_variants(build_attr) == {
|
||||
"azure": "nixos-image-azure-25.05.20250102.6df2492-x86_64-linux.vhd",
|
||||
"vmware": "nixos-image-vmware-25.05.20250102.6df2492-x86_64-linux.vmdk",
|
||||
}
|
||||
mock_run.assert_called_with(
|
||||
[
|
||||
"nix-instantiate",
|
||||
"--eval",
|
||||
"--strict",
|
||||
"--json",
|
||||
"--expr",
|
||||
textwrap.dedent("""
|
||||
let
|
||||
value = import <nixpkgs/nixos>;
|
||||
set = if builtins.isFunction value then value {} else value;
|
||||
in
|
||||
builtins.mapAttrs (n: v: v.passthru.filePath) set.config.system.build.images
|
||||
"""),
|
||||
],
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
build_attr = m.BuildAttr(Path("/tmp"), "preAttr")
|
||||
assert n.get_build_image_variants(build_attr, {"inst_flag": True}) == {
|
||||
"azure": "nixos-image-azure-25.05.20250102.6df2492-x86_64-linux.vhd",
|
||||
"vmware": "nixos-image-vmware-25.05.20250102.6df2492-x86_64-linux.vmdk",
|
||||
}
|
||||
mock_run.assert_called_with(
|
||||
[
|
||||
"nix-instantiate",
|
||||
"--eval",
|
||||
"--strict",
|
||||
"--json",
|
||||
"--expr",
|
||||
textwrap.dedent("""
|
||||
let
|
||||
value = import "/tmp";
|
||||
set = if builtins.isFunction value then value {} else value;
|
||||
in
|
||||
builtins.mapAttrs (n: v: v.passthru.filePath) set.preAttr.config.system.build.images
|
||||
"""),
|
||||
"--inst-flag",
|
||||
],
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
|
||||
@patch(
|
||||
get_qualified_name(n.run_wrapper, n),
|
||||
autospec=True,
|
||||
return_value=CompletedProcess(
|
||||
[],
|
||||
0,
|
||||
"""
|
||||
{
|
||||
"azure": "nixos-image-azure-25.05.20250102.6df2492-x86_64-linux.vhd",
|
||||
"vmware": "nixos-image-vmware-25.05.20250102.6df2492-x86_64-linux.vmdk"
|
||||
}
|
||||
""",
|
||||
),
|
||||
)
|
||||
def test_get_build_image_variants_flake(mock_run: Any) -> None:
|
||||
flake = m.Flake(Path("flake.nix"), "myAttr")
|
||||
assert n.get_build_image_variants_flake(flake, {"eval_flag": True}) == {
|
||||
"azure": "nixos-image-azure-25.05.20250102.6df2492-x86_64-linux.vhd",
|
||||
"vmware": "nixos-image-vmware-25.05.20250102.6df2492-x86_64-linux.vmdk",
|
||||
}
|
||||
mock_run.assert_called_with(
|
||||
[
|
||||
"nix",
|
||||
"eval",
|
||||
"--json",
|
||||
"flake.nix#myAttr.config.system.build.images",
|
||||
"--apply",
|
||||
"builtins.mapAttrs (n: v: v.passthru.filePath)",
|
||||
"--eval-flag",
|
||||
],
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
|
||||
def test_get_nixpkgs_rev() -> None:
|
||||
assert n.get_nixpkgs_rev(None) is None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user