Skip to content

Commit

Permalink
nixos-rebuild-ng: init
Browse files Browse the repository at this point in the history
  • Loading branch information
thiagokokada committed Nov 6, 2024
1 parent d25ccf9 commit 5ff4130
Show file tree
Hide file tree
Showing 12 changed files with 637 additions and 0 deletions.
152 changes: 152 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,152 @@
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, 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

0 comments on commit 5ff4130

Please sign in to comment.