Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos-rebuild-ng: implement build-image #371142

Merged
merged 4 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
79 changes: 60 additions & 19 deletions pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
):
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from .process import Remote, run_wrapper

type ImageVariants = dict[str, str]


class NRError(Exception):
"nixos-rebuild general error."
Expand All @@ -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"
Expand Down
54 changes: 54 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,6 +19,7 @@
Flake,
Generation,
GenerationJson,
ImageVariants,
NRError,
Profile,
Remote,
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion pkgs/by-name/ni/nixos-rebuild-ng/src/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ extend-select = [

[tool.pytest.ini_options]
pythonpath = ["."]
addopts = ["--import-mode=importlib"]
addopts = "--import-mode=importlib"
69 changes: 69 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,75 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]:
)


@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:
Expand Down
Loading