nixos-rebuild-ng: explicitly parse Nix flags

This commit is contained in:
Thiago Kenji Okada 2024-11-17 13:01:22 +00:00
parent 3e2f5ef1a6
commit 3b41ec0691
7 changed files with 155 additions and 53 deletions

View File

@ -119,7 +119,7 @@ ruff format .
## TODO
- [ ] Remote host/builders (via SSH)
- [ ] Improve nix arguments handling (e.g.: `nixFlags` vs `copyFlags` in the
- [x] Improve nix arguments handling (e.g.: `nixFlags` vs `copyFlags` in the
old `nixos-rebuild`)
- [ ] `_NIXOS_REBUILD_EXEC`
- [ ] Port `nixos-rebuild.passthru.tests`

View File

@ -21,19 +21,19 @@ from .nix import (
switch_to_configuration,
upgrade_channels,
)
from .utils import info
from .utils import flags_to_dict, info
VERBOSE = False
VERBOSE = 0
def parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="nixos-rebuild",
description="Reconfigure a NixOS machine",
add_help=False,
allow_abbrev=False,
)
parser.add_argument("--help", action="store_true")
parser.add_argument("--help", "-h", action="store_true")
parser.add_argument("--file", "-f")
parser.add_argument("--attr", "-A")
parser.add_argument("--flake", nargs="?", const=True)
@ -48,13 +48,44 @@ def parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
parser.add_argument("--upgrade-all", action="store_true")
parser.add_argument("--json", action="store_true")
parser.add_argument("action", choices=Action.values(), nargs="?")
parser.add_argument("--verbose", "-v", action="count", default=0)
args, remainder = parser.parse_known_args(argv[1:])
common_group = parser.add_argument_group("Common flags")
common_group.add_argument("--include", "-I")
common_group.add_argument("--max-jobs", "-j")
common_group.add_argument("--cores")
common_group.add_argument("--log-format")
common_group.add_argument("--quiet", action="store_true")
common_group.add_argument("--print-build-logs", "-L", action="store_true")
common_group.add_argument("--show-trace", action="store_true")
common_group.add_argument("--keep-going", "-k", action="store_true")
common_group.add_argument("--keep-failed", "-K", action="store_true")
common_group.add_argument("--fallback", action="store_true")
common_group.add_argument("--repair", action="store_true")
common_group.add_argument("--option", nargs=2)
nix_group = parser.add_argument_group("Classic Nix flags")
nix_group.add_argument("--no-build-output", "-Q", action="store_true")
flake_group = parser.add_argument_group("Flake flags")
flake_group.add_argument("--accept-flake-config", action="store_true")
flake_group.add_argument("--refresh", action="store_true")
flake_group.add_argument("--impure", action="store_true")
flake_group.add_argument("--offline", action="store_true")
flake_group.add_argument("--no-net", action="store_true")
flake_group.add_argument("--recreate-lock-file", action="store_true")
flake_group.add_argument("--no-update-lock-file", action="store_true")
flake_group.add_argument("--no-write-lock-file", action="store_true")
flake_group.add_argument("--no-registries", action="store_true")
flake_group.add_argument("--commit-lock-file", action="store_true")
flake_group.add_argument("--update-input")
flake_group.add_argument("--override-input", nargs=2)
args = parser.parse_args(argv[1:])
global VERBOSE
# Manually parse verbose flag since this is a nix flag that also affect
# the script
VERBOSE = any(v == "--verbose" or v.startswith("-v") for v in remainder)
# This flag affects both nix and this script
VERBOSE = args.verbose
# https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/linux/nixos-rebuild/nixos-rebuild.sh#L56
if args.action == Action.DRY_RUN.value:
@ -76,11 +107,48 @@ def parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
r = run(["man", "8", "nixos-rebuild"], check=False)
parser.exit(r.returncode)
return args, remainder
return args
def execute(argv: list[str]) -> None:
args, nix_flags = parse_args(argv)
args = parse_args(argv)
common_flags = flags_to_dict(
args,
[
"verbose",
"include",
"max_jobs",
"cores",
"log_format",
"quiet",
"print_build_logs",
"show_trace",
"keep_going",
"keep_failed",
"fallback",
"repair",
"option",
],
)
nix_flags = common_flags | flags_to_dict(args, ["no_build_output"])
flake_flags = common_flags | flags_to_dict(
args,
[
"accept_flake_config",
"refresh",
"impure",
"offline",
"no_net",
"recreate_lock_file",
"no_update_lock_file",
"no_write_lock_file",
"no_registries",
"commit_lock_file",
"update_input",
"override_input",
],
)
profile = Profile.from_name(args.profile_name)
flake = Flake.from_arg(args.flake)
@ -96,7 +164,7 @@ def execute(argv: list[str]) -> None:
# untrusted tree.
can_run = action in (Action.SWITCH, Action.BOOT, Action.TEST)
if can_run and not flake:
nixpkgs_path = find_file("nixpkgs", nix_flags)
nixpkgs_path = find_file("nixpkgs", **nix_flags)
rev = get_nixpkgs_rev(nixpkgs_path)
if nixpkgs_path and rev:
(nixpkgs_path / ".version-suffix").write_text(rev)
@ -110,8 +178,8 @@ def execute(argv: list[str]) -> None:
path_to_config = nixos_build_flake(
"toplevel",
flake,
nix_flags,
no_link=True,
**flake_flags,
)
set_profile(profile, path_to_config)
else:
@ -119,8 +187,8 @@ def execute(argv: list[str]) -> None:
"system",
args.attr,
args.file,
nix_flags,
no_out_link=True,
**nix_flags,
)
set_profile(profile, path_to_config)
switch_to_configuration(
@ -142,18 +210,18 @@ def execute(argv: list[str]) -> None:
path_to_config = nixos_build_flake(
"toplevel",
flake,
nix_flags,
keep_going=True,
dry_run=dry_run,
**flake_flags,
)
else:
path_to_config = nixos_build(
"system",
args.attr,
args.file,
nix_flags,
keep_going=True,
dry_run=dry_run,
**nix_flags,
)
if action in (Action.TEST, Action.DRY_ACTIVATE):
switch_to_configuration(
@ -168,21 +236,21 @@ def execute(argv: list[str]) -> None:
path_to_config = nixos_build_flake(
attr,
flake,
nix_flags,
keep_going=True,
**flake_flags,
)
else:
path_to_config = nixos_build(
attr,
args.attr,
args.file,
nix_flags,
keep_going=True,
**nix_flags,
)
vm_path = next(path_to_config.glob("bin/run-*-vm"), "./result/bin/run-*-vm")
print(f"Done. The virtual machine can be started by running '{vm_path}'")
case Action.EDIT:
edit(flake, nix_flags)
edit(flake, **flake_flags)
case Action.DRY_RUN:
assert False, "DRY_RUN should be a DRY_BUILD alias"
case Action.LIST_GENERATIONS:

View File

@ -15,20 +15,27 @@ from .models import (
NRError,
Profile,
)
from .utils import dict_to_flags, info
from .utils import Args, dict_to_flags, info
FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"]
def edit(flake: Flake | None, nix_flags: list[str] | None = None) -> None:
def edit(flake: Flake | None, **flake_flags: Args) -> None:
"Try to find and open NixOS configuration file in editor."
if flake:
run(
["nix", *FLAKE_FLAGS, "edit", *(nix_flags or []), "--", str(flake)],
[
"nix",
*FLAKE_FLAGS,
"edit",
*(dict_to_flags(flake_flags)),
"--",
str(flake),
],
check=False,
)
else:
if nix_flags:
if flake_flags:
raise NRError("'edit' does not support extra Nix flags")
nixos_config = Path(
os.getenv("NIXOS_CONFIG")
@ -49,10 +56,10 @@ def edit(flake: Flake | None, nix_flags: list[str] | None = None) -> None:
raise NRError("cannot find NixOS config file")
def find_file(file: str, nix_flags: list[str] | None = None) -> Path | None:
def find_file(file: str, **nix_flags: Args) -> Path | None:
"Find classic Nixpkgs location."
r = run(
["nix-instantiate", "--find-file", file, *(nix_flags or [])],
["nix-instantiate", "--find-file", file, *dict_to_flags(nix_flags)],
stdout=PIPE,
check=False,
text=True,
@ -207,8 +214,7 @@ def nixos_build(
attr: str,
pre_attr: str | None,
file: str | None,
nix_flags: list[str] | None = None,
**kwargs: bool | str,
**nix_flags: Args,
) -> Path:
"""Build NixOS attribute using classic Nix.
@ -227,7 +233,7 @@ def nixos_build(
]
else:
run_args = ["nix-build", "<nixpkgs/nixos>", "--attr", attr]
run_args += dict_to_flags(kwargs) + (nix_flags or [])
run_args += dict_to_flags(nix_flags)
r = run(run_args, check=True, text=True, stdout=PIPE)
return Path(r.stdout.strip())
@ -235,8 +241,7 @@ def nixos_build(
def nixos_build_flake(
attr: str,
flake: Flake,
nix_flags: list[str] | None = None,
**kwargs: bool | str,
**flake_flags: Args,
) -> Path:
"""Build NixOS attribute using Flakes.
@ -249,7 +254,7 @@ def nixos_build_flake(
"--print-out-paths",
f"{flake}.config.system.build.{attr}",
]
run_args += dict_to_flags(kwargs) + (nix_flags or [])
run_args += dict_to_flags(flake_flags)
r = run(run_args, check=True, text=True, stdout=PIPE)
return Path(r.stdout.strip())

View File

@ -1,18 +1,20 @@
from __future__ import annotations
import argparse
import sys
from functools import partial
from typing import Any
from typing import TypeAlias
info = partial(print, file=sys.stderr)
Args: TypeAlias = bool | str | list[str] | int | None
def dict_to_flags(d: dict[str, Any]) -> list[str]:
def dict_to_flags(d: dict[str, Args]) -> list[str]:
flags = []
for key, value in d.items():
flag = f"--{'-'.join(key.split('_'))}"
match value:
case None | False:
case None | False | 0 | []:
pass
case True:
flags.append(flag)
@ -26,3 +28,8 @@ def dict_to_flags(d: dict[str, Any]) -> list[str]:
for v in value:
flags.append(v)
return flags
def flags_to_dict(args: argparse.Namespace, keys: list[str]) -> dict[str, Args]:
d = vars(args)
return {k: d[k] for k in keys}

View File

@ -29,25 +29,27 @@ def test_parse_args() -> None:
nr.parse_args(["nixos-rebuild", "edit", "--attr", "attr"])
assert e.value.code == 2
r1, remainder = nr.parse_args(
r1 = nr.parse_args(
[
"nixos-rebuild",
"switch",
"--install-grub",
"--flake",
"/etc/nixos",
"--extra",
"flag",
"--option",
"foo",
"bar",
]
)
assert remainder == ["--extra", "flag"]
assert nr.VERBOSE == 0
assert r1.flake == "/etc/nixos"
assert r1.install_bootloader is True
assert r1.install_grub is True
assert r1.profile_name == "system"
assert r1.action == "switch"
assert r1.option == ["foo", "bar"]
r2, remainder = nr.parse_args(
r2 = nr.parse_args(
[
"nixos-rebuild",
"dry-run",
@ -57,9 +59,11 @@ def test_parse_args() -> None:
"foo",
"--attr",
"bar",
"-vvv",
]
)
assert remainder == []
assert nr.VERBOSE == 3
assert r2.verbose == 3
assert r2.flake is False
assert r2.action == "dry-build"
assert r2.file == "foo"
@ -88,7 +92,6 @@ def test_execute_nix_boot(mock_which: Any, mock_run: Any, tmp_path: Path) -> Non
nr.execute(["nixos-rebuild", "boot", "--no-flake", "-vvv"])
assert nr.VERBOSE is True
assert mock_run.call_count == 6
mock_run.assert_has_calls(
[
@ -164,7 +167,6 @@ def test_execute_nix_switch_flake(mock_run: Any, tmp_path: Path) -> None:
]
)
assert nr.VERBOSE is True
assert mock_run.call_count == 3
mock_run.assert_has_calls(
[
@ -177,7 +179,7 @@ def test_execute_nix_switch_flake(mock_run: Any, tmp_path: Path) -> None:
"--print-out-paths",
"/path/to/config#nixosConfigurations.hostname.config.system.build.toplevel",
"--no-link",
"--verbose",
"-v",
],
check=True,
text=True,
@ -209,8 +211,7 @@ def test_execute_switch_rollback(mock_run: Any, tmp_path: Path) -> None:
nr.execute(["nixos-rebuild", "switch", "--rollback", "--install-bootloader"])
assert nr.VERBOSE is False
assert mock_run.call_count == 3
assert mock_run.call_count >= 2
# ignoring update_nixpkgs_rev calls
mock_run.assert_has_calls(
[
@ -268,7 +269,6 @@ def test_execute_test_rollback(
]
)
assert nr.VERBOSE is False
assert mock_run.call_count == 2
mock_run.assert_has_calls(
[

View File

@ -16,7 +16,7 @@ from .helpers import get_qualified_name
def test_edit(mock_run: Any, monkeypatch: Any, tmpdir: Any) -> None:
# Flake
flake = m.Flake.parse(".#attr")
n.edit(flake, ["--commit-lock-file"])
n.edit(flake, commit_lock_file=True)
mock_run.assert_called_with(
[
"nix",
@ -193,8 +193,8 @@ def test_nixos_build_flake(mock_run: Any) -> None:
assert n.nixos_build_flake(
"toplevel",
flake,
["--nix-flag", "foo"],
no_link=True,
nix_flag="foo",
) == Path("/path/to/file")
mock_run.assert_called_with(
[
@ -220,9 +220,7 @@ def test_nixos_build_flake(mock_run: Any) -> None:
return_value=CompletedProcess([], 0, stdout=" \n/path/to/file\n "),
)
def test_nixos_build(mock_run: Any, monkeypatch: Any) -> None:
assert n.nixos_build("attr", None, None, ["--nix-flag", "foo"]) == Path(
"/path/to/file"
)
assert n.nixos_build("attr", None, None, nix_flag="foo") == Path("/path/to/file")
mock_run.assert_called_with(
["nix-build", "<nixpkgs/nixos>", "--attr", "attr", "--nix-flag", "foo"],
check=True,

View File

@ -1,8 +1,10 @@
import argparse
from nixos_rebuild import utils as u
def test_dict_to_flags() -> None:
r = u.dict_to_flags(
r1 = u.dict_to_flags(
{
"test_flag_1": True,
"test_flag_2": False,
@ -12,7 +14,7 @@ def test_dict_to_flags() -> None:
"verbose": 5,
}
)
assert r == [
assert r1 == [
"--test-flag-1",
"--test-flag-3",
"value",
@ -21,3 +23,25 @@ def test_dict_to_flags() -> None:
"v2",
"-vvvvv",
]
r2 = u.dict_to_flags({"verbose": 0, "empty_list": []})
assert r2 == []
def test_flags_to_dict() -> None:
r = u.flags_to_dict(
argparse.Namespace(
test_flag_1=True,
test_flag_2=False,
test_flag_3="value",
test_flag_4=["v1", "v2"],
test_flag_5=None,
verbose=5,
),
["test_flag_1", "test_flag_3", "test_flag_4", "verbose"],
)
assert r == {
"test_flag_1": True,
"test_flag_3": "value",
"test_flag_4": ["v1", "v2"],
"verbose": 5,
}