mirror of
https://gitlab.com/khumba/nvd.git
synced 2024-09-20 08:15:04 +02:00
Initial commit.
This commit is contained in:
commit
3e619dafcc
2 changed files with 602 additions and 0 deletions
538
nvd
Executable file
538
nvd
Executable file
|
@ -0,0 +1,538 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# TODO Closure disk size change.
|
||||
|
||||
# Package install state (by pname):
|
||||
# A: Added to closure
|
||||
# R: Removed from closure
|
||||
# U: Upgrade
|
||||
# D: Downgrade
|
||||
# C: Changed versions or selection state
|
||||
|
||||
# Package selection state (by pname):
|
||||
# +: Package is newly selected in environment.systemPackages.
|
||||
# -: Package is newly unselected.
|
||||
# *: Package is selected; state unchanged.
|
||||
# .: Not in environment.systemPackages; dependency.
|
||||
# l: Package manifest only available on the left; not selected there.
|
||||
# L: Package manifest only available on the left; selected there.
|
||||
# r: Package manifest only available on the right; not selected there.
|
||||
# R: Package manifest only available on the right; selected there.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Nix/NixOS package version diff tool",
|
||||
epilog="See the nvd(1) manual page for more information.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--color",
|
||||
default="auto",
|
||||
help="Controls use of colour; one of 'auto', 'never', 'always'.")
|
||||
parser.add_argument(
|
||||
"root1",
|
||||
help="The first Nix store ptah to compare.")
|
||||
parser.add_argument(
|
||||
"root2",
|
||||
help="The second Nix store path to compare.")
|
||||
|
||||
ARGS = parser.parse_args()
|
||||
|
||||
USE_COLOUR = ARGS.color == "always" or (ARGS.color == "auto" and sys.stdout.isatty())
|
||||
|
||||
SGR_RESET = 0
|
||||
SGR_BOLD = 1
|
||||
SGR_FG = 30
|
||||
SGR_BG = 40
|
||||
SGR_BRIGHT = 60 # Add to SGR_FG or SGR_BG.
|
||||
SGR_BLACK = 0
|
||||
SGR_RED = 1
|
||||
SGR_GREEN = 2
|
||||
SGR_YELLOW = 3
|
||||
SGR_BLUE = 4
|
||||
SGR_MAGENTA = 5
|
||||
SGR_CYAN = 6
|
||||
|
||||
def sgr(*args):
|
||||
assert all(isinstance(arg, int) for arg in args), \
|
||||
"sgr() args must all be integers: {args!r}"
|
||||
if not USE_COLOUR:
|
||||
return ""
|
||||
return "\x1b[" + ";".join(str(arg) for arg in args) + "m"
|
||||
|
||||
INST_ADDED = sgr(SGR_FG + SGR_BRIGHT + SGR_GREEN) + "A"
|
||||
INST_REMOVED = sgr(SGR_FG + SGR_BRIGHT + SGR_RED) + "R"
|
||||
INST_UPGRADED = sgr(SGR_FG + SGR_BRIGHT + SGR_CYAN) + "U"
|
||||
INST_DOWNGRADED = sgr(SGR_FG + SGR_BRIGHT + SGR_YELLOW) + "D"
|
||||
INST_CHANGED = sgr(SGR_FG + SGR_BRIGHT + SGR_MAGENTA) + "C"
|
||||
|
||||
SEL_SELECTED = "*"
|
||||
SEL_UNSELECTED = "."
|
||||
SEL_NEWLY_SELECTED = "+"
|
||||
SEL_NEWLY_UNSELECTED = "-"
|
||||
SEL_LEFT_ONLY_SELECTED = "L"
|
||||
SEL_LEFT_ONLY_UNSELECTED = "l"
|
||||
SEL_RIGHT_ONLY_SELECTED = "R"
|
||||
SEL_RIGHT_ONLY_UNSELECTED = "r"
|
||||
SEL_NO_MANIFESTS = ""
|
||||
|
||||
NIX_STORE_PATH_REGEX = re.compile(r"^/nix/store/[a-z0-9]+-(.+?)(-([0-9].*?))?(\.drv)?$")
|
||||
|
||||
def raise_arg(e):
|
||||
raise e
|
||||
|
||||
class StorePath:
|
||||
def __init__(self, path: Union[str, Path]):
|
||||
assert str(path).startswith("/nix/store/"), \
|
||||
f"Doesn't start with /nix/store/: {str(resolved)!r}"
|
||||
self._path = Path(path)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<StorePath: {self._path}>"
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._path)
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(other) is StorePath and self._path == other._path
|
||||
|
||||
def path(self) -> Path:
|
||||
return self._path
|
||||
|
||||
def to_base_path(self) -> "StorePath":
|
||||
return StorePath(os.path.join(*self._path.parts[0:4]))
|
||||
|
||||
# For the version comparison algorithm, see:
|
||||
# https://nixos.org/manual/nix/stable/#ssec-version-comparisons
|
||||
|
||||
class VersionChunk:
|
||||
def __init__(self, chunk_value: Union[int, str]):
|
||||
assert isinstance(chunk_value, int) or isinstance(chunk_value, str)
|
||||
self._chunk_value = chunk_value
|
||||
|
||||
def __str__(self):
|
||||
return repr(self._chunk_value)
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, VersionChunk):
|
||||
return NotImplemented
|
||||
|
||||
x = self._chunk_value
|
||||
y = other._chunk_value
|
||||
x_int = isinstance(x, int)
|
||||
y_int = isinstance(y, int)
|
||||
if x_int and y_int:
|
||||
return x < y
|
||||
elif (x == "" and y_int) or x == "pre":
|
||||
return True
|
||||
elif (x_int and y == "") or y == "pre":
|
||||
return False
|
||||
elif x_int: # y is a string
|
||||
return False
|
||||
elif y_int: # x is a string
|
||||
return True
|
||||
else: # both are strings
|
||||
return x < y
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, VersionChunk):
|
||||
return NotImplemented
|
||||
|
||||
return self._chunk_value == other._chunk_value
|
||||
|
||||
class Version:
|
||||
def __init__(self, text: Optional[str]):
|
||||
if text is None:
|
||||
text = ""
|
||||
|
||||
self._text = text
|
||||
|
||||
self._chunks: List[VersionChunk] = []
|
||||
while text != "":
|
||||
first_char = text[0]
|
||||
if first_char.isdigit():
|
||||
last = 0
|
||||
while last + 1 < len(text) and text[last + 1].isdigit():
|
||||
last += 1
|
||||
self._chunks.append(VersionChunk(int(text[0 : last + 1])))
|
||||
text = text[last + 1:]
|
||||
elif first_char.isalpha():
|
||||
last = 0
|
||||
while last + 1 < len(text) and text[last + 1].isalpha():
|
||||
last += 1
|
||||
self._chunks.append(VersionChunk(text[0 : last + 1]))
|
||||
text = text[last + 1:]
|
||||
else:
|
||||
last = 0
|
||||
while last + 1 < len(text) and not text[last + 1].isdigit() and not text[last + 1].isalpha():
|
||||
last += 1
|
||||
# No chunk append here, only care about alnum runs.
|
||||
text = text[last + 1:]
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Version):
|
||||
return NotImplemented
|
||||
return self._chunks == other._chunks
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, Version):
|
||||
return NotImplemented
|
||||
return self._chunks < other._chunks
|
||||
|
||||
class Package:
|
||||
def __init__(self, *, pname: str, version: Version, store_path: StorePath):
|
||||
assert isinstance(pname, str), f"Not a string: {pname!r}"
|
||||
assert isinstance(version, Version), f"Not a Version: {version!r}"
|
||||
assert isinstance(store_path, StorePath), f"Not a StorePath: {store_path!r}"
|
||||
self._pname = pname
|
||||
self._version = version
|
||||
self._store_path = store_path
|
||||
|
||||
def pname(self) -> str:
|
||||
return self._pname
|
||||
|
||||
def version(self) -> Version:
|
||||
return self._version
|
||||
|
||||
def store_path(self) -> StorePath:
|
||||
return self._store_path
|
||||
|
||||
class PackageManifest:
|
||||
def __init__(self, packages: List[Package]):
|
||||
assert isinstance(packages, List)
|
||||
assert all(isinstance(package, Package) for package in packages)
|
||||
|
||||
self._packages = packages
|
||||
|
||||
self._packages_by_pname: Dict[str, List[Package]] = {}
|
||||
for entry in packages:
|
||||
self._packages_by_pname.setdefault(entry.pname(), []).append(entry)
|
||||
|
||||
@staticmethod
|
||||
def parse_tree(root: Path) -> "PackageManifest":
|
||||
assert isinstance(root, Path), f"Not a Path: {root!r}"
|
||||
|
||||
store_paths = set()
|
||||
for curdir, dirs, files in os.walk(root, onerror=raise_arg):
|
||||
for filename in files:
|
||||
path_str = os.path.join(curdir, filename)
|
||||
if os.path.islink(path_str):
|
||||
store_paths.add(StorePath(os.readlink(path_str)).to_base_path())
|
||||
|
||||
packages = []
|
||||
for store_path in store_paths:
|
||||
pname, version = parse_pname_version(str(store_path.path()))
|
||||
packages.append(Package(
|
||||
pname=pname,
|
||||
version=Version(version),
|
||||
store_path=store_path,
|
||||
))
|
||||
|
||||
return PackageManifest(packages)
|
||||
|
||||
def contains_pname(self, pname: str) -> bool:
|
||||
return pname in self._packages_by_pname
|
||||
|
||||
class PackageManifestPair:
|
||||
def __init__(
|
||||
self,
|
||||
left_manifest: Optional[PackageManifest],
|
||||
right_manifest: Optional[PackageManifest]
|
||||
):
|
||||
assert left_manifest is None or isinstance(left_manifest, PackageManifest)
|
||||
assert right_manifest is None or isinstance(right_manifest, PackageManifest)
|
||||
self._left_manifest = left_manifest
|
||||
self._right_manifest = right_manifest
|
||||
|
||||
def get_left_manifest(self) -> Optional[PackageManifest]:
|
||||
return self._left_manifest
|
||||
|
||||
def get_right_manifest(self) -> Optional[PackageManifest]:
|
||||
return self._right_manifest
|
||||
|
||||
def get_selection_state(self, pname: str) -> str:
|
||||
left_manifest = self._left_manifest
|
||||
right_manifest = self._right_manifest
|
||||
|
||||
if left_manifest is not None and right_manifest is not None:
|
||||
in_left_manifest = left_manifest.contains_pname(pname)
|
||||
in_right_manifest = right_manifest.contains_pname(pname)
|
||||
selection_state_str = [
|
||||
SEL_UNSELECTED,
|
||||
SEL_NEWLY_UNSELECTED,
|
||||
SEL_NEWLY_SELECTED,
|
||||
SEL_SELECTED,
|
||||
][int(in_left_manifest) + 2 * int(in_right_manifest)]
|
||||
elif left_manifest is not None:
|
||||
in_left_manifest = left_manifest.contains_pname(pname)
|
||||
selection_state_str = [
|
||||
SEL_LEFT_ONLY_UNSELECTED,
|
||||
SEL_LEFT_ONLY_SELECTED,
|
||||
][int(in_left_manifest)]
|
||||
elif right_manifest is not None:
|
||||
in_right_manifest = right_manifest.contains_pname(pname)
|
||||
selection_state_str = [
|
||||
SEL_RIGHT_ONLY_UNSELECTED,
|
||||
SEL_RIGHT_ONLY_SELECTED,
|
||||
][int(in_right_manifest)]
|
||||
else:
|
||||
selection_state_str = SEL_NO_MANIFESTS
|
||||
|
||||
return selection_state_str
|
||||
|
||||
def is_selection_state_changed(self, pname: str) -> str:
|
||||
in_left_manifest = \
|
||||
self._left_manifest is not None and \
|
||||
self._left_manifest.contains_pname(pname)
|
||||
in_right_manifest = \
|
||||
self._right_manifest is not None and \
|
||||
self._right_manifest.contains_pname(pname)
|
||||
return in_left_manifest != in_right_manifest
|
||||
|
||||
def parse_pname_version(path: str) -> Tuple[str, str]:
|
||||
base_path = str(StorePath(path).to_base_path().path())
|
||||
|
||||
match = NIX_STORE_PATH_REGEX.search(base_path)
|
||||
assert match is not None, f"Couldn't parse path: {path}"
|
||||
|
||||
pname = match.group(1)
|
||||
version = match.group(3)
|
||||
|
||||
return pname, version
|
||||
|
||||
def closure_paths_to_map(paths: List[str]) -> Dict[str, List[str]]:
|
||||
result = {}
|
||||
|
||||
for path in paths:
|
||||
name, version = parse_pname_version(path)
|
||||
if name not in result:
|
||||
result[name] = [version]
|
||||
else:
|
||||
result[name].append(version)
|
||||
|
||||
for version_list in result.values():
|
||||
version_list.sort(key=lambda ver: ver or "")
|
||||
|
||||
return result
|
||||
|
||||
def render_versions(version_list: List[str], *, colour: bool = False) -> str:
|
||||
if version_list == []:
|
||||
return "<no versions>"
|
||||
|
||||
items = []
|
||||
|
||||
version_list = list(version_list) # Create a copy we can modify.
|
||||
while version_list != []:
|
||||
version = version_list.pop()
|
||||
count = 1
|
||||
while version_list != [] and version_list[-1] == version:
|
||||
version_list.pop()
|
||||
count += 1
|
||||
|
||||
if version == None:
|
||||
version = "<none>"
|
||||
elif colour: # (Don't apply colour to <none>.)
|
||||
version = sgr(SGR_FG + SGR_YELLOW) + version + sgr(SGR_RESET)
|
||||
|
||||
if count == 1:
|
||||
items.append(version)
|
||||
else:
|
||||
items.append(version + f" x{count}")
|
||||
|
||||
items.reverse()
|
||||
return ", ".join(items)
|
||||
|
||||
def print_package_list(
|
||||
*,
|
||||
pnames: List[str],
|
||||
manifest_pair: PackageManifestPair,
|
||||
left_versions_map: Dict[str, List[str]],
|
||||
right_versions_map: Optional[Dict[str, List[str]]] = None,
|
||||
fixed_install_state: Optional[str] = None,
|
||||
):
|
||||
assert (right_versions_map is None) != (fixed_install_state is None), \
|
||||
"Expect either one versions map, or a fixed install state."
|
||||
have_single_version = right_versions_map is None
|
||||
|
||||
count = 1
|
||||
count_width = len(str(len(pnames)))
|
||||
count_format_str = "#{:0" + str(count_width) + "d}"
|
||||
pname_width = max(len(pname) for pname in pnames)
|
||||
pname_format_str = "{:" + str(pname_width) + "}"
|
||||
|
||||
for pname in pnames:
|
||||
left_versions = left_versions_map[pname]
|
||||
|
||||
if have_single_version:
|
||||
install_state_str = fixed_install_state
|
||||
else:
|
||||
right_versions = right_versions_map[pname] if right_versions_map is not None else None
|
||||
# TODO Terrible - make this efficient.
|
||||
lv_parsed = [Version(v) for v in left_versions]
|
||||
rv_parsed = [Version(v) for v in right_versions]
|
||||
if all(lv < rv for lv in lv_parsed for rv in rv_parsed):
|
||||
install_state_str = INST_UPGRADED
|
||||
elif all(lv > rv for lv in lv_parsed for rv in rv_parsed):
|
||||
install_state_str = INST_DOWNGRADED
|
||||
else:
|
||||
install_state_str = INST_CHANGED
|
||||
|
||||
selected_at_all = (
|
||||
manifest_pair.get_left_manifest() is not None
|
||||
and manifest_pair.get_left_manifest().contains_pname(pname)
|
||||
) or (
|
||||
manifest_pair.get_right_manifest() is not None
|
||||
and manifest_pair.get_right_manifest().contains_pname(pname)
|
||||
)
|
||||
if selected_at_all:
|
||||
bold_if_selected_sgr = sgr(SGR_BOLD)
|
||||
pname_sgr = bold_if_selected_sgr + sgr(SGR_FG + SGR_GREEN + SGR_BRIGHT)
|
||||
else:
|
||||
bold_if_selected_sgr = ""
|
||||
pname_sgr = bold_if_selected_sgr + sgr(SGR_FG + SGR_GREEN)
|
||||
|
||||
status_str = "[{}{}]".format(
|
||||
bold_if_selected_sgr + install_state_str + sgr(SGR_RESET),
|
||||
pname_sgr + manifest_pair.get_selection_state(pname) + sgr(SGR_RESET),
|
||||
)
|
||||
count_str = count_format_str.format(count)
|
||||
pname_str = pname_format_str.format(pname)
|
||||
print("{} {} {} {}{}".format(
|
||||
status_str,
|
||||
count_str,
|
||||
pname_sgr + pname_str + sgr(SGR_RESET),
|
||||
render_versions(left_versions, colour=True),
|
||||
"" if have_single_version else (" -> " + render_versions(right_versions, colour=True)),
|
||||
))
|
||||
count += 1
|
||||
|
||||
def main():
|
||||
left_path = Path(ARGS.root1)
|
||||
right_path = Path(ARGS.root2)
|
||||
|
||||
if not left_path.exists():
|
||||
sys.stderr.write(f"Path does not exist: {left_path}\n")
|
||||
sys.exit(1)
|
||||
|
||||
if not right_path.exists():
|
||||
sys.stderr.write(f"Path does not exist: {right_path}\n")
|
||||
sys.exit(1)
|
||||
|
||||
left_resolved = left_path.resolve()
|
||||
right_resolved = right_path.resolve()
|
||||
|
||||
print(f"<<< {left_path}")
|
||||
print(f">>> {right_path}")
|
||||
|
||||
left_manifest: Optional[PackageManifest] = None
|
||||
right_manifest: Optional[PackageManifest] = None
|
||||
|
||||
if (left_resolved / "sw").is_dir() and (right_resolved / "sw").is_dir():
|
||||
left_manifest = PackageManifest.parse_tree(left_resolved / "sw")
|
||||
right_manifest = PackageManifest.parse_tree(right_resolved / "sw")
|
||||
else:
|
||||
left_manifest = PackageManifest.parse_tree(left_resolved)
|
||||
right_manifest = PackageManifest.parse_tree(right_resolved)
|
||||
|
||||
manifest_pair = PackageManifestPair(left_manifest, right_manifest)
|
||||
|
||||
left_closure_paths: List[str] = \
|
||||
subprocess.run(["nix-store", "-qR", str(left_resolved)], stdout=PIPE, text=True) \
|
||||
.stdout.rstrip("\n").split("\n")
|
||||
right_closure_paths: List[str] = \
|
||||
subprocess.run(["nix-store", "-qR", (right_resolved)], stdout=PIPE, text=True) \
|
||||
.stdout.rstrip("\n").split("\n")
|
||||
|
||||
# Maps from pname to lists of versions.
|
||||
left_closure_map: Dict[str, List[str]] = closure_paths_to_map(left_closure_paths)
|
||||
right_closure_map: Dict[str, List[str]] = closure_paths_to_map(right_closure_paths)
|
||||
|
||||
left_package_names = set(left_closure_map.keys())
|
||||
right_package_names = set(right_closure_map.keys())
|
||||
|
||||
common_package_names = sorted(left_package_names & right_package_names)
|
||||
left_only_package_names = sorted(left_package_names - right_package_names)
|
||||
right_only_package_names = sorted(right_package_names - left_package_names)
|
||||
|
||||
# Announce version changes.
|
||||
package_names_with_changed_versions = []
|
||||
package_names_with_changed_selection_states = []
|
||||
for pname in common_package_names:
|
||||
if left_closure_map[pname] != right_closure_map[pname]:
|
||||
package_names_with_changed_versions.append(pname)
|
||||
elif manifest_pair.is_selection_state_changed(pname):
|
||||
package_names_with_changed_selection_states.append(pname)
|
||||
|
||||
any_changes_displayed = False
|
||||
|
||||
# Announce version changes.
|
||||
if package_names_with_changed_versions != []:
|
||||
any_changes_displayed = True
|
||||
print("Version changes:")
|
||||
print_package_list(
|
||||
pnames=package_names_with_changed_versions,
|
||||
manifest_pair=manifest_pair,
|
||||
left_versions_map=left_closure_map,
|
||||
right_versions_map=right_closure_map,
|
||||
)
|
||||
|
||||
# Announce specific changes for packages whose versions haven't changed.
|
||||
if package_names_with_changed_selection_states != []:
|
||||
any_changes_displayed = True
|
||||
print("Selection state changes:")
|
||||
print_package_list(
|
||||
pnames=package_names_with_changed_selection_states,
|
||||
manifest_pair=manifest_pair,
|
||||
left_versions_map=left_closure_map,
|
||||
fixed_install_state=INST_CHANGED,
|
||||
)
|
||||
|
||||
# Announce added packages.
|
||||
if right_only_package_names != []:
|
||||
any_changes_displayed = True
|
||||
print("Added packages:")
|
||||
print_package_list(
|
||||
pnames=right_only_package_names,
|
||||
manifest_pair=manifest_pair,
|
||||
left_versions_map=right_closure_map, # Yes, this is correct.
|
||||
fixed_install_state=INST_ADDED,
|
||||
)
|
||||
|
||||
# Announce removed packages.
|
||||
if left_only_package_names != []:
|
||||
any_changes_displayed = True
|
||||
print("Removed packages:")
|
||||
print_package_list(
|
||||
pnames=left_only_package_names,
|
||||
manifest_pair=manifest_pair,
|
||||
left_versions_map=left_closure_map,
|
||||
fixed_install_state=INST_REMOVED,
|
||||
)
|
||||
|
||||
if not any_changes_displayed:
|
||||
print("No version or selection state changes.")
|
||||
|
||||
left_closure_paths_set = set(left_closure_paths)
|
||||
right_closure_paths_set = set(right_closure_paths)
|
||||
left_only_paths_count = len(left_closure_paths_set - right_closure_paths_set)
|
||||
right_only_paths_count = len(right_closure_paths_set - left_closure_paths_set)
|
||||
|
||||
print(
|
||||
f"Closure size: {len(left_closure_paths)} -> {len(right_closure_paths)} "
|
||||
f"({right_only_paths_count} paths added, {left_only_paths_count} paths removed, "
|
||||
f"delta {right_only_paths_count - left_only_paths_count:+d})."
|
||||
)
|
||||
|
||||
# Importing this module is kinda broken because of the constant initializations
|
||||
# at the top.
|
||||
if __name__ == "__main__":
|
||||
main()
|
64
nvd.1
Normal file
64
nvd.1
Normal file
|
@ -0,0 +1,64 @@
|
|||
.TH nvd 1 2021-04-06 nvd "User Commands"
|
||||
.SH NAME
|
||||
nvd \- Nix/NixOS package version diff tool
|
||||
.SH SYNOPSIS
|
||||
.P
|
||||
.B nvd [ -h | --help ]
|
||||
.P
|
||||
.B nvd [ --color=(auto|always|never) ] root1 root2
|
||||
.SH DESCRIPTION
|
||||
.P
|
||||
.B nvd
|
||||
is a tool for diffing the versions of all store paths in the closures of two Nix
|
||||
store paths, neatly summarizing the differences. This is mainly intended for
|
||||
comparing two system configurations and is inspired by the output of
|
||||
.B emerge -pv
|
||||
from Gentoo's Portage package manager.
|
||||
.B nvd
|
||||
could also be likened to the output of Debian's
|
||||
.B apt upgrade -V,
|
||||
or any equivalent from other distributions' package managers.
|
||||
.B nvd
|
||||
isn't limited to comparing system configurations though, and can work with any
|
||||
two store paths.
|
||||
.P
|
||||
When given two system configurations,
|
||||
.B nvd
|
||||
distinguishes between packages that are explicitly included in
|
||||
.B environment.systemPackages,
|
||||
versus the rest of the packages that make up the system. The former packages
|
||||
are dubbed
|
||||
.I selected packages.
|
||||
.B nvd
|
||||
marks and bolds any selected packages it lists. Additionally,
|
||||
.B nvd
|
||||
reports on any changes to packages' selection states between the two
|
||||
configurations. This is done by looking at all packages that are referenced
|
||||
directly from the configurations' system paths, within each
|
||||
.I <system>/sw
|
||||
subdirectory. When either of the arguments isn't a system configuration,
|
||||
i.e. when either of the arguments is missing a
|
||||
.I sw
|
||||
subdirectory, then
|
||||
.I selected packages
|
||||
refer to direct dependencies of each of the arguments instead, and the
|
||||
comparison is still performed.
|
||||
.B nvd
|
||||
doesn't actually know if a Nix package represents a system configuration; it
|
||||
just uses the existence of a
|
||||
.I sw
|
||||
directory as a heuristic.
|
||||
.SH OPTIONS
|
||||
.P
|
||||
.TP
|
||||
-h, --help
|
||||
Displays a brief help message.
|
||||
.TP
|
||||
--color=(auto|always|never)
|
||||
When to display colour. The default
|
||||
.B auto
|
||||
detects whether stdout is a terminal and only uses colour if it is. Passing
|
||||
.B always
|
||||
forces colour to always be used, and passing
|
||||
.B never
|
||||
forces colour to never be used.
|
Loading…
Reference in a new issue