Unverified Commit e0d9824a authored by Thiago Kenji Okada's avatar Thiago Kenji Okada Committed by GitHub
Browse files

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

parents a077d7e5 eccb8552
Loading
Loading
Loading
Loading
+14 −1
Original line number Diff line number Diff line
@@ -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
+60 −19
Original line number Diff line number Diff line
@@ -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,7 +487,13 @@ def execute(argv: list[str]) -> None:
                        sudo=args.sudo,
                    )

            if action in (Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE):
            # 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,
@@ -469,16 +502,22 @@ def execute(argv: list[str]) -> None:
                        specialisation=args.specialisation,
                        install_bootloader=args.install_bootloader,
                    )
            elif action in (Action.BUILD_VM, Action.BUILD_VM_WITH_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(
                    f"Done. The virtual machine can be started by running '{vm_path}'"
                    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)

+3 −0
Original line number Diff line number Diff line
@@ -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"
+54 −0
Original line number Diff line number Diff line
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.

+1 −1
Original line number Diff line number Diff line
@@ -52,4 +52,4 @@ extend-select = [

[tool.pytest.ini_options]
pythonpath = ["."]
addopts = ["--import-mode=importlib"]
addopts = "--import-mode=importlib"
Loading