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: init #354029

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
154 changes: 154 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/nixos_rebuild/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import argparse
import sys
from pathlib import Path
from textwrap import dedent
from typing import assert_never

from .models import Action, Flake
from .nix import (
edit,
nix_build,
nix_flake_build,
nix_set_profile,
nix_switch_to_configuration,
)
from .process import run_exec
from .utils import info


def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="nixos-rebuild",
description="Reconfigure a NixOS machine",
add_help=False,
)
parser.add_argument("--help", action="store_true")
parser.add_argument("--file", "-f")
parser.add_argument("--attr", "-A")
parser.add_argument("--flake", nargs="?", const=True)
parser.add_argument("--no-flake", dest="flake", action="store_false")
parser.add_argument("--install-bootloader", action="store_true")
# TODO: add deprecated=True in Python >=3.13
parser.add_argument("--install-grub", action="store_true")
parser.add_argument("--profile-name", default="system")
parser.add_argument("action", choices=Action.values(), nargs="?")
r = parser.parse_args(argv[1:])

# https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/linux/nixos-rebuild/nixos-rebuild.sh#L56
if r.action == Action.DRY_RUN.value:
r.action = Action.DRY_BUILD.value

if r.install_grub:
info(f"{argv[0]}: --install-grub deprecated, use --install-bootloader instead")
r.install_bootloader = True

if r.flake and (r.file or r.attr):
sys.exit("error: '--flake' cannot be used with '--file' or '--attr'")

return r


def run(argv: list[str]) -> None:
args = parse_args(argv)
if args.help or args.action is None:
run_exec(["man", "8", "nixos-rebuild"])

if args.profile_name == "system":
profile = Path("/nix/var/nix/profiles/system")
else:
profile = Path("/nix/var/nix/profiles/system-profiles") / args.profile_name
profile.parent.mkdir(mode=0o755, parents=True, exist_ok=True)

flake = Flake.from_arg(args.flake)

match action := Action(args.action):
case (
Action.SWITCH
| Action.BOOT
| Action.TEST
| Action.BUILD
| Action.DRY_BUILD
| Action.DRY_ACTIVATE
):
set_profile = action in (Action.SWITCH, Action.BOOT)
switch_to_configuration = action in (
Action.SWITCH,
Action.BOOT,
Action.TEST,
Action.DRY_ACTIVATE,
)
no_link = action in (Action.SWITCH, Action.BOOT)
keep_going = action in (
Action.TEST,
Action.BUILD,
Action.DRY_BUILD,
Action.DRY_ACTIVATE,
)
dry_run = action == Action.DRY_BUILD
info("building the system configuration...")
if flake:
path_to_config = nix_flake_build(
"config.system.build.toplevel",
flake,
no_link=no_link,
keep_going=keep_going,
dry_run=dry_run,
)
else:
path_to_config = nix_build(
"system",
args.attr,
args.file,
no_out_link=no_link,
keep_going=keep_going,
dry_run=dry_run,
)
if set_profile:
nix_set_profile(profile, path_to_config)
if switch_to_configuration:
nix_switch_to_configuration(
path_to_config,
action,
install_bootloader=args.install_bootloader,
)
case Action.BUILD_VM | Action.BUILD_VM_WITH_BOOTLOADER:
info("building the system configuration...")
attr = "vm" if action == Action.BUILD_VM else "vmWithBootLoader"
if flake:
path_to_config = nix_flake_build(
f"config.system.build.{attr}",
flake,
)
else:
path_to_config = nix_build(
attr,
args.attr,
args.file,
keep_going=True,
)
print(
dedent(f"""
Done. The virtual machine can be started by running {next(path_to_config.glob("bin/run-*-vm"))}
""")
)
case Action.EDIT:
if args.file or args.attr:
sys.exit("error: '--file' and '--attr' are not supported with 'edit'")
edit(flake)
case Action.DRY_RUN:
assert False, "DRY_RUN should be a DRY_BUILD alias"
case Action.REPL | Action.LIST_GENERATIONS:
raise NotImplementedError(action)
case _:
assert_never(action)


def main() -> None:
try:
run(sys.argv)
except KeyboardInterrupt:
sys.exit(130)


if __name__ == "__main__":
main()
71 changes: 71 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/nixos_rebuild/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

import platform
import re
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any


class Action(Enum):
SWITCH = "switch"
BOOT = "boot"
TEST = "test"
BUILD = "build"
EDIT = "edit"
REPL = "repl"
DRY_BUILD = "dry-build"
DRY_RUN = "dry-run"
DRY_ACTIVATE = "dry-activate"
BUILD_VM = "build-vm"
BUILD_VM_WITH_BOOTLOADER = "build-vm-with-bootloader"
LIST_GENERATIONS = "list-generations"

def __str__(self) -> str:
return self.value

@staticmethod
def values() -> list[str]:
return [a.value for a in Action]


@dataclass
class Flake:
path: Path
attr: str

def __str__(self) -> str:
return f"{self.path}#{self.attr}"

@staticmethod
def parse(flake_str: str) -> Flake:
m = re.match(r"^(?P<path>[^\#]*)\#?(?P<attr>[^\#\"]*)$", flake_str)
assert m is not None, "match is None"
attr = m.group("attr")
if not attr:
hostname = platform.node() or "default"
attr = f"nixosConfigurations.{hostname}"
else:
attr = f"nixosConfigurations.{attr}"
return Flake(Path(m.group("path")), attr)

@staticmethod
def from_arg(flake_arg: Any) -> Flake | None:
match flake_arg:
case str(s):
return Flake.parse(s)
case True:
return Flake.parse(".")
case False:
return None
case _:
# Use /etc/nixos/flake.nix if it exists.
default_path = Path("/etc/nixos/flake.nix")
if default_path.exists():
# It can be a symlink to the actual flake.
if default_path.is_symlink():
default_path = default_path.readlink()
return Flake.parse(str(default_path.parent))
else:
return None
78 changes: 78 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/nixos_rebuild/nix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os
import sys
from pathlib import Path
from typing import Final

from .models import Action, Flake
from .process import run_capture, run_cmd, run_exec
from .utils import kwargs_to_flags

FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"]


def edit(flake: Flake | None) -> None:
if flake:
# TODO: lockFlags
run_exec(["nix", *FLAKE_FLAGS, "edit", "--", str(flake)])
else:
nixos_config = Path(
os.environ.get("NIXOS_CONFIG")
or run_capture(["nix-instantiate", "--find-file", "nixos-config"])
or "/etc/nixos/default.nix"
)
if nixos_config.is_dir():
nixos_config /= "default.nix"

if nixos_config.exists():
run_exec([os.environ.get("EDITOR", "nano"), str(nixos_config)])
else:
sys.exit("warning: cannot find NixOS config file")


def nix_build(
attr: str,
pre_attr: str | None,
file: str | None,
**kwargs: bool | str,
) -> Path:
if pre_attr or file:
run_args = [
"nix-build",
file or "default.nix",
"--attr",
f"{'.'.join([x for x in [pre_attr, attr] if x])}",
]
else:
run_args = ["nix-build", "<nixpkgs/nixos>", "--attr", attr]
run_args = kwargs_to_flags(run_args, **kwargs)
return Path(run_capture(run_args).strip())


def nix_flake_build(attr: str, flake: Flake, **kwargs: bool | str) -> Path:
run_args = [
"nix",
*FLAKE_FLAGS,
"build",
"--print-out-paths",
f"{flake}.{attr}",
]
run_args = kwargs_to_flags(run_args, **kwargs)
return Path(run_capture(run_args).strip())


def nix_set_profile(profile: Path, path_to_config: Path) -> None:
run_cmd(["nix-env", "-p", str(profile), "--set", str(path_to_config)])


def nix_switch_to_configuration(
path_to_config: Path,
action: Action,
install_bootloader: bool = False,
) -> None:
run_cmd(
[str(path_to_config / "bin/switch-to-configuration"), str(action)],
env={
"NIXOS_INSTALL_BOOTLOADER": "1" if install_bootloader else "0",
"LOCALE_ARCHIVE": os.environ.get("LOCALE_ARCHIVE", ""),
},
)
30 changes: 30 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/nixos_rebuild/process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import subprocess
import sys
from typing import Any


def run_cmd(
args: list[str],
check: bool = True,
**kwargs: Any,
) -> subprocess.CompletedProcess[str]:
r = subprocess.run(args, text=True, **kwargs)

if check:
try:
r.check_returncode()
except subprocess.CalledProcessError as ex:
sys.exit(str(ex))

return r


def run_capture(args: list[str]) -> str:
r = run_cmd(args, stdout=subprocess.PIPE)
return r.stdout


def run_exec(args: list[str]) -> None:
# We will exit anyway, so ignore the check here
r = run_cmd(args, check=False)
sys.exit(r.returncode)
16 changes: 16 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/nixos_rebuild/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import sys
from functools import partial

info = partial(print, file=sys.stderr)


def kwargs_to_flags(flags: list[str], **kwargs: bool | str) -> list[str]:
for k, v in kwargs.items():
f = f"--{'-'.join(k.split('_'))}"
match v:
case True:
flags.append(f)
case str():
flags.append(f)
flags.append(v)
return flags
Loading
Loading