nix-update/nix_update/__init__.py
2024-06-06 12:22:10 +00:00

344 lines
9.9 KiB
Python

import argparse
import os
import shutil
import sys
import tempfile
from typing import NoReturn
from .eval import CargoLockInSource, Package, eval_attr
from .options import Options
from .update import update
from .utils import run
from .version.version import VersionPreference
def die(msg: str) -> NoReturn:
print(msg, file=sys.stderr)
sys.exit(1)
def parse_args(args: list[str]) -> Options:
parser = argparse.ArgumentParser()
help = "File to import rather than default.nix. Examples, ./release.nix"
parser.add_argument("-f", "--file", default="./.", help=help)
parser.add_argument(
"-F", "--flake", action="store_true", help="Update a flake attribute instead"
)
parser.add_argument("--build", action="store_true", help="build the package")
parser.add_argument(
"--test", action="store_true", help="Run package's `passthru.tests`"
)
parser.add_argument(
"--review", action="store_true", help="Run `nixpkgs-review wip`"
)
parser.add_argument("--format", action="store_true", help="Run `nixfmt`")
parser.add_argument(
"--commit", action="store_true", help="Commit the updated package"
)
parser.add_argument(
"-u",
"--use-update-script",
action="store_true",
help="Use passthru.updateScript instead if possible",
)
parser.add_argument(
"--url",
help="URL to the repository to check for a release instead of using the URL in the src attribute of the package",
)
parser.add_argument(
"--write-commit-message",
metavar="FILE",
help="Write commit message to FILE",
)
parser.add_argument(
"-vr",
"--version-regex",
help="Regex to extract version with, i.e. 'jq-(.*)'",
default="(.*)",
)
parser.add_argument(
"--run",
action="store_true",
help="provide a shell based on `nix run` with the package in $PATH",
)
parser.add_argument(
"--shell", action="store_true", help="provide a shell with the package"
)
parser.add_argument(
"--version",
nargs="?",
default=VersionPreference.STABLE,
help="Version to update to. Possible values are: "
+ ", ".join(VersionPreference),
)
parser.add_argument(
"--override-filename",
nargs="?",
help="Set filename where nix-update will update version/hash",
default=None,
)
parser.add_argument(
"--system",
help="The system used to to calculate the hash and run other nix commands",
default=None,
)
default_attribute = os.getenv("UPDATE_NIX_ATTR_PATH")
parser.add_argument(
"attribute",
default=default_attribute,
nargs="?" if default_attribute else None, # type: ignore
help="""Attribute name within the file evaluated (defaults to environment variable "UPDATE_NIX_ATTR_PATH")""",
)
a = parser.parse_args(args)
return Options(
import_path=os.path.realpath(a.file),
flake=a.flake,
build=a.build,
commit=a.commit,
use_update_script=a.use_update_script,
url=a.url,
write_commit_message=a.write_commit_message,
run=a.run,
shell=a.shell,
version=a.version,
version_preference=VersionPreference.from_str(a.version),
attribute=a.attribute,
test=a.test,
version_regex=a.version_regex,
review=a.review,
format=a.format,
override_filename=a.override_filename,
system=a.system,
extra_flags=(["--system", a.system] if a.system else [])
+ ["--extra-experimental-features", "flakes nix-command"],
)
def nix_shell(options: Options) -> None:
if options.flake:
run(
[
"nix",
"shell",
f"{options.import_path}#{options.attribute}",
]
+ options.extra_flags,
stdout=None,
check=False,
)
else:
expr = f"let pkgs = import {options.escaped_import_path} {{}}; in pkgs.mkShell {{ buildInputs = [ pkgs.{options.escaped_attribute} ]; }}"
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "default.nix")
with open(path, "w") as f:
f.write(expr)
run(["nix-shell", path] + options.extra_flags, stdout=None, check=False)
def git_has_diff(git_dir: str, package: Package) -> bool:
diff = run(["git", "-C", git_dir, "diff", "--", package.filename])
return len(diff.stdout) > 0
def format_commit_message(package: Package) -> str:
new_version = getattr(package.new_version, "number", None)
if (
new_version
and package.old_version != new_version
and new_version.startswith("v")
):
new_version = new_version[1:]
msg = f"{package.attribute}: {package.old_version} -> {new_version}"
if package.diff_url:
msg += f"\n\nDiff: {package.diff_url}"
if package.changelog:
msg += f"\n\nChangelog: {package.changelog}"
return msg
def git_commit(git_dir: str, package: Package) -> None:
msg = format_commit_message(package)
new_version = package.new_version
files_changed = [package.filename]
if isinstance(package.cargo_lock, CargoLockInSource):
files_changed.append(package.cargo_lock.path)
if new_version and (
package.old_version != new_version.number
or (new_version.rev and new_version.rev != package.rev)
):
run(
["git", "-C", git_dir, "commit", "--verbose", "--message", msg]
+ files_changed,
stdout=None,
)
else:
with tempfile.NamedTemporaryFile(mode="w") as f:
f.write(msg)
f.flush()
run(
["git", "-C", git_dir, "commit", "--verbose", "--template", f.name]
+ files_changed,
stdout=None,
)
def write_commit_message(path: str, package: Package) -> None:
with open(path, "w") as f:
f.write(format_commit_message(package))
f.write("\n")
def find_git_root(path: str) -> str | None:
prefix = [path]
release_nix = [".git"]
while True:
root_path = os.path.join(*prefix)
release_nix_path = os.path.join(root_path, *release_nix)
if os.path.exists(release_nix_path):
return root_path
if os.path.abspath(root_path) == "/":
return None
prefix.append("..")
def validate_git_dir(import_path: str) -> str:
if os.path.isdir(import_path):
git_dir = find_git_root(import_path)
else:
git_dir = find_git_root(os.path.dirname(import_path))
if git_dir is None:
die(f"Could not find a git repository relative to {import_path}")
return git_dir
def nix_run(options: Options) -> None:
cmd = [
"nix",
"shell",
"-L",
] + options.extra_flags
if options.flake:
cmd.append(f"{options.import_path}#{options.attribute}")
else:
cmd.extend(["-f", options.import_path, options.attribute])
run(
cmd,
stdout=None,
check=False,
)
def nix_build_tool() -> str:
"Return `nom` if found in $PATH"
if shutil.which("nom"):
return "nom"
else:
return "nix"
def nix_build(options: Options) -> None:
cmd = [
nix_build_tool(),
"build",
"-L",
] + options.extra_flags
if options.flake:
cmd.append(f"{options.import_path}#{options.attribute}")
else:
cmd.extend(["-f", options.import_path, options.attribute])
run(cmd, stdout=None)
def nix_test(opts: Options, package: Package) -> None:
if not package.tests:
die(f"Package '{package.name}' does not define any tests")
cmd = [nix_build_tool(), "build", "-L"] + opts.extra_flags
if opts.flake:
for t in package.tests:
cmd.append(f"{opts.import_path}#{package.attribute}.tests.{t}")
else:
cmd.extend(["-f", opts.import_path])
for t in package.tests:
cmd.append("-A")
cmd.append(f"{package.attribute}.tests.{t}")
run(cmd, stdout=None)
def nixpkgs_review() -> None:
cmd = [
"nixpkgs-review",
"wip",
]
run(cmd, stdout=None)
def main(args: list[str] = sys.argv[1:]) -> None:
options = parse_args(args)
if not os.path.exists(options.import_path):
die(f"path {options.import_path} does not exist")
git_dir = None
if options.commit or options.review:
git_dir = validate_git_dir(options.import_path)
package = update(options)
if package.maintainers:
print("Package maintainers:")
for maintainer in package.maintainers:
print(
f" - {maintainer['name']}"
+ (f" (@{maintainer['github']})" if "github" in maintainer else "")
)
if options.build:
nix_build(options)
if options.run:
nix_run(options)
if options.shell:
nix_shell(options)
if not git_dir:
git_dir = find_git_root(options.import_path)
changes_detected = not git_dir or git_has_diff(git_dir, package)
if not changes_detected:
print("No changes detected, skipping remaining steps")
return
if options.test:
nix_test(options, package)
if options.review:
if options.flake:
print("--review is unsupported with --flake")
else:
nixpkgs_review()
if options.format:
run(["nixfmt", package.filename], stdout=None)
if options.commit:
assert git_dir is not None
if package.changelog:
# If we have a changelog we will re-eval the package in case it has changed
package.changelog = eval_attr(options).changelog
git_commit(git_dir, package)
if options.write_commit_message is not None:
write_commit_message(options.write_commit_message, package)
if __name__ == "__main__":
main()