nixos-rebuild-ng: improve developer experience (#356468)
This commit is contained in:
@@ -53,6 +53,38 @@ an attempt of the rewrite.
|
|||||||
|
|
||||||
And use `nixos-rebuild-ng` instead of `nixos-rebuild`.
|
And use `nixos-rebuild-ng` instead of `nixos-rebuild`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```console
|
||||||
|
nix-build -A nixos-rebuild-ng.tests.ci
|
||||||
|
```
|
||||||
|
|
||||||
|
The command above will run the unit tests and linters, and also check if the
|
||||||
|
code is formatted. However, sometimes is more convenient to run just a few
|
||||||
|
tests to debug, in this case you can run:
|
||||||
|
|
||||||
|
```console
|
||||||
|
nix-shell -A nixos-rebuild-ng.devShell
|
||||||
|
```
|
||||||
|
|
||||||
|
The command above should automatically put you inside `src` directory, and you
|
||||||
|
can run:
|
||||||
|
|
||||||
|
```console
|
||||||
|
# run program
|
||||||
|
python -m nixos_rebuild
|
||||||
|
# run tests
|
||||||
|
python -m pytest
|
||||||
|
# check types
|
||||||
|
mypy .
|
||||||
|
# fix lint issues
|
||||||
|
ruff check --fix .
|
||||||
|
# format code
|
||||||
|
ruff format .
|
||||||
|
```
|
||||||
|
|
||||||
## Current caveats
|
## Current caveats
|
||||||
|
|
||||||
- For now we will install it in `nixos-rebuild-ng` path by default, to avoid
|
- For now we will install it in `nixos-rebuild-ng` path by default, to avoid
|
||||||
@@ -96,4 +128,10 @@ And use `nixos-rebuild-ng` instead of `nixos-rebuild`.
|
|||||||
- [ ] Improve documentation
|
- [ ] Improve documentation
|
||||||
- [ ] `nixos-rebuild repl` (calling old `nixos-rebuild` for now)
|
- [ ] `nixos-rebuild repl` (calling old `nixos-rebuild` for now)
|
||||||
- [ ] `nix` build/bootstrap
|
- [ ] `nix` build/bootstrap
|
||||||
- [ ] Reduce build closure
|
- [ ] Generate tab completion via [`shtab`](https://docs.iterative.ai/shtab/)
|
||||||
|
- [x] Reduce build closure
|
||||||
|
|
||||||
|
## TODON'T
|
||||||
|
|
||||||
|
- Reimplement `systemd-run` logic (will be moved to the new
|
||||||
|
[`apply`](https://github.com/NixOS/nixpkgs/pull/344407) script)
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
|
mkShell,
|
||||||
nix,
|
nix,
|
||||||
nixos-rebuild,
|
nixos-rebuild,
|
||||||
python3,
|
python3,
|
||||||
|
python3Packages,
|
||||||
|
runCommand,
|
||||||
withNgSuffix ? true,
|
withNgSuffix ? true,
|
||||||
}:
|
}:
|
||||||
python3.pkgs.buildPythonApplication {
|
python3Packages.buildPythonApplication rec {
|
||||||
pname = "nixos-rebuild-ng";
|
pname = "nixos-rebuild-ng";
|
||||||
version = "0.0.0";
|
version = "0.0.0";
|
||||||
src = ./src;
|
src = ./src;
|
||||||
pyproject = true;
|
pyproject = true;
|
||||||
|
|
||||||
build-system = with python3.pkgs; [
|
build-system = with python3Packages; [
|
||||||
setuptools
|
setuptools
|
||||||
];
|
];
|
||||||
|
|
||||||
dependencies = with python3.pkgs; [
|
dependencies = with python3Packages; [
|
||||||
tabulate
|
tabulate
|
||||||
types-tabulate
|
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
@@ -53,22 +55,48 @@ python3.pkgs.buildPythonApplication {
|
|||||||
mv $out/bin/nixos-rebuild $out/bin/nixos-rebuild-ng
|
mv $out/bin/nixos-rebuild $out/bin/nixos-rebuild-ng
|
||||||
'';
|
'';
|
||||||
|
|
||||||
nativeCheckInputs = with python3.pkgs; [
|
nativeCheckInputs = with python3Packages; [
|
||||||
pytestCheckHook
|
pytestCheckHook
|
||||||
mypy
|
|
||||||
ruff
|
|
||||||
];
|
];
|
||||||
|
|
||||||
pytestFlagsArray = [ "-vv" ];
|
pytestFlagsArray = [ "-vv" ];
|
||||||
|
|
||||||
postCheck = ''
|
passthru =
|
||||||
echo -e "\x1b[32m## run mypy\x1b[0m"
|
let
|
||||||
mypy nixos_rebuild tests
|
python-with-pkgs = python3.withPackages (
|
||||||
echo -e "\x1b[32m## run ruff\x1b[0m"
|
ps: with ps; [
|
||||||
ruff check nixos_rebuild tests
|
mypy
|
||||||
echo -e "\x1b[32m## run ruff format\x1b[0m"
|
pytest
|
||||||
ruff format --check nixos_rebuild tests
|
ruff
|
||||||
'';
|
types-tabulate
|
||||||
|
# dependencies
|
||||||
|
tabulate
|
||||||
|
]
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShell = mkShell {
|
||||||
|
packages = [ python-with-pkgs ];
|
||||||
|
shellHook = ''
|
||||||
|
cd pkgs/by-name/ni/nixos-rebuild-ng/src || true
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# NOTE: this is a passthru test rather than a build-time test because we
|
||||||
|
# want to keep the build closures small
|
||||||
|
tests.ci = runCommand "${pname}-ci" { nativeBuildInputs = [ python-with-pkgs ]; } ''
|
||||||
|
export RUFF_CACHE_DIR="$(mktemp -d)"
|
||||||
|
|
||||||
|
echo -e "\x1b[32m## run mypy\x1b[0m"
|
||||||
|
mypy ${src}
|
||||||
|
echo -e "\x1b[32m## run ruff\x1b[0m"
|
||||||
|
ruff check ${src}
|
||||||
|
echo -e "\x1b[32m## run ruff format\x1b[0m"
|
||||||
|
ruff format --check ${src}
|
||||||
|
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "Rebuild your NixOS configuration and switch to it, on local hosts and remote";
|
description = "Rebuild your NixOS configuration and switch to it, on local hosts and remote";
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import sys
|
|||||||
from subprocess import run
|
from subprocess import run
|
||||||
from typing import assert_never
|
from typing import assert_never
|
||||||
|
|
||||||
from tabulate import tabulate
|
|
||||||
|
|
||||||
from .models import Action, Flake, NRError, Profile
|
from .models import Action, Flake, NRError, Profile
|
||||||
from .nix import (
|
from .nix import (
|
||||||
edit,
|
edit,
|
||||||
|
find_file,
|
||||||
|
get_nixpkgs_rev,
|
||||||
list_generations,
|
list_generations,
|
||||||
nixos_build,
|
nixos_build,
|
||||||
nixos_build_flake,
|
nixos_build_flake,
|
||||||
@@ -88,7 +88,20 @@ def execute(argv: list[str]) -> None:
|
|||||||
if args.upgrade or args.upgrade_all:
|
if args.upgrade or args.upgrade_all:
|
||||||
upgrade_channels(bool(args.upgrade_all))
|
upgrade_channels(bool(args.upgrade_all))
|
||||||
|
|
||||||
match action := Action(args.action):
|
action = Action(args.action)
|
||||||
|
# Only run shell scripts from the Nixpkgs tree if the action is
|
||||||
|
# "switch", "boot", or "test". With other actions (such as "build"),
|
||||||
|
# the user may reasonably expect that no code from the Nixpkgs tree is
|
||||||
|
# executed, so it's safe to run nixos-rebuild against a potentially
|
||||||
|
# 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)
|
||||||
|
rev = get_nixpkgs_rev(nixpkgs_path)
|
||||||
|
if nixpkgs_path and rev:
|
||||||
|
(nixpkgs_path / ".version-suffix").write_text(rev)
|
||||||
|
|
||||||
|
match action:
|
||||||
case Action.SWITCH | Action.BOOT:
|
case Action.SWITCH | Action.BOOT:
|
||||||
info("building the system configuration...")
|
info("building the system configuration...")
|
||||||
if args.rollback:
|
if args.rollback:
|
||||||
@@ -177,6 +190,8 @@ def execute(argv: list[str]) -> None:
|
|||||||
if args.json:
|
if args.json:
|
||||||
print(json.dumps(generations, indent=2))
|
print(json.dumps(generations, indent=2))
|
||||||
else:
|
else:
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"generation": "Generation",
|
"generation": "Generation",
|
||||||
"date": "Build-date",
|
"date": "Build-date",
|
||||||
@@ -216,7 +231,3 @@ def main() -> None:
|
|||||||
raise ex
|
raise ex
|
||||||
else:
|
else:
|
||||||
sys.exit(str(ex))
|
sys.exit(str(ex))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from . import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -46,9 +46,7 @@ class Action(Enum):
|
|||||||
class Flake:
|
class Flake:
|
||||||
path: Path
|
path: Path
|
||||||
attr: str
|
attr: str
|
||||||
_re: ClassVar[re.Pattern[str]] = re.compile(
|
_re: ClassVar = re.compile(r"^(?P<path>[^\#]*)\#?(?P<attr>[^\#\"]*)$")
|
||||||
r"^(?P<path>[^\#]*)\#?(?P<attr>[^\#\"]*)$"
|
|
||||||
)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -59,11 +57,8 @@ class Flake:
|
|||||||
m = cls._re.match(flake_str)
|
m = cls._re.match(flake_str)
|
||||||
assert m is not None, f"got no matches for {flake_str}"
|
assert m is not None, f"got no matches for {flake_str}"
|
||||||
attr = m.group("attr")
|
attr = m.group("attr")
|
||||||
if not attr:
|
nixos_attr = f"nixosConfigurations.{attr or hostname or "default"}"
|
||||||
attr = f"nixosConfigurations.{hostname or "default"}"
|
return Flake(Path(m.group("path")), nixos_attr)
|
||||||
else:
|
|
||||||
attr = f"nixosConfigurations.{attr}"
|
|
||||||
return Flake(Path(m.group("path")), attr)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_arg(cls, flake_arg: Any) -> Flake | None:
|
def from_arg(cls, flake_arg: Any) -> Flake | None:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE, CalledProcessError, run
|
from subprocess import PIPE, CalledProcessError, run
|
||||||
@@ -14,7 +15,7 @@ from .models import (
|
|||||||
NRError,
|
NRError,
|
||||||
Profile,
|
Profile,
|
||||||
)
|
)
|
||||||
from .utils import dict_to_flags
|
from .utils import dict_to_flags, info
|
||||||
|
|
||||||
FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"]
|
FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"]
|
||||||
|
|
||||||
@@ -48,6 +49,49 @@ def edit(flake: Flake | None, nix_flags: list[str] | None = None) -> None:
|
|||||||
raise NRError("cannot find NixOS config file")
|
raise NRError("cannot find NixOS config file")
|
||||||
|
|
||||||
|
|
||||||
|
def find_file(file: str, nix_flags: list[str] | None = None) -> Path | None:
|
||||||
|
"Find classic Nixpkgs location."
|
||||||
|
r = run(
|
||||||
|
["nix-instantiate", "--find-file", file, *(nix_flags or [])],
|
||||||
|
stdout=PIPE,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if r.returncode:
|
||||||
|
return None
|
||||||
|
return Path(r.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def get_nixpkgs_rev(nixpkgs_path: Path | None) -> str | None:
|
||||||
|
"""Get Nixpkgs path as a Git revision.
|
||||||
|
|
||||||
|
Can be used to generate `.version-suffix` file."""
|
||||||
|
if not nixpkgs_path:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Git is not included in the closure for nixos-rebuild so we need to check
|
||||||
|
if not shutil.which("git"):
|
||||||
|
info(f"warning: Git not found; cannot figure out revision of '{nixpkgs_path}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get current revision
|
||||||
|
r = run(
|
||||||
|
["git", "-C", nixpkgs_path, "rev-parse", "--short", "HEAD"],
|
||||||
|
check=False,
|
||||||
|
stdout=PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
rev = r.stdout.strip()
|
||||||
|
|
||||||
|
if rev:
|
||||||
|
# Check if repo is dirty
|
||||||
|
if run(["git", "-C", nixpkgs_path, "diff", "--quiet"], check=False).returncode:
|
||||||
|
rev += "M"
|
||||||
|
return f".git.{rev}"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _parse_generation_from_nix_store(path: Path, profile: Profile) -> Generation:
|
def _parse_generation_from_nix_store(path: Path, profile: Profile) -> Generation:
|
||||||
entry_id = path.name.split("-")[1]
|
entry_id = path.name.split("-")[1]
|
||||||
current = path.name == profile.path.readlink().name
|
current = path.name == profile.path.readlink().name
|
||||||
|
|||||||
@@ -67,10 +67,17 @@ def test_parse_args() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@patch(get_qualified_name(nr.nix.run, nr.nix), autospec=True)
|
@patch(get_qualified_name(nr.nix.run, nr.nix), autospec=True)
|
||||||
def test_execute_nix_boot(mock_run: Any, tmp_path: Path) -> None:
|
@patch(get_qualified_name(nr.nix.shutil.which), autospec=True, return_value="/bin/git")
|
||||||
|
def test_execute_nix_boot(mock_which: Any, mock_run: Any, tmp_path: Path) -> None:
|
||||||
|
nixpkgs_path = tmp_path / "nixpkgs"
|
||||||
|
nixpkgs_path.mkdir()
|
||||||
config_path = tmp_path / "test"
|
config_path = tmp_path / "test"
|
||||||
config_path.touch()
|
config_path.touch()
|
||||||
mock_run.side_effect = [
|
mock_run.side_effect = [
|
||||||
|
# update_nixpkgs_rev
|
||||||
|
CompletedProcess([], 0, str(nixpkgs_path)),
|
||||||
|
CompletedProcess([], 0, "nixpkgs-rev"),
|
||||||
|
CompletedProcess([], 0),
|
||||||
# nixos_build
|
# nixos_build
|
||||||
CompletedProcess([], 0, str(config_path)),
|
CompletedProcess([], 0, str(config_path)),
|
||||||
# set_profile
|
# set_profile
|
||||||
@@ -82,9 +89,25 @@ def test_execute_nix_boot(mock_run: Any, tmp_path: Path) -> None:
|
|||||||
nr.execute(["nixos-rebuild", "boot", "--no-flake", "-vvv"])
|
nr.execute(["nixos-rebuild", "boot", "--no-flake", "-vvv"])
|
||||||
|
|
||||||
assert nr.VERBOSE is True
|
assert nr.VERBOSE is True
|
||||||
assert mock_run.call_count == 3
|
assert mock_run.call_count == 6
|
||||||
mock_run.assert_has_calls(
|
mock_run.assert_has_calls(
|
||||||
[
|
[
|
||||||
|
call(
|
||||||
|
["nix-instantiate", "--find-file", "nixpkgs", "-vvv"],
|
||||||
|
stdout=PIPE,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
["git", "-C", nixpkgs_path, "rev-parse", "--short", "HEAD"],
|
||||||
|
check=False,
|
||||||
|
stdout=PIPE,
|
||||||
|
text=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
["git", "-C", nixpkgs_path, "diff", "--quiet"],
|
||||||
|
check=False,
|
||||||
|
),
|
||||||
call(
|
call(
|
||||||
[
|
[
|
||||||
"nix-build",
|
"nix-build",
|
||||||
@@ -180,11 +203,15 @@ def test_execute_nix_switch_flake(mock_run: Any, tmp_path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@patch(get_qualified_name(nr.nix.run, nr.nix), autospec=True)
|
@patch(get_qualified_name(nr.nix.run, nr.nix), autospec=True)
|
||||||
def test_execute_switch_rollback(mock_run: Any) -> None:
|
def test_execute_switch_rollback(mock_run: Any, tmp_path: Path) -> None:
|
||||||
|
nixpkgs_path = tmp_path / "nixpkgs"
|
||||||
|
nixpkgs_path.touch()
|
||||||
|
|
||||||
nr.execute(["nixos-rebuild", "switch", "--rollback", "--install-bootloader"])
|
nr.execute(["nixos-rebuild", "switch", "--rollback", "--install-bootloader"])
|
||||||
|
|
||||||
assert nr.VERBOSE is False
|
assert nr.VERBOSE is False
|
||||||
assert mock_run.call_count == 2
|
assert mock_run.call_count == 3
|
||||||
|
# ignoring update_nixpkgs_rev calls
|
||||||
mock_run.assert_has_calls(
|
mock_run.assert_has_calls(
|
||||||
[
|
[
|
||||||
call(
|
call(
|
||||||
|
|||||||
@@ -42,6 +42,61 @@ def test_edit(mock_run: Any, monkeypatch: Any, tmpdir: Any) -> None:
|
|||||||
mock_run.assert_called_with(["editor", default_nix], check=False)
|
mock_run.assert_called_with(["editor", default_nix], check=False)
|
||||||
|
|
||||||
|
|
||||||
|
@patch(get_qualified_name(n.shutil.which), autospec=True, return_value="/bin/git")
|
||||||
|
def test_get_nixpkgs_rev(mock_which: Any) -> None:
|
||||||
|
assert n.get_nixpkgs_rev(None) is None
|
||||||
|
|
||||||
|
path = Path("/path/to/nix")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
get_qualified_name(n.run, n),
|
||||||
|
autospec=True,
|
||||||
|
side_effect=[CompletedProcess([], 0, "")],
|
||||||
|
) as mock_run:
|
||||||
|
assert n.get_nixpkgs_rev(path) is None
|
||||||
|
mock_run.assert_called_with(
|
||||||
|
["git", "-C", path, "rev-parse", "--short", "HEAD"],
|
||||||
|
check=False,
|
||||||
|
stdout=PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_calls = [
|
||||||
|
call(
|
||||||
|
["git", "-C", path, "rev-parse", "--short", "HEAD"],
|
||||||
|
check=False,
|
||||||
|
stdout=PIPE,
|
||||||
|
text=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
["git", "-C", path, "diff", "--quiet"],
|
||||||
|
check=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
get_qualified_name(n.run, n),
|
||||||
|
autospec=True,
|
||||||
|
side_effect=[
|
||||||
|
CompletedProcess([], 0, "0f7c82403fd6"),
|
||||||
|
CompletedProcess([], returncode=0),
|
||||||
|
],
|
||||||
|
) as mock_run:
|
||||||
|
assert n.get_nixpkgs_rev(path) == ".git.0f7c82403fd6"
|
||||||
|
mock_run.assert_has_calls(expected_calls)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
get_qualified_name(n.run, n),
|
||||||
|
autospec=True,
|
||||||
|
side_effect=[
|
||||||
|
CompletedProcess([], 0, "0f7c82403fd6"),
|
||||||
|
CompletedProcess([], returncode=1),
|
||||||
|
],
|
||||||
|
) as mock_run:
|
||||||
|
assert n.get_nixpkgs_rev(path) == ".git.0f7c82403fd6M"
|
||||||
|
mock_run.assert_has_calls(expected_calls)
|
||||||
|
|
||||||
|
|
||||||
def test_get_generations_from_nix_store(tmp_path: Path) -> None:
|
def test_get_generations_from_nix_store(tmp_path: Path) -> None:
|
||||||
nixos_path = tmp_path / "nixos-system"
|
nixos_path = tmp_path / "nixos-system"
|
||||||
nixos_path.mkdir()
|
nixos_path.mkdir()
|
||||||
|
|||||||
Reference in New Issue
Block a user