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 5d7ce95
Show file tree
Hide file tree
Showing 9 changed files with 558 additions and 0 deletions.
175 changes: 175 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,175 @@
import argparse
import os
import sys
from pathlib import Path
from textwrap import dedent
from typing import Final

from .log import info
from .models import Action, Flake
from .process import run_capture, run_cmd, run_exec

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,
no_out_link: bool = True,
keep_going: bool = False,
) -> 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]
# TODO: kwargs magic?
if no_out_link:
run_args.append("--no-out-link")
if keep_going:
run_args.append("--keep-going")
return Path(run_capture(run_args).strip())


def nix_flake_build(attr: str, flake: Flake, no_link: bool = True) -> Path:
run_args = [
"nix",
*FLAKE_FLAGS,
"build",
"--print-out-paths",
f"{flake}.{attr}",
]
# TODO: kwargs magic?
if no_link:
run_args.append("--no-link")

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", ""),
},
)


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:])

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

return r


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

profile = Path("/nix/var/nix/profiles/system")
if args.profile_name != "system":
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)
if flake and (args.file or args.attr):
sys.exit("error: '--flake' cannot be used with '--file' or '--attr'")

match action := Action(args.action):
case Action.SWITCH | Action.BOOT:
info("building the system configuration...")
if flake:
path_to_config = nix_flake_build("config.system.build.toplevel", flake)
else:
path_to_config = nix_build("system", args.attr, args.file)
nix_set_profile(profile, path_to_config)
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,
no_link=False,
)
else:
path_to_config = nix_build(
attr,
args.attr,
args.file,
no_out_link=False,
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 _:
# TODO: replace with Typing.assert_never once all Actions are
# implemented
raise NotImplementedError(str(action))


if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit(130)
4 changes: 4 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/nixos_rebuild/log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import sys
from functools import partial

info = partial(print, file=sys.stderr)
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
38 changes: 38 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,38 @@
import subprocess
import sys
from typing import Any

from .log import info


# Similar to `subprocess.run`, but handles Ctrl+C gracefully
def run_cmd(
args: list[str],
check: bool = True,
**kwargs: Any,
) -> subprocess.Popen[Any]:
proc = subprocess.Popen(args, **kwargs)
try:
proc.wait()
except KeyboardInterrupt:
proc.terminate()
proc.wait()
sys.exit(proc.returncode or 130)

if check and proc.returncode:
info(f"warning: error(s) occured while running command:\n$ {' '.join(args)}")
sys.exit(proc.returncode)

return proc


def run_capture(args: list[str]) -> str:
r = run_cmd(args, text=True, stdout=subprocess.PIPE)
assert r.stdout is not None, "stdout is None"
return str(r.stdout.read())


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)
60 changes: 60 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
lib,
installShellFiles,
nix,
nixos-rebuild,
python3Packages,
}:
let
fallback = import ./../../../../nixos/modules/installer/tools/nix-fallback-paths.nix;
fs = lib.fileset;
in
python3Packages.buildPythonApplication {
pname = "nixos-rebuild-ng";
version = "0.1";
src = fs.toSource {
root = ./.;
fileset = fs.unions [
./nixos_rebuild
./pyproject.toml
./tests
];
};
pyproject = true;

nativeBuildInputs = [
installShellFiles
python3Packages.setuptools
];

postInstall = ''
installManPage ${nixos-rebuild}/share/man/man8/nixos-rebuild.8
installShellCompletion \
--bash ${nixos-rebuild}/share/bash-completion/completions/_nixos-rebuild
'';

doCheck = true;
nativeCheckInputs = with python3Packages; [
pytestCheckHook
mypy
ruff
black
];
postCheck = ''
echo -e "\x1b[32m## run mypy\x1b[0m"
mypy --strict nixos_rebuild
echo -e "\x1b[32m## run ruff\x1b[0m"
ruff check .
echo -e "\x1b[32m## run ruff format\x1b[0m"
ruff format --check .
'';

meta = {
description = "Rebuild your NixOS configuration and switch to it, on local hosts and remote";
homepage = "https://github.com/NixOS/nixpkgs/tree/master/pkgs/os-specific/linux/nixos-rebuild";
license = lib.licenses.mit;
maintainers = [ lib.maintainers.thiagokokada ];
mainProgram = "nixos-rebuild";
};
}
10 changes: 10 additions & 0 deletions pkgs/by-name/ni/nixos-rebuild-ng/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "nixos-rebuild-ng"
version = "0.0.0"

[project.scripts]
nixos-rebuild = "nixos_rebuild:main"
Loading

0 comments on commit 5d7ce95

Please sign in to comment.