diff --git a/CHANGELOG.md b/CHANGELOG.md index 843c398..6bac023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.1.0 +- Add a mandatory action argument to the CLI. The existing diff functionality + is under the `diff` command. A new `list` command is now implemented as well + (issue #5). + - Optimized first level dependency calculation to read depenencies from `nix-store` rather than walking a directory tree manually (issue #4). This also fixes nvd's support for things other than simple `buildEnv`s, e.g. file diff --git a/src/nvd b/src/nvd index 32a2726..d551bc7 100755 --- a/src/nvd +++ b/src/nvd @@ -19,6 +19,7 @@ # See the nvd(1) man page for user-facing docs. # Package install state (by pname): +# I: Installed (unchanged) # A: Added to closure # R: Removed from closure # U: Upgrade @@ -36,6 +37,7 @@ # R: Package manifest only available on the right; selected there. import argparse +import fnmatch import os import os.path import re @@ -46,24 +48,7 @@ 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()) +USE_COLOUR = False SGR_RESET = 0 SGR_BOLD = 1 @@ -85,6 +70,7 @@ def sgr(*args): return "" return "\x1b[" + ";".join(str(arg) for arg in args) + "m" +INST_INSTALLED = "I" 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" @@ -257,6 +243,9 @@ class PackageManifest: def contains_pname(self, pname: str) -> bool: return pname in self._packages_by_pname + def all_pnames(self): + return self._packages_by_pname.keys() + class PackageManifestPair: def __init__( self, @@ -378,6 +367,10 @@ def print_package_list( "Expect either one versions map, or a fixed install state." have_single_version = right_versions_map is None + if not pnames: + print("No packages to display.") + return + count = 1 count_width = len(str(len(pnames))) count_format_str = "#{:0" + str(count_width) + "d}" @@ -430,9 +423,51 @@ def print_package_list( )) count += 1 -def main(): - left_path = Path(ARGS.root1) - right_path = Path(ARGS.root2) +def run_list(*, root, only_selected, name_patterns): + path = Path(root) + + if not path.exists(): + sys.stderr.write(f"Path does not exist: {path}\n") + sys.exit(1) + + path = path.resolve() + if (path / "sw").is_dir(): + manifest = PackageManifest.parse_tree((path / "sw").resolve()) + else: + manifest = PackageManifest.parse_tree(path / "sw") + + closure_paths: List[str] = subprocess.run( + ["nix-store", "-qR", str(path)], + stdout=PIPE, + text=True, + ).stdout.rstrip("\n").split("\n") + + closure_map: Dict[str, List[str]] = closure_paths_to_map(closure_paths) + + if only_selected: + target_pnames = list(manifest.all_pnames()) + else: + target_pnames = list(closure_map.keys()) + + target_pnames = [ + pname + for pname in target_pnames + if all(fnmatch.fnmatch(pname, pattern) for pattern in name_patterns) + ] + + target_pnames.sort() + + print_package_list( + pnames=target_pnames, + manifest_pair=PackageManifestPair(manifest, manifest), + left_versions_map=closure_map, + right_versions_map=None, + fixed_install_state=INST_INSTALLED, + ) + +def run_diff(*, root1, root2): + left_path = Path(root1) + right_path = Path(root2) if not left_path.exists(): sys.stderr.write(f"Path does not exist: {left_path}\n") @@ -547,6 +582,67 @@ def main(): f"delta {right_only_paths_count - left_only_paths_count:+d})." ) +def parse_args(): + 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'.") + + subparsers = parser.add_subparsers(dest="action") + + diff_parser = subparsers.add_parser( + "diff", + help="Diff two Nix store paths and their closures.") + diff_parser.add_argument( + "root1", + help="The first Nix store path to compare.") + diff_parser.add_argument( + "root2", + help="The second Nix store path to compare.") + + list_parser = subparsers.add_parser( + "list", + help="List packages in the closure of a Nix store path.") + list_parser.add_argument( + "-r", "--root", + default="/run/current-system", + help="The The Nix store path to work with.") + list_parser.add_argument( + "-s", "--selected", + dest="only_selected", + action="store_true", + help="Only show selected packages (direct dependencies).") + list_parser.add_argument( + nargs="*", + default=[], + dest="name_patterns", + help="Patterns (globs) that must all match package names.") + + return parser.parse_args() + +def main(): + global USE_COLOUR + + args = parse_args() + action = args.action + USE_COLOUR = args.color == "always" or (args.color == "auto" and sys.stdout.isatty()) + del args.action + del args.color + + if action is None: + print("nvd: Subcommand required, see 'nvd --help'.") + sys.exit(1) + + { + "list": run_list, + "diff": run_diff, + }[action](**vars(args)) + # Importing this module is kinda broken because of the constant initializations # at the top. if __name__ == "__main__": diff --git a/src/nvd.1 b/src/nvd.1 index 0a921e2..8c68756 100644 --- a/src/nvd.1 +++ b/src/nvd.1 @@ -1,28 +1,52 @@ -.TH nvd 1 2021-04-09 nvd "User Commands" +.TH nvd 1 2021-05-16 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 +.B nvd [ --color=(auto|always|never) ] diff +.I root1 +.I root2 +.P +.B nvd [ --color=(auto|always|never) ] list +.RS +.B [ -r|--root +.I store-path +.B ] +.br +.B [ -s|--selected ] +.br +.B [ +.I name-pattern +.B ]* +.RE .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 +is a tool for working with Nix store paths and their closures, grouped by +package name, with nice presentation for package versions. This is mainly +intended for listing installed packages and 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 list -i +and .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 The +.B diff +command compares the packages and package versions included in two closures. +The +.B list +command displays the list of packages included in a closure, optionally filtered +to some criteria. .P -When given two system configurations, .B nvd distinguishes between packages that are explicitly included in .B environment.systemPackages, @@ -88,14 +112,41 @@ detects whether stdout is a terminal and only uses colour if it is. Passing forces colour to always be used, and passing .B never forces colour to never be used. +.TP +-r|--root +For the +.B list +command, this is the root store path to use, or a link to the store path to use. +.TP +-s|--selected +For the +.B list +command, only print out selected packages (direct dependencies). +.TP + +For the +.B list +command, only print out names matching all of the given patterns. The character +.B ? +matches exactly one of any character, and the character +.B * +matches zero or more of any character. .SH OUTPUT .P -The output of +If the +.B diff +command is requested, then the output of .B nvd displays the given names of the two store paths being compared on the first two -lines, then changed packages on subsequent lines, one line per package, grouped -by the type of change. Newly added or removed packages, packages with version -or count changes, and packages with selection state changes are displayed. Hash changes are +lines. After this, for both the +.B diff +and +.B list +commands, package names, versions, and other information is printed out, with +one line per package name, in lexicographic order. When producing a diff, +packages are grouped by the type of change: newly added or removed packages, +packages with version or count changes, and packages with selection state +changes. Hash changes are .B not considered important, and will not cause a package to be displayed, however changes in the number of times a package @@ -132,6 +183,7 @@ removed, upgraded or downgraded. The possibilities here are: .P .RS .EX +I: Installed (no change). A: Package is newly added to the closure. R: All versions of the package are being removed the closure. U: The package is being upgraded. @@ -140,7 +192,15 @@ C: None of the above apply; unspecified changes. .EE .RE .P -Packages can be displayed with +The +.B list +command displays only the +.B I +state, and the +.B diff +command can display all states but the +.B I +state. Packages can be displayed with .B C install state for a few different reasons, including the number of copies of an existing version in the closure changing, or due to the selection state changing @@ -200,7 +260,7 @@ plus additional versions of some existing packages: .P .RS .EX -$ nvd /nix/var/nix/profiles/system-{29,30}-link +$ nvd diff /nix/var/nix/profiles/system-{29,30}-link <<< /nix/var/nix/profiles/system-29-link >>> /nix/var/nix/profiles/system-30-link Version changes: @@ -232,7 +292,7 @@ have been installed. .P .RS .EX -$ nvd /nix/var/nix/profiles/system-{33,34}-link +$ nvd diff /nix/var/nix/profiles/system-{33,34}-link <<< /nix/var/nix/profiles/system/system-33-link >>> /nix/var/nix/profiles/system/system-34-link Version changes: @@ -263,7 +323,7 @@ one, despite this actually being a newer commit than the old one: .P .RS .EX -$ nvd /nix/var/nix/profiles/system-{43,44}-link +$ nvd diff /nix/var/nix/profiles/system-{43,44}-link <<< /nix/var/nix/profiles/system/system-43-link >>> /nix/var/nix/profiles/system/system-44-link Version changes: @@ -278,7 +338,7 @@ to output no packages at all if no versions have changed: .P .RS .EX -$ nvd /nix/var/nix/profiles/system-{23,24}-link +$ nvd diff /nix/var/nix/profiles/system-{23,24}-link <<< /nix/var/nix/profiles/system/system-23-link >>> /nix/var/nix/profiles/system/system-24-link No version or selection state changes.