mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-21 22:43:01 +00:00
nixos-rebuild-ng: init
This commit is contained in:
parent
3d8f220f71
commit
9e6ece3ee1
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
@ -293,6 +293,7 @@
|
||||
- any-glob-to-any-file:
|
||||
- nixos/**/*
|
||||
- pkgs/by-name/sw/switch-to-configuration-ng/**/*
|
||||
- pkgs/by-name/ni/nixos-rebuild-ng/**/*
|
||||
- pkgs/os-specific/linux/nixos-rebuild/**/*
|
||||
|
||||
"6.topic: nixos-container":
|
||||
|
@ -143,6 +143,8 @@ nixos/modules/installer/tools/nix-fallback-paths.nix @NixOS/nix-team @raitobeza
|
||||
/nixos/tests/amazon-ssm-agent.nix @arianvp
|
||||
/nixos/modules/system/boot/grow-partition.nix @arianvp
|
||||
|
||||
# nixos-rebuild-ng
|
||||
/pkgs/by-name/ni/nixos-rebuild-ng @thiagokokada
|
||||
|
||||
|
||||
# Updaters
|
||||
|
99
pkgs/by-name/ni/nixos-rebuild-ng/README.md
Normal file
99
pkgs/by-name/ni/nixos-rebuild-ng/README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# nixos-rebuild-ng
|
||||
|
||||
Work-in-Progress rewrite of
|
||||
[`nixos-rebuild`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/linux/nixos-rebuild/nixos-rebuild.sh).
|
||||
|
||||
## Why the rewrite?
|
||||
|
||||
The current state of `nixos-rebuild` is dare: it is one of the most critical
|
||||
piece of code we have in NixOS, but it has tons of issues:
|
||||
- The code is written in Bash, and while this by itself is not necessary bad,
|
||||
it means that it is difficult to do refactorings due to the lack of tooling
|
||||
for the language
|
||||
- The code itself is a hacky mess. Changing even one line of code can cause
|
||||
issues that affects dozens of people
|
||||
- Lack of proper testing (we do have some integration tests, but no unit tests
|
||||
and coverage is probably pitiful)
|
||||
- The code predates some of the improvements `nix` had over the years, e.g.: it
|
||||
builds Flakes inside a temporary directory and read the resulting symlink
|
||||
since the code seems to predate `--print-out-paths` flag
|
||||
|
||||
Given all of those above, improvements in the `nixos-rebuild` are difficult to
|
||||
do. A full rewrite is probably the easier way to improve the situation since
|
||||
this can be done in a separate package that will not break anyone. So this is
|
||||
an attempt of the rewrite.
|
||||
|
||||
## Why Python?
|
||||
|
||||
- It is the language of choice for many critical things inside `nixpkgs`, like
|
||||
the `NixOSTest` and `systemd-boot-builder.py` activation scripts
|
||||
- It is a language with great tooling, e.g.: `mypy` for type checking, `ruff`
|
||||
for linting, `pytest` for unit testing
|
||||
- It is a scripting language that fits well with the scope of this project
|
||||
- Python's standard library is great and it means we will need a low number of
|
||||
external dependencies for this project. For example, `nixos-rebuild`
|
||||
currently depends in `jq` for JSON parsing, while Python has `json` in
|
||||
standard library
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
- Do: be as much of a drop-in replacement as possible
|
||||
- Do: fix obvious bugs
|
||||
- Do: improvements that are non-breaking
|
||||
- Don't: change logic in breaking ways even if this would be an improvement
|
||||
|
||||
## How to use
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [ pkgs.nixos-rebuild-ng ];
|
||||
}
|
||||
```
|
||||
|
||||
And use `nixos-rebuild-ng` instead of `nixos-rebuild`.
|
||||
|
||||
## Current caveats
|
||||
|
||||
- For now we will install it in `nixos-rebuild-ng` path by default, to avoid
|
||||
conflicting with the current `nixos-rebuild`. This means you can keep both in
|
||||
your system at the same time, but it also means that a few things like bash
|
||||
completion are broken right now (since it looks at `nixos-rebuild` binary)
|
||||
- `_NIXOS_REBUILD_EXEC` is **not** implemented yet, so different from
|
||||
`nixos-rebuild`, this will use the current version of `nixos-rebuild-ng` in
|
||||
your `PATH` to build/set profile/switch, while `nixos-rebuild` builds the new
|
||||
version (the one that will be switched) and re-exec to it instead. This means
|
||||
that in case of bugs in `nixos-rebuild-ng`, the only way that you will get
|
||||
them fixed is **after** you switch to a new version
|
||||
- `nix` bootstrap is also **not** implemented yet, so this means that you will
|
||||
eval with an old version of Nix instead of a newer one. This is unlikely to
|
||||
cause issues, because the build will happen in the daemon anyway (that is
|
||||
only changed after the switch), and unless you are using bleeding edge `nix`
|
||||
features you will probably have zero problems here. You can basically think
|
||||
that using `nixos-rebuild-ng` is similar to running `nixos-rebuild --fast`
|
||||
right now
|
||||
- Ignore any performance advantages of the rewrite right now, because of the 2
|
||||
caveats above
|
||||
- `--target-host` and `--build-host` are not implemented yet and this is
|
||||
probably the thing that will be most difficult to implement. Help here is
|
||||
welcome
|
||||
- Bugs in the profile manipulation can cause corruption of your profile that
|
||||
may be difficult to fix, so right now I only recommend using
|
||||
`nixos-rebuild-ng` if you are testing in a VM or in a filesystem with
|
||||
snapshots like btrfs or ZFS. Those bugs are unlikely to be unfixable but the
|
||||
errors can be difficult to understand. If you want to go anyway,
|
||||
`nix-collect-garbage -d` and `nix store repair` are your friends
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Remote host/builders (via SSH)
|
||||
- [ ] Improve nix arguments handling (e.g.: `nixFlags` vs `copyFlags` in the
|
||||
old `nixos-rebuild`)
|
||||
- [ ] `_NIXOS_REBUILD_EXEC`
|
||||
- [ ] Port `nixos-rebuild.passthru.tests`
|
||||
- [ ] Change module system to allow easier opt-in, like
|
||||
`system.switch.enableNg` for `switch-to-configuration-ng`
|
||||
- [ ] Improve documentation
|
||||
- [ ] `nixos-rebuild repl` (calling old `nixos-rebuild` for now)
|
||||
- [ ] `nix` build/bootstrap
|
||||
- [ ] Reduce build closure
|
80
pkgs/by-name/ni/nixos-rebuild-ng/package.nix
Normal file
80
pkgs/by-name/ni/nixos-rebuild-ng/package.nix
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
lib,
|
||||
installShellFiles,
|
||||
nix,
|
||||
nixos-rebuild,
|
||||
python3,
|
||||
withNgSuffix ? true,
|
||||
}:
|
||||
python3.pkgs.buildPythonApplication {
|
||||
pname = "nixos-rebuild-ng";
|
||||
version = "0.0.0";
|
||||
src = ./src;
|
||||
pyproject = true;
|
||||
|
||||
build-system = with python3.pkgs; [
|
||||
setuptools
|
||||
];
|
||||
|
||||
dependencies = with python3.pkgs; [
|
||||
tabulate
|
||||
types-tabulate
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
installShellFiles
|
||||
];
|
||||
|
||||
propagatedBuildInputs = [
|
||||
# Make sure that we use the Nix package we depend on, not something
|
||||
# else from the PATH for nix-{env,instantiate,build}. This is
|
||||
# important, because NixOS defaults the architecture of the rebuilt
|
||||
# system to the architecture of the nix-* binaries used. So if on an
|
||||
# amd64 system the user has an i686 Nix package in her PATH, then we
|
||||
# would silently downgrade the whole system to be i686 NixOS on the
|
||||
# next reboot.
|
||||
# The binary will be included in the wrapper for Python.
|
||||
nix
|
||||
];
|
||||
|
||||
preBuild = ''
|
||||
substituteInPlace nixos_rebuild/__init__.py \
|
||||
--subst-var-by nixos_rebuild ${lib.getExe nixos-rebuild}
|
||||
'';
|
||||
|
||||
postInstall =
|
||||
''
|
||||
installManPage ${nixos-rebuild}/share/man/man8/nixos-rebuild.8
|
||||
|
||||
installShellCompletion \
|
||||
--bash ${nixos-rebuild}/share/bash-completion/completions/_nixos-rebuild
|
||||
''
|
||||
+ lib.optionalString withNgSuffix ''
|
||||
mv $out/bin/nixos-rebuild $out/bin/nixos-rebuild-ng
|
||||
'';
|
||||
|
||||
nativeCheckInputs = with python3.pkgs; [
|
||||
pytestCheckHook
|
||||
mypy
|
||||
ruff
|
||||
];
|
||||
|
||||
pytestFlagsArray = [ "-vv" ];
|
||||
|
||||
postCheck = ''
|
||||
echo -e "\x1b[32m## run mypy\x1b[0m"
|
||||
mypy nixos_rebuild tests
|
||||
echo -e "\x1b[32m## run ruff\x1b[0m"
|
||||
ruff check nixos_rebuild tests
|
||||
echo -e "\x1b[32m## run ruff format\x1b[0m"
|
||||
ruff format --check nixos_rebuild tests
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Rebuild your NixOS configuration and switch to it, on local hosts and remote";
|
||||
homepage = "https://github.com/NixOS/nixpkgs/tree/master/pkgs/by-name/ni/nixos-rebuild-ng";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = [ lib.maintainers.thiagokokada ];
|
||||
mainProgram = if withNgSuffix then "nixos-rebuild-ng" else "nixos-rebuild";
|
||||
};
|
||||
}
|
222
pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py
Normal file
222
pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py
Normal file
@ -0,0 +1,222 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from subprocess import run
|
||||
from typing import assert_never
|
||||
|
||||
from tabulate import tabulate
|
||||
|
||||
from .models import Action, Flake, NRError, Profile
|
||||
from .nix import (
|
||||
edit,
|
||||
list_generations,
|
||||
nixos_build,
|
||||
nixos_build_flake,
|
||||
rollback,
|
||||
rollback_temporary_profile,
|
||||
set_profile,
|
||||
switch_to_configuration,
|
||||
upgrade_channels,
|
||||
)
|
||||
from .utils import info
|
||||
|
||||
VERBOSE = False
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
|
||||
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("--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", "-p", default="system")
|
||||
parser.add_argument("--specialisation", "-c")
|
||||
parser.add_argument("--rollback", action="store_true")
|
||||
parser.add_argument("--upgrade", action="store_true")
|
||||
parser.add_argument("--upgrade-all", action="store_true")
|
||||
parser.add_argument("--json", action="store_true")
|
||||
parser.add_argument("action", choices=Action.values(), nargs="?")
|
||||
|
||||
args, remainder = parser.parse_known_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)
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/linux/nixos-rebuild/nixos-rebuild.sh#L56
|
||||
if args.action == Action.DRY_RUN.value:
|
||||
args.action = Action.DRY_BUILD.value
|
||||
|
||||
if args.install_grub:
|
||||
info(
|
||||
f"{parser.prog}: warning: --install-grub deprecated, use --install-bootloader instead"
|
||||
)
|
||||
args.install_bootloader = True
|
||||
|
||||
if args.action == Action.EDIT.value and (args.file or args.attr):
|
||||
parser.error("--file and --attr are not supported with 'edit'")
|
||||
|
||||
if args.flake and (args.file or args.attr):
|
||||
parser.error("--flake cannot be used with --file or --attr")
|
||||
|
||||
if args.help or args.action is None:
|
||||
r = run(["man", "8", "nixos-rebuild"], check=False)
|
||||
parser.exit(r.returncode)
|
||||
|
||||
return args, remainder
|
||||
|
||||
|
||||
def execute(argv: list[str]) -> None:
|
||||
args, nix_flags = parse_args(argv)
|
||||
|
||||
profile = Profile.from_name(args.profile_name)
|
||||
flake = Flake.from_arg(args.flake)
|
||||
|
||||
if args.upgrade or args.upgrade_all:
|
||||
upgrade_channels(bool(args.upgrade_all))
|
||||
|
||||
match action := Action(args.action):
|
||||
case Action.SWITCH | Action.BOOT:
|
||||
info("building the system configuration...")
|
||||
if args.rollback:
|
||||
path_to_config = rollback(profile)
|
||||
elif flake:
|
||||
path_to_config = nixos_build_flake(
|
||||
"toplevel",
|
||||
flake,
|
||||
nix_flags,
|
||||
no_link=True,
|
||||
)
|
||||
set_profile(profile, path_to_config)
|
||||
else:
|
||||
path_to_config = nixos_build(
|
||||
"system",
|
||||
args.attr,
|
||||
args.file,
|
||||
nix_flags,
|
||||
no_out_link=True,
|
||||
)
|
||||
set_profile(profile, path_to_config)
|
||||
switch_to_configuration(
|
||||
path_to_config,
|
||||
action,
|
||||
specialisation=args.specialisation,
|
||||
install_bootloader=args.install_bootloader,
|
||||
)
|
||||
case Action.TEST | Action.BUILD | Action.DRY_BUILD | Action.DRY_ACTIVATE:
|
||||
info("building the system configuration...")
|
||||
dry_run = action == Action.DRY_BUILD
|
||||
if args.rollback and action in (Action.TEST, Action.BUILD):
|
||||
maybe_path_to_config = rollback_temporary_profile(profile)
|
||||
if maybe_path_to_config: # kinda silly but this makes mypy happy
|
||||
path_to_config = maybe_path_to_config
|
||||
else:
|
||||
raise NRError("could not find previous generation")
|
||||
elif flake:
|
||||
path_to_config = nixos_build_flake(
|
||||
"toplevel",
|
||||
flake,
|
||||
nix_flags,
|
||||
keep_going=True,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
else:
|
||||
path_to_config = nixos_build(
|
||||
"system",
|
||||
args.attr,
|
||||
args.file,
|
||||
nix_flags,
|
||||
keep_going=True,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
if action in (Action.TEST, Action.DRY_ACTIVATE):
|
||||
switch_to_configuration(
|
||||
path_to_config,
|
||||
action,
|
||||
specialisation=args.specialisation,
|
||||
)
|
||||
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 = nixos_build_flake(
|
||||
attr,
|
||||
flake,
|
||||
nix_flags,
|
||||
keep_going=True,
|
||||
)
|
||||
else:
|
||||
path_to_config = nixos_build(
|
||||
attr,
|
||||
args.attr,
|
||||
args.file,
|
||||
nix_flags,
|
||||
keep_going=True,
|
||||
)
|
||||
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)
|
||||
case Action.DRY_RUN:
|
||||
assert False, "DRY_RUN should be a DRY_BUILD alias"
|
||||
case Action.LIST_GENERATIONS:
|
||||
generations = list_generations(profile)
|
||||
if args.json:
|
||||
print(json.dumps(generations, indent=2))
|
||||
else:
|
||||
headers = {
|
||||
"generation": "Generation",
|
||||
"date": "Build-date",
|
||||
"nixosVersion": "NixOS version",
|
||||
"kernelVersion": "Kernel",
|
||||
"configurationRevision": "Configuration Revision",
|
||||
"specialisations": "Specialisation",
|
||||
"current": "Current",
|
||||
}
|
||||
# Not exactly the same format as legacy nixos-rebuild but close
|
||||
# enough
|
||||
table = tabulate(
|
||||
generations,
|
||||
headers=headers,
|
||||
tablefmt="plain",
|
||||
numalign="left",
|
||||
stralign="left",
|
||||
disable_numparse=True,
|
||||
)
|
||||
print(table)
|
||||
case Action.REPL:
|
||||
# For now just redirect it to `nixos-rebuild` instead of
|
||||
# duplicating the code
|
||||
os.execv(
|
||||
"@nixos_rebuild@",
|
||||
argv,
|
||||
)
|
||||
case _:
|
||||
assert_never(action)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
execute(sys.argv)
|
||||
except (Exception, KeyboardInterrupt) as ex:
|
||||
if VERBOSE:
|
||||
raise ex
|
||||
else:
|
||||
sys.exit(str(ex))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
121
pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py
Normal file
121
pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py
Normal file
@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, TypedDict, override
|
||||
|
||||
|
||||
class NRError(Exception):
|
||||
"nixos-rebuild general error."
|
||||
|
||||
def __init__(self, message: str):
|
||||
self.message = message
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"error: {self.message}"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
@staticmethod
|
||||
def values() -> list[str]:
|
||||
return [a.value for a in Action]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Flake:
|
||||
path: Path
|
||||
attr: str
|
||||
_re: ClassVar[re.Pattern[str]] = re.compile(
|
||||
r"^(?P<path>[^\#]*)\#?(?P<attr>[^\#\"]*)$"
|
||||
)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.path}#{self.attr}"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, flake_str: str, hostname: str | None = None) -> Flake:
|
||||
m = cls._re.match(flake_str)
|
||||
assert m is not None, f"got no matches for {flake_str}"
|
||||
attr = m.group("attr")
|
||||
if not attr:
|
||||
attr = f"nixosConfigurations.{hostname or "default"}"
|
||||
else:
|
||||
attr = f"nixosConfigurations.{attr}"
|
||||
return Flake(Path(m.group("path")), attr)
|
||||
|
||||
@classmethod
|
||||
def from_arg(cls, flake_arg: Any) -> Flake | None:
|
||||
hostname = platform.node()
|
||||
match flake_arg:
|
||||
case str(s):
|
||||
return cls.parse(s, hostname)
|
||||
case True:
|
||||
return cls.parse(".", hostname)
|
||||
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 cls.parse(str(default_path.parent), hostname)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Generation:
|
||||
id: int
|
||||
timestamp: str # we may want to have a proper timestamp type in future
|
||||
current: bool
|
||||
|
||||
|
||||
# camelCase since this will be used as output for `--json` flag
|
||||
class GenerationJson(TypedDict):
|
||||
generation: int
|
||||
date: str
|
||||
nixosVersion: str
|
||||
kernelVersion: str
|
||||
configurationRevision: str
|
||||
specialisations: list[str]
|
||||
current: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Profile:
|
||||
name: str
|
||||
path: Path
|
||||
|
||||
@staticmethod
|
||||
def from_name(name: str = "system") -> Profile:
|
||||
match name:
|
||||
case "system":
|
||||
return Profile(name, Path("/nix/var/nix/profiles/system"))
|
||||
case _:
|
||||
path = Path("/nix/var/nix/profiles/system-profiles") / name
|
||||
path.parent.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
return Profile(name, path)
|
282
pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py
Normal file
282
pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py
Normal file
@ -0,0 +1,282 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, CalledProcessError, run
|
||||
from typing import Final
|
||||
|
||||
from .models import (
|
||||
Action,
|
||||
Flake,
|
||||
Generation,
|
||||
GenerationJson,
|
||||
NRError,
|
||||
Profile,
|
||||
)
|
||||
from .utils import dict_to_flags
|
||||
|
||||
FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"]
|
||||
|
||||
|
||||
def edit(flake: Flake | None, nix_flags: list[str] | None = None) -> None:
|
||||
"Try to find and open NixOS configuration file in editor."
|
||||
if flake:
|
||||
run(
|
||||
["nix", *FLAKE_FLAGS, "edit", *(nix_flags or []), "--", str(flake)],
|
||||
check=False,
|
||||
)
|
||||
else:
|
||||
if nix_flags:
|
||||
raise NRError("'edit' does not support extra Nix flags")
|
||||
nixos_config = Path(
|
||||
os.getenv("NIXOS_CONFIG")
|
||||
or run(
|
||||
["nix-instantiate", "--find-file", "nixos-config"],
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
check=False,
|
||||
).stdout.strip()
|
||||
or "/etc/nixos/default.nix"
|
||||
)
|
||||
if nixos_config.is_dir():
|
||||
nixos_config /= "default.nix"
|
||||
|
||||
if nixos_config.exists():
|
||||
run([os.getenv("EDITOR", "nano"), nixos_config], check=False)
|
||||
else:
|
||||
raise NRError("cannot find NixOS config file")
|
||||
|
||||
|
||||
def _parse_generation_from_nix_store(path: Path, profile: Profile) -> Generation:
|
||||
entry_id = path.name.split("-")[1]
|
||||
current = path.name == profile.path.readlink().name
|
||||
timestamp = datetime.fromtimestamp(path.stat().st_ctime).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
return Generation(
|
||||
id=int(entry_id),
|
||||
timestamp=timestamp,
|
||||
current=current,
|
||||
)
|
||||
|
||||
|
||||
def _parse_generation_from_nix_env(line: str) -> Generation:
|
||||
parts = line.split()
|
||||
|
||||
entry_id = parts[0]
|
||||
timestamp = f"{parts[1]} {parts[2]}"
|
||||
current = "(current)" in parts
|
||||
|
||||
return Generation(
|
||||
id=int(entry_id),
|
||||
timestamp=timestamp,
|
||||
current=current,
|
||||
)
|
||||
|
||||
|
||||
def get_generations(profile: Profile, lock_profile: bool = False) -> list[Generation]:
|
||||
"""Get all NixOS generations from profile.
|
||||
|
||||
Includes generation ID (e.g.: 1, 2), timestamp (e.g.: when it was created)
|
||||
and if this is the current active profile or not.
|
||||
|
||||
If `lock_profile = True` this command will need root to run successfully.
|
||||
"""
|
||||
if not profile.path.exists():
|
||||
raise NRError(f"no profile '{profile.name}' found")
|
||||
|
||||
result = []
|
||||
if lock_profile:
|
||||
# Using `nix-env --list-generations` needs root to lock the profile
|
||||
# TODO: do we actually need to lock profile for e.g.: rollback?
|
||||
# https://github.com/NixOS/nix/issues/5144
|
||||
r = run(
|
||||
["nix-env", "-p", profile.path, "--list-generations"],
|
||||
text=True,
|
||||
stdout=True,
|
||||
check=True,
|
||||
)
|
||||
for line in r.stdout.splitlines():
|
||||
result.append(_parse_generation_from_nix_env(line))
|
||||
else:
|
||||
for p in profile.path.parent.glob("system-*-link"):
|
||||
result.append(_parse_generation_from_nix_store(p, profile))
|
||||
return sorted(result, key=lambda d: d.id)
|
||||
|
||||
|
||||
def list_generations(profile: Profile) -> list[GenerationJson]:
|
||||
"""Get all NixOS generations from profile, including extra information.
|
||||
|
||||
Includes OS information like the commit, kernel version, configuration
|
||||
revision and specialisations.
|
||||
|
||||
Will be formatted in a way that is expected by the output of
|
||||
`nixos-rebuild list-generations --json`.
|
||||
"""
|
||||
generations = get_generations(profile)
|
||||
result = []
|
||||
for generation in reversed(generations):
|
||||
generation_path = (
|
||||
profile.path.parent / f"{profile.path.name}-{generation.id}-link"
|
||||
)
|
||||
try:
|
||||
nixos_version = (generation_path / "nixos-version").read_text().strip()
|
||||
except IOError:
|
||||
nixos_version = "Unknown"
|
||||
try:
|
||||
kernel_version = next(
|
||||
(generation_path / "kernel-modules/lib/modules").iterdir()
|
||||
).name
|
||||
except IOError:
|
||||
kernel_version = "Unknown"
|
||||
specialisations = [
|
||||
s.name for s in (generation_path / "specialisation").glob("*") if s.is_dir()
|
||||
]
|
||||
try:
|
||||
configuration_revision = run(
|
||||
[generation_path / "sw/bin/nixos-version", "--configuration-revision"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
except (CalledProcessError, IOError):
|
||||
configuration_revision = "Unknown"
|
||||
|
||||
result.append(
|
||||
GenerationJson(
|
||||
generation=generation.id,
|
||||
date=generation.timestamp,
|
||||
nixosVersion=nixos_version,
|
||||
kernelVersion=kernel_version,
|
||||
configurationRevision=configuration_revision,
|
||||
specialisations=specialisations,
|
||||
current=generation.current,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def nixos_build(
|
||||
attr: str,
|
||||
pre_attr: str | None,
|
||||
file: str | None,
|
||||
nix_flags: list[str] | None = None,
|
||||
**kwargs: bool | str,
|
||||
) -> Path:
|
||||
"""Build NixOS attribute using classic Nix.
|
||||
|
||||
It will by default build `<nixpkgs/nixos>` with `attr`, however it
|
||||
optionally supports building from an external file and custom attributes
|
||||
paths.
|
||||
|
||||
Returns the built attribute as 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 += dict_to_flags(kwargs) + (nix_flags or [])
|
||||
r = run(run_args, check=True, text=True, stdout=PIPE)
|
||||
return Path(r.stdout.strip())
|
||||
|
||||
|
||||
def nixos_build_flake(
|
||||
attr: str,
|
||||
flake: Flake,
|
||||
nix_flags: list[str] | None = None,
|
||||
**kwargs: bool | str,
|
||||
) -> Path:
|
||||
"""Build NixOS attribute using Flakes.
|
||||
|
||||
Returns the built attribute as path.
|
||||
"""
|
||||
run_args = [
|
||||
"nix",
|
||||
*FLAKE_FLAGS,
|
||||
"build",
|
||||
"--print-out-paths",
|
||||
f"{flake}.config.system.build.{attr}",
|
||||
]
|
||||
run_args += dict_to_flags(kwargs) + (nix_flags or [])
|
||||
r = run(run_args, check=True, text=True, stdout=PIPE)
|
||||
return Path(r.stdout.strip())
|
||||
|
||||
|
||||
def rollback(profile: Profile) -> Path:
|
||||
"Rollback Nix profile, like one created by `nixos-rebuild switch`."
|
||||
run(["nix-env", "--rollback", "-p", profile.path], check=True)
|
||||
# Rollback config PATH is the own profile
|
||||
return profile.path
|
||||
|
||||
|
||||
def rollback_temporary_profile(profile: Profile) -> Path | None:
|
||||
"Rollback a temporary Nix profile, like one created by `nixos-rebuild test`."
|
||||
generations = get_generations(profile, lock_profile=True)
|
||||
previous_gen_id = None
|
||||
for generation in generations:
|
||||
if not generation.current:
|
||||
previous_gen_id = generation.id
|
||||
|
||||
if previous_gen_id:
|
||||
return profile.path.parent / f"{profile.name}-{previous_gen_id}-link"
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def set_profile(profile: Profile, path_to_config: Path) -> None:
|
||||
"Set a path as the current active Nix profile."
|
||||
run(["nix-env", "-p", profile.path, "--set", path_to_config], check=True)
|
||||
|
||||
|
||||
def switch_to_configuration(
|
||||
path_to_config: Path,
|
||||
action: Action,
|
||||
install_bootloader: bool = False,
|
||||
specialisation: str | None = None,
|
||||
) -> None:
|
||||
"""Call `<config>/bin/switch-to-configuration <action>`.
|
||||
|
||||
Expects a built path to run, like one generated with `nixos_build` or
|
||||
`nixos_build_flake` functions.
|
||||
"""
|
||||
if specialisation:
|
||||
if action not in (Action.SWITCH, Action.TEST):
|
||||
raise NRError(
|
||||
"'--specialisation' can only be used with 'switch' and 'test'"
|
||||
)
|
||||
path_to_config = path_to_config / f"specialisation/{specialisation}"
|
||||
|
||||
if not path_to_config.exists():
|
||||
raise NRError(f"specialisation not found: {specialisation}")
|
||||
|
||||
run(
|
||||
[path_to_config / "bin/switch-to-configuration", str(action)],
|
||||
env={
|
||||
"NIXOS_INSTALL_BOOTLOADER": "1" if install_bootloader else "0",
|
||||
"LOCALE_ARCHIVE": os.getenv("LOCALE_ARCHIVE", ""),
|
||||
},
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def upgrade_channels(all: bool = False) -> None:
|
||||
"""Upgrade channels for classic Nix.
|
||||
|
||||
It will either upgrade just the `nixos` channel (including any channel
|
||||
that has a `.update-on-nixos-rebuild` file) or all.
|
||||
"""
|
||||
for channel_path in Path("/nix/var/nix/profiles/per-user/root/channels/").glob("*"):
|
||||
if (
|
||||
all
|
||||
or channel_path.name == "nixos"
|
||||
or (channel_path / ".update-on-nixos-rebuild").exists()
|
||||
):
|
||||
run(["nix-channel", "--update", channel_path.name], check=False)
|
28
pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/utils.py
Normal file
28
pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/utils.py
Normal file
@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
info = partial(print, file=sys.stderr)
|
||||
|
||||
|
||||
def dict_to_flags(d: dict[str, Any]) -> list[str]:
|
||||
flags = []
|
||||
for key, value in d.items():
|
||||
flag = f"--{'-'.join(key.split('_'))}"
|
||||
match value:
|
||||
case None | False:
|
||||
pass
|
||||
case True:
|
||||
flags.append(flag)
|
||||
case int():
|
||||
flags.append(f"-{key[0] * value}")
|
||||
case str():
|
||||
flags.append(flag)
|
||||
flags.append(value)
|
||||
case list():
|
||||
flags.append(flag)
|
||||
for v in value:
|
||||
flags.append(v)
|
||||
return flags
|
51
pkgs/by-name/ni/nixos-rebuild-ng/src/pyproject.toml
Normal file
51
pkgs/by-name/ni/nixos-rebuild-ng/src/pyproject.toml
Normal file
@ -0,0 +1,51 @@
|
||||
[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"
|
||||
|
||||
[tool.mypy]
|
||||
# `--strict` config, but explicit options to avoid breaking build when mypy is
|
||||
# updated
|
||||
warn_unused_configs = true
|
||||
disallow_any_generics = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
check_untyped_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
warn_return_any = true
|
||||
# no_implicit_reexport = true
|
||||
strict_equality = true
|
||||
extra_checks = true
|
||||
|
||||
# extra options not included in `--strict`
|
||||
enable_error_code = ["explicit-override", "mutable-override"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "pytest.*"
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = [
|
||||
# ensure imports are sorted
|
||||
"I",
|
||||
# require 'from __future__ import annotations'
|
||||
"FA102",
|
||||
# require `check` argument for `subprocess.run`
|
||||
"PLW1510",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/" = ["FA102"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = ["--import-mode=importlib"]
|
13
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/helpers.py
Normal file
13
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/helpers.py
Normal file
@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
def get_qualified_name(
|
||||
method: Callable[..., Any],
|
||||
module: ModuleType | None = None,
|
||||
) -> str:
|
||||
module_name = getattr(module, "__name__", method.__module__)
|
||||
method_name = getattr(method, "__qualname__", method.__name__)
|
||||
return f"{module_name}.{method_name}"
|
270
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py
Normal file
270
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py
Normal file
@ -0,0 +1,270 @@
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, CompletedProcess
|
||||
from typing import Any
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import nixos_rebuild as nr
|
||||
|
||||
from .helpers import get_qualified_name
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(monkeypatch: Any) -> None:
|
||||
monkeypatch.setenv("LOCALE_ARCHIVE", "/locale")
|
||||
|
||||
|
||||
def test_parse_args() -> None:
|
||||
with pytest.raises(SystemExit) as e:
|
||||
nr.parse_args(["nixos-rebuild", "unknown-action"])
|
||||
assert e.value.code == 2
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
nr.parse_args(["nixos-rebuild", "test", "--flake", "--file", "abc"])
|
||||
assert e.value.code == 2
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
nr.parse_args(["nixos-rebuild", "edit", "--attr", "attr"])
|
||||
assert e.value.code == 2
|
||||
|
||||
r1, remainder = nr.parse_args(
|
||||
[
|
||||
"nixos-rebuild",
|
||||
"switch",
|
||||
"--install-grub",
|
||||
"--flake",
|
||||
"/etc/nixos",
|
||||
"--extra",
|
||||
"flag",
|
||||
]
|
||||
)
|
||||
assert remainder == ["--extra", "flag"]
|
||||
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"
|
||||
|
||||
r2, remainder = nr.parse_args(
|
||||
[
|
||||
"nixos-rebuild",
|
||||
"dry-run",
|
||||
"--flake",
|
||||
"--no-flake",
|
||||
"-f",
|
||||
"foo",
|
||||
"--attr",
|
||||
"bar",
|
||||
]
|
||||
)
|
||||
assert remainder == []
|
||||
assert r2.flake is False
|
||||
assert r2.action == "dry-build"
|
||||
assert r2.file == "foo"
|
||||
assert r2.attr == "bar"
|
||||
|
||||
|
||||
@patch(get_qualified_name(nr.nix.run, nr.nix), autospec=True)
|
||||
def test_execute_nix_boot(mock_run: Any, tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "test"
|
||||
config_path.touch()
|
||||
mock_run.side_effect = [
|
||||
# nixos_build
|
||||
CompletedProcess([], 0, str(config_path)),
|
||||
# set_profile
|
||||
CompletedProcess([], 0),
|
||||
# switch_to_configuration
|
||||
CompletedProcess([], 0),
|
||||
]
|
||||
|
||||
nr.execute(["nixos-rebuild", "boot", "--no-flake", "-vvv"])
|
||||
|
||||
assert nr.VERBOSE is True
|
||||
assert mock_run.call_count == 3
|
||||
mock_run.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
[
|
||||
"nix-build",
|
||||
"<nixpkgs/nixos>",
|
||||
"--attr",
|
||||
"system",
|
||||
"--no-out-link",
|
||||
"-vvv",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
),
|
||||
call(
|
||||
[
|
||||
"nix-env",
|
||||
"-p",
|
||||
Path("/nix/var/nix/profiles/system"),
|
||||
"--set",
|
||||
config_path,
|
||||
],
|
||||
check=True,
|
||||
),
|
||||
call(
|
||||
[config_path / "bin/switch-to-configuration", "boot"],
|
||||
env={"NIXOS_INSTALL_BOOTLOADER": "0", "LOCALE_ARCHIVE": "/locale"},
|
||||
check=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@patch(get_qualified_name(nr.nix.run, nr.nix), autospec=True)
|
||||
def test_execute_nix_switch_flake(mock_run: Any, tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "test"
|
||||
config_path.touch()
|
||||
mock_run.side_effect = [
|
||||
# nixos_build_flake
|
||||
CompletedProcess([], 0, str(config_path)),
|
||||
# set_profile
|
||||
CompletedProcess([], 0),
|
||||
# switch_to_configuration
|
||||
CompletedProcess([], 0),
|
||||
]
|
||||
|
||||
nr.execute(
|
||||
[
|
||||
"nixos-rebuild",
|
||||
"switch",
|
||||
"--flake",
|
||||
"/path/to/config#hostname",
|
||||
"--install-bootloader",
|
||||
"--verbose",
|
||||
]
|
||||
)
|
||||
|
||||
assert nr.VERBOSE is True
|
||||
assert mock_run.call_count == 3
|
||||
mock_run.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
[
|
||||
"nix",
|
||||
"--extra-experimental-features",
|
||||
"nix-command flakes",
|
||||
"build",
|
||||
"--print-out-paths",
|
||||
"/path/to/config#nixosConfigurations.hostname.config.system.build.toplevel",
|
||||
"--no-link",
|
||||
"--verbose",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
),
|
||||
call(
|
||||
[
|
||||
"nix-env",
|
||||
"-p",
|
||||
Path("/nix/var/nix/profiles/system"),
|
||||
"--set",
|
||||
config_path,
|
||||
],
|
||||
check=True,
|
||||
),
|
||||
call(
|
||||
[config_path / "bin/switch-to-configuration", "switch"],
|
||||
env={"NIXOS_INSTALL_BOOTLOADER": "1", "LOCALE_ARCHIVE": "/locale"},
|
||||
check=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@patch(get_qualified_name(nr.nix.run, nr.nix), autospec=True)
|
||||
def test_execute_switch_rollback(mock_run: Any) -> None:
|
||||
nr.execute(["nixos-rebuild", "switch", "--rollback", "--install-bootloader"])
|
||||
|
||||
assert nr.VERBOSE is False
|
||||
assert mock_run.call_count == 2
|
||||
mock_run.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
[
|
||||
"nix-env",
|
||||
"--rollback",
|
||||
"-p",
|
||||
Path("/nix/var/nix/profiles/system"),
|
||||
],
|
||||
check=True,
|
||||
),
|
||||
call(
|
||||
[
|
||||
Path("/nix/var/nix/profiles/system/bin/switch-to-configuration"),
|
||||
"switch",
|
||||
],
|
||||
env={"NIXOS_INSTALL_BOOTLOADER": "1", "LOCALE_ARCHIVE": "/locale"},
|
||||
check=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@patch(get_qualified_name(nr.nix.run, nr.nix), autospec=True)
|
||||
@patch(get_qualified_name(nr.nix.Path.exists, nr.nix), autospec=True, return_value=True)
|
||||
@patch(get_qualified_name(nr.nix.Path.mkdir, nr.nix), autospec=True)
|
||||
def test_execute_test_rollback(
|
||||
mock_path_mkdir: Any,
|
||||
mock_path_exists: Any,
|
||||
mock_run: Any,
|
||||
) -> None:
|
||||
mock_run.side_effect = [
|
||||
# rollback_temporary_profile
|
||||
CompletedProcess(
|
||||
[],
|
||||
0,
|
||||
stdout=textwrap.dedent("""\
|
||||
2082 2024-11-07 22:58:56
|
||||
2083 2024-11-07 22:59:41
|
||||
2084 2024-11-07 23:54:17 (current)
|
||||
"""),
|
||||
),
|
||||
# switch_to_configuration
|
||||
CompletedProcess([], 0),
|
||||
]
|
||||
|
||||
nr.execute(
|
||||
[
|
||||
"nixos-rebuild",
|
||||
"test",
|
||||
"--rollback",
|
||||
"--profile-name",
|
||||
"foo",
|
||||
]
|
||||
)
|
||||
|
||||
assert nr.VERBOSE is False
|
||||
assert mock_run.call_count == 2
|
||||
mock_run.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
[
|
||||
"nix-env",
|
||||
"-p",
|
||||
Path("/nix/var/nix/profiles/system-profiles/foo"),
|
||||
"--list-generations",
|
||||
],
|
||||
text=True,
|
||||
stdout=True,
|
||||
check=True,
|
||||
),
|
||||
call(
|
||||
[
|
||||
Path(
|
||||
"/nix/var/nix/profiles/system-profiles/foo-2083-link/bin/switch-to-configuration"
|
||||
),
|
||||
"test",
|
||||
],
|
||||
env={"NIXOS_INSTALL_BOOTLOADER": "0", "LOCALE_ARCHIVE": "/locale"},
|
||||
check=True,
|
||||
),
|
||||
]
|
||||
)
|
100
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py
Normal file
100
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py
Normal file
@ -0,0 +1,100 @@
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from nixos_rebuild import models as m
|
||||
|
||||
from .helpers import get_qualified_name
|
||||
|
||||
|
||||
def test_flake_parse() -> None:
|
||||
assert m.Flake.parse("/path/to/flake#attr") == m.Flake(
|
||||
Path("/path/to/flake"), "nixosConfigurations.attr"
|
||||
)
|
||||
assert m.Flake.parse("/path/ to /flake", "hostname") == m.Flake(
|
||||
Path("/path/ to /flake"), "nixosConfigurations.hostname"
|
||||
)
|
||||
assert m.Flake.parse("/path/to/flake", "hostname") == m.Flake(
|
||||
Path("/path/to/flake"), "nixosConfigurations.hostname"
|
||||
)
|
||||
assert m.Flake.parse(".#attr") == m.Flake(Path("."), "nixosConfigurations.attr")
|
||||
assert m.Flake.parse("#attr") == m.Flake(Path("."), "nixosConfigurations.attr")
|
||||
assert m.Flake.parse(".", None) == m.Flake(Path("."), "nixosConfigurations.default")
|
||||
assert m.Flake.parse("", "") == m.Flake(Path("."), "nixosConfigurations.default")
|
||||
|
||||
|
||||
@patch(get_qualified_name(platform.node), autospec=True)
|
||||
def test_flake_from_arg(mock_node: Any) -> None:
|
||||
mock_node.return_value = "hostname"
|
||||
|
||||
# Flake string
|
||||
assert m.Flake.from_arg("/path/to/flake#attr") == m.Flake(
|
||||
Path("/path/to/flake"), "nixosConfigurations.attr"
|
||||
)
|
||||
|
||||
# False
|
||||
assert m.Flake.from_arg(False) is None
|
||||
|
||||
# True
|
||||
assert m.Flake.from_arg(True) == m.Flake(Path("."), "nixosConfigurations.hostname")
|
||||
|
||||
# None when we do not have /etc/nixos/flake.nix
|
||||
with patch(
|
||||
get_qualified_name(m.Path.exists, m),
|
||||
autospec=True,
|
||||
return_value=False,
|
||||
):
|
||||
assert m.Flake.from_arg(None) is None
|
||||
|
||||
# None when we have a file in /etc/nixos/flake.nix
|
||||
with (
|
||||
patch(
|
||||
get_qualified_name(m.Path.exists, m),
|
||||
autospec=True,
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
get_qualified_name(m.Path.is_symlink, m),
|
||||
autospec=True,
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert m.Flake.from_arg(None) == m.Flake(
|
||||
Path("/etc/nixos"), "nixosConfigurations.hostname"
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
get_qualified_name(m.Path.exists, m),
|
||||
autospec=True,
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
get_qualified_name(m.Path.is_symlink, m),
|
||||
autospec=True,
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
get_qualified_name(m.Path.readlink, m),
|
||||
autospec=True,
|
||||
return_value=Path("/path/to/flake.nix"),
|
||||
),
|
||||
):
|
||||
assert m.Flake.from_arg(None) == m.Flake(
|
||||
Path("/path/to"), "nixosConfigurations.hostname"
|
||||
)
|
||||
|
||||
|
||||
@patch(get_qualified_name(m.Path.mkdir, m), autospec=True)
|
||||
def test_profile_from_name(mock_mkdir: Any) -> None:
|
||||
assert m.Profile.from_name("system") == m.Profile(
|
||||
"system",
|
||||
Path("/nix/var/nix/profiles/system"),
|
||||
)
|
||||
|
||||
assert m.Profile.from_name("something") == m.Profile(
|
||||
"something",
|
||||
Path("/nix/var/nix/profiles/system-profiles/something"),
|
||||
)
|
||||
mock_mkdir.assert_called_once()
|
327
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py
Normal file
327
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py
Normal file
@ -0,0 +1,327 @@
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, CompletedProcess
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import nixos_rebuild.nix as n
|
||||
from nixos_rebuild import models as m
|
||||
|
||||
from .helpers import get_qualified_name
|
||||
|
||||
|
||||
@patch(get_qualified_name(n.run, n), autospec=True)
|
||||
def test_edit(mock_run: Any, monkeypatch: Any, tmpdir: Any) -> None:
|
||||
# Flake
|
||||
flake = m.Flake.parse(".#attr")
|
||||
n.edit(flake, ["--commit-lock-file"])
|
||||
mock_run.assert_called_with(
|
||||
[
|
||||
"nix",
|
||||
"--extra-experimental-features",
|
||||
"nix-command flakes",
|
||||
"edit",
|
||||
"--commit-lock-file",
|
||||
"--",
|
||||
".#nixosConfigurations.attr",
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Classic
|
||||
with monkeypatch.context() as mp:
|
||||
default_nix = tmpdir.join("default.nix")
|
||||
default_nix.write("{}")
|
||||
|
||||
mp.setenv("NIXOS_CONFIG", str(tmpdir))
|
||||
mp.setenv("EDITOR", "editor")
|
||||
|
||||
n.edit(None)
|
||||
mock_run.assert_called_with(["editor", default_nix], check=False)
|
||||
|
||||
|
||||
def test_get_generations_from_nix_store(tmp_path: Path) -> None:
|
||||
nixos_path = tmp_path / "nixos-system"
|
||||
nixos_path.mkdir()
|
||||
|
||||
(tmp_path / "system").symlink_to(tmp_path / "system-2-link")
|
||||
# In the "wrong" order on purpose to make sure we are sorting the results
|
||||
(tmp_path / "system-1-link").symlink_to(nixos_path)
|
||||
(tmp_path / "system-3-link").symlink_to(nixos_path)
|
||||
(tmp_path / "system-2-link").symlink_to(nixos_path)
|
||||
|
||||
assert n.get_generations(
|
||||
m.Profile("system", tmp_path / "system"),
|
||||
lock_profile=False,
|
||||
) == [
|
||||
m.Generation(id=1, current=False, timestamp=ANY),
|
||||
m.Generation(id=2, current=True, timestamp=ANY),
|
||||
m.Generation(id=3, current=False, timestamp=ANY),
|
||||
]
|
||||
|
||||
|
||||
@patch(
|
||||
get_qualified_name(n.run, n),
|
||||
autospec=True,
|
||||
return_value=CompletedProcess(
|
||||
[],
|
||||
0,
|
||||
stdout=textwrap.dedent("""\
|
||||
2082 2024-11-07 22:58:56
|
||||
2083 2024-11-07 22:59:41
|
||||
2084 2024-11-07 23:54:17 (current)
|
||||
"""),
|
||||
),
|
||||
)
|
||||
def test_get_generations_from_nix_env(mock_run: Any, tmp_path: Path) -> None:
|
||||
path = tmp_path / "test"
|
||||
path.touch()
|
||||
|
||||
assert n.get_generations(m.Profile("system", path), lock_profile=True) == [
|
||||
m.Generation(id=2082, current=False, timestamp="2024-11-07 22:58:56"),
|
||||
m.Generation(id=2083, current=False, timestamp="2024-11-07 22:59:41"),
|
||||
m.Generation(id=2084, current=True, timestamp="2024-11-07 23:54:17"),
|
||||
]
|
||||
|
||||
|
||||
@patch(
|
||||
get_qualified_name(n.get_generations),
|
||||
autospec=True,
|
||||
return_value=[
|
||||
m.Generation(
|
||||
id=1,
|
||||
timestamp="2024-11-07 23:54:17",
|
||||
current=False,
|
||||
),
|
||||
m.Generation(
|
||||
id=2,
|
||||
timestamp="2024-11-07 23:54:17",
|
||||
current=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_list_generations(mock_get_generations: Any, tmp_path: Path) -> None:
|
||||
# Probably better to test this function in a real system, this test is
|
||||
# mostly to make sure it doesn't break horribly
|
||||
assert n.list_generations(m.Profile("system", tmp_path)) == [
|
||||
{
|
||||
"configurationRevision": "Unknown",
|
||||
"current": True,
|
||||
"date": "2024-11-07 23:54:17",
|
||||
"generation": 2,
|
||||
"kernelVersion": "Unknown",
|
||||
"nixosVersion": "Unknown",
|
||||
"specialisations": [],
|
||||
},
|
||||
{
|
||||
"configurationRevision": "Unknown",
|
||||
"current": False,
|
||||
"date": "2024-11-07 23:54:17",
|
||||
"generation": 1,
|
||||
"kernelVersion": "Unknown",
|
||||
"nixosVersion": "Unknown",
|
||||
"specialisations": [],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@patch(
|
||||
get_qualified_name(n.run, n),
|
||||
autospec=True,
|
||||
return_value=CompletedProcess([], 0, stdout=" \n/path/to/file\n "),
|
||||
)
|
||||
def test_nixos_build_flake(mock_run: Any) -> None:
|
||||
flake = m.Flake.parse(".#hostname")
|
||||
|
||||
assert n.nixos_build_flake(
|
||||
"toplevel",
|
||||
flake,
|
||||
["--nix-flag", "foo"],
|
||||
no_link=True,
|
||||
) == Path("/path/to/file")
|
||||
mock_run.assert_called_with(
|
||||
[
|
||||
"nix",
|
||||
"--extra-experimental-features",
|
||||
"nix-command flakes",
|
||||
"build",
|
||||
"--print-out-paths",
|
||||
".#nixosConfigurations.hostname.config.system.build.toplevel",
|
||||
"--no-link",
|
||||
"--nix-flag",
|
||||
"foo",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
|
||||
@patch(
|
||||
get_qualified_name(n.run, n),
|
||||
autospec=True,
|
||||
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"
|
||||
)
|
||||
mock_run.assert_called_with(
|
||||
["nix-build", "<nixpkgs/nixos>", "--attr", "attr", "--nix-flag", "foo"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
n.nixos_build("attr", "preAttr", "file")
|
||||
mock_run.assert_called_with(
|
||||
["nix-build", "file", "--attr", "preAttr.attr"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
n.nixos_build("attr", None, "file", no_out_link=True)
|
||||
mock_run.assert_called_with(
|
||||
["nix-build", "file", "--attr", "attr", "--no-out-link"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
n.nixos_build("attr", "preAttr", None, no_out_link=False, keep_going=True)
|
||||
mock_run.assert_called_with(
|
||||
["nix-build", "default.nix", "--attr", "preAttr.attr", "--keep-going"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
|
||||
@patch(get_qualified_name(n.run, n), autospec=True)
|
||||
def test_rollback(mock_run: Any, tmp_path: Path) -> None:
|
||||
path = tmp_path / "test"
|
||||
path.touch()
|
||||
|
||||
profile = m.Profile("system", path)
|
||||
|
||||
assert n.rollback(profile) == profile.path
|
||||
mock_run.assert_called_with(["nix-env", "--rollback", "-p", path], check=True)
|
||||
|
||||
|
||||
def test_rollback_temporary_profile(tmp_path: Path) -> None:
|
||||
path = tmp_path / "test"
|
||||
path.touch()
|
||||
profile = m.Profile("system", path)
|
||||
|
||||
with patch(get_qualified_name(n.run, n), autospec=True) as mock_run:
|
||||
mock_run.return_value = CompletedProcess(
|
||||
[],
|
||||
0,
|
||||
stdout=textwrap.dedent("""\
|
||||
2082 2024-11-07 22:58:56
|
||||
2083 2024-11-07 22:59:41
|
||||
2084 2024-11-07 23:54:17 (current)
|
||||
"""),
|
||||
)
|
||||
assert (
|
||||
n.rollback_temporary_profile(m.Profile("system", path))
|
||||
== path.parent / "system-2083-link"
|
||||
)
|
||||
assert (
|
||||
n.rollback_temporary_profile(m.Profile("foo", path))
|
||||
== path.parent / "foo-2083-link"
|
||||
)
|
||||
|
||||
with patch(get_qualified_name(n.run, n), autospec=True) as mock_run:
|
||||
mock_run.return_value = CompletedProcess([], 0, stdout="")
|
||||
assert n.rollback_temporary_profile(profile) is None
|
||||
|
||||
|
||||
@patch(get_qualified_name(n.run, n), autospec=True)
|
||||
def test_set_profile(mock_run: Any) -> None:
|
||||
profile_path = Path("/path/to/profile")
|
||||
config_path = Path("/path/to/config")
|
||||
n.set_profile(m.Profile("system", profile_path), config_path)
|
||||
|
||||
mock_run.assert_called_with(
|
||||
["nix-env", "-p", profile_path, "--set", config_path], check=True
|
||||
)
|
||||
|
||||
|
||||
@patch(get_qualified_name(n.run, n), autospec=True)
|
||||
def test_switch_to_configuration(mock_run: Any, monkeypatch: Any) -> None:
|
||||
profile_path = Path("/path/to/profile")
|
||||
config_path = Path("/path/to/config")
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setenv("LOCALE_ARCHIVE", "")
|
||||
|
||||
n.switch_to_configuration(
|
||||
profile_path,
|
||||
m.Action.SWITCH,
|
||||
specialisation=None,
|
||||
install_bootloader=False,
|
||||
)
|
||||
mock_run.assert_called_with(
|
||||
[profile_path / "bin/switch-to-configuration", "switch"],
|
||||
env={"NIXOS_INSTALL_BOOTLOADER": "0", "LOCALE_ARCHIVE": ""},
|
||||
check=True,
|
||||
)
|
||||
|
||||
with pytest.raises(m.NRError) as e:
|
||||
n.switch_to_configuration(
|
||||
config_path,
|
||||
m.Action.BOOT,
|
||||
specialisation="special",
|
||||
)
|
||||
assert (
|
||||
str(e.value)
|
||||
== "error: '--specialisation' can only be used with 'switch' and 'test'"
|
||||
)
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setenv("LOCALE_ARCHIVE", "/path/to/locale")
|
||||
mp.setattr(Path, Path.exists.__name__, lambda self: True)
|
||||
|
||||
n.switch_to_configuration(
|
||||
Path("/path/to/config"),
|
||||
m.Action.TEST,
|
||||
install_bootloader=True,
|
||||
specialisation="special",
|
||||
)
|
||||
mock_run.assert_called_with(
|
||||
[
|
||||
config_path / "specialisation/special/bin/switch-to-configuration",
|
||||
"test",
|
||||
],
|
||||
env={"NIXOS_INSTALL_BOOTLOADER": "1", "LOCALE_ARCHIVE": "/path/to/locale"},
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
@patch(
|
||||
get_qualified_name(n.Path.glob, n),
|
||||
autospec=True,
|
||||
return_value=[
|
||||
Path("/nix/var/nix/profiles/per-user/root/channels/nixos"),
|
||||
Path("/nix/var/nix/profiles/per-user/root/channels/nixos-hardware"),
|
||||
Path("/nix/var/nix/profiles/per-user/root/channels/home-manager"),
|
||||
],
|
||||
)
|
||||
def test_upgrade_channels(mock_glob: Any) -> None:
|
||||
with patch(get_qualified_name(n.run, n), autospec=True) as mock_run:
|
||||
n.upgrade_channels(False)
|
||||
mock_run.assert_called_with(["nix-channel", "--update", "nixos"], check=False)
|
||||
|
||||
with patch(get_qualified_name(n.run, n), autospec=True) as mock_run:
|
||||
n.upgrade_channels(True)
|
||||
mock_run.assert_has_calls(
|
||||
[
|
||||
call(["nix-channel", "--update", "nixos"], check=False),
|
||||
call(["nix-channel", "--update", "nixos-hardware"], check=False),
|
||||
call(["nix-channel", "--update", "home-manager"], check=False),
|
||||
]
|
||||
)
|
23
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_utils.py
Normal file
23
pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_utils.py
Normal file
@ -0,0 +1,23 @@
|
||||
from nixos_rebuild import utils as u
|
||||
|
||||
|
||||
def test_dict_to_flags() -> None:
|
||||
r = u.dict_to_flags(
|
||||
{
|
||||
"test_flag_1": True,
|
||||
"test_flag_2": False,
|
||||
"test_flag_3": "value",
|
||||
"test_flag_4": ["v1", "v2"],
|
||||
"test_flag_5": None,
|
||||
"verbose": 5,
|
||||
}
|
||||
)
|
||||
assert r == [
|
||||
"--test-flag-1",
|
||||
"--test-flag-3",
|
||||
"value",
|
||||
"--test-flag-4",
|
||||
"v1",
|
||||
"v2",
|
||||
"-vvvvv",
|
||||
]
|
Loading…
Reference in New Issue
Block a user