nix-update/nix_update/update.py

386 lines
13 KiB
Python
Raw Normal View History

2020-03-16 13:06:32 +03:00
import fileinput
import json
import re
import shutil
2020-11-29 11:31:55 +03:00
import subprocess
import sys
import tempfile
import tomllib
from concurrent.futures import ThreadPoolExecutor
from os import path
from pathlib import Path
2020-03-16 13:06:32 +03:00
from .errors import UpdateError
2023-04-03 15:59:22 +03:00
from .eval import CargoLockInSource, CargoLockInStore, Package, eval_attr
from .git import old_version_from_git
2020-03-17 16:22:33 +03:00
from .options import Options
2020-04-21 12:06:55 +03:00
from .utils import info, run
from .version import fetch_latest_version
2022-11-24 20:33:58 +03:00
from .version.gitlab import GITLAB_API
2022-11-21 23:07:18 +03:00
from .version.version import Version, VersionPreference
2020-03-16 13:06:32 +03:00
def replace_version(package: Package) -> bool:
assert package.new_version is not None
2020-03-24 16:25:17 +03:00
old_version = package.old_version
new_version = package.new_version.number
2020-03-24 16:25:17 +03:00
if new_version.startswith("v"):
new_version = new_version[1:]
2020-03-16 13:06:32 +03:00
changed = old_version != new_version or (
package.new_version.rev is not None and package.new_version.rev != package.rev
)
if changed:
2020-03-24 16:25:17 +03:00
info(f"Update {old_version} -> {new_version} in {package.filename}")
with fileinput.FileInput(package.filename, inplace=True) as f:
2020-03-16 13:06:32 +03:00
for line in f:
if package.new_version.rev:
line = line.replace(package.rev, package.new_version.rev)
print(line.replace(f'"{old_version}"', f'"{new_version}"'), end="")
2020-03-17 15:36:50 +03:00
else:
2020-03-24 16:25:17 +03:00
info(f"Not updating version, already {old_version}")
2020-03-16 13:06:32 +03:00
return changed
2020-03-16 13:06:32 +03:00
2020-11-29 11:31:55 +03:00
def to_sri(hashstr: str) -> str:
if "-" in hashstr:
return hashstr
2022-11-21 00:09:48 +03:00
length = len(hashstr)
if length == 32:
2020-11-29 11:31:55 +03:00
prefix = "md5:"
2022-11-21 00:09:48 +03:00
elif length == 40:
2020-11-29 11:31:55 +03:00
# could be also base32 == 32, but we ignore this case and hope no one is using it
prefix = "sha1:"
2022-11-21 00:09:48 +03:00
elif length == 64 or length == 52:
2020-11-29 11:31:55 +03:00
prefix = "sha256:"
2022-11-21 00:09:48 +03:00
elif length == 103 or length == 128:
2020-11-29 11:31:55 +03:00
prefix = "sha512:"
else:
return hashstr
cmd = [
"nix",
"--extra-experimental-features",
2020-11-29 11:31:55 +03:00
"nix-command",
2021-01-09 09:51:27 +03:00
"hash",
2020-11-29 11:31:55 +03:00
"to-sri",
f"{prefix}{hashstr}",
]
proc = run(cmd)
2020-11-29 11:31:55 +03:00
return proc.stdout.rstrip("\n")
2020-03-16 13:06:32 +03:00
def replace_hash(filename: str, current: str, target: str) -> None:
2020-11-29 11:31:55 +03:00
normalized_hash = to_sri(target)
if to_sri(current) != normalized_hash:
2020-03-16 13:06:32 +03:00
with fileinput.FileInput(filename, inplace=True) as f:
for line in f:
line = line.replace(current, normalized_hash)
2020-03-16 13:06:32 +03:00
print(line, end="")
def get_package(opts: Options) -> str:
return (
f"(let flake = builtins.getFlake {opts.escaped_import_path}; in flake.packages.${{builtins.currentSystem}}.{opts.escaped_attribute} or flake.{opts.escaped_attribute})"
2023-01-05 03:55:31 +03:00
if opts.flake
else f"(import {opts.escaped_import_path} {disable_check_meta(opts)}).{opts.escaped_attribute}"
2023-01-05 03:55:31 +03:00
)
def nix_prefetch(opts: Options, attr: str) -> str:
expr = f"{get_package(opts)}.{attr}"
2023-08-25 09:53:31 +03:00
extra_env: dict[str, str] = {}
tempdir: tempfile.TemporaryDirectory[str] | None = None
stderr = ""
if extra_env.get("XDG_RUNTIME_DIR") is None:
tempdir = tempfile.TemporaryDirectory()
extra_env["XDG_RUNTIME_DIR"] = tempdir.name
try:
2022-11-10 18:56:26 +03:00
res = run(
[
"nix-build",
"--expr",
2022-11-14 17:48:28 +03:00
f'let src = {expr}; in (src.overrideAttrs or (f: src // f src)) (_: {{ outputHash = ""; outputHashAlgo = "sha256"; }})',
2023-01-05 03:55:31 +03:00
]
+ opts.extra_flags,
2022-11-10 18:56:26 +03:00
extra_env=extra_env,
stderr=subprocess.PIPE,
2022-11-10 18:56:26 +03:00
check=False,
)
stderr = res.stderr.strip()
got = ""
for line in stderr.split("\n"):
line = line.strip()
if line.startswith("got:"):
got = line.split("got:")[1].strip()
break
finally:
if tempdir:
tempdir.cleanup()
2023-02-12 18:35:13 +03:00
if got == "":
print(stderr, file=sys.stderr)
raise UpdateError(
f"failed to retrieve hash when trying to update {opts.attribute}.{attr}"
)
2023-02-12 18:35:13 +03:00
else:
return got
2020-03-16 13:06:32 +03:00
def disable_check_meta(opts: Options) -> str:
return f'(if (builtins.hasAttr "config" (builtins.functionArgs (import {opts.escaped_import_path}))) then {{ config.checkMeta = false; overlays = []; }} else {{ }})'
2023-08-25 09:53:31 +03:00
def git_prefetch(x: tuple[str, tuple[str, str]]) -> tuple[str, str]:
rev, (key, url) = x
res = run(["nix-prefetch-git", url, rev, "--fetch-submodules"])
return key, to_sri(json.loads(res.stdout)["sha256"])
2022-12-08 20:11:00 +03:00
def update_src_hash(opts: Options, filename: str, current_hash: str) -> None:
2023-01-05 03:55:31 +03:00
target_hash = nix_prefetch(opts, "src")
2020-03-16 13:06:32 +03:00
replace_hash(filename, current_hash, target_hash)
2022-09-17 03:11:22 +03:00
def update_go_modules_hash(opts: Options, filename: str, current_hash: str) -> None:
target_hash = nix_prefetch(opts, "goModules")
replace_hash(filename, current_hash, target_hash)
def update_go_modules_hash_old(opts: Options, filename: str, current_hash: str) -> None:
2023-01-05 03:55:31 +03:00
target_hash = nix_prefetch(opts, "go-modules")
2020-03-16 13:06:32 +03:00
replace_hash(filename, current_hash, target_hash)
def update_cargo_deps_hash(opts: Options, filename: str, current_hash: str) -> None:
2023-01-05 03:55:31 +03:00
target_hash = nix_prefetch(opts, "cargoDeps")
2020-05-08 12:21:06 +03:00
replace_hash(filename, current_hash, target_hash)
def update_cargo_lock(
opts: Options, filename: str, dst: CargoLockInSource | CargoLockInStore
) -> None:
res = run(
[
"nix",
"build",
"--impure",
"--print-out-paths",
"--expr",
f"""
{get_package(opts)}.overrideAttrs (old: {{
cargoDeps = null;
postUnpack = ''
cp -r "$sourceRoot/${{old.cargoRoot or "."}}/Cargo.lock" $out
exit
'';
outputs = [ "out" ];
separateDebugInfo = false;
}})
""",
]
+ opts.extra_flags,
)
src = Path(res.stdout.strip())
if not src.is_file():
return
with open(src, "rb") as f:
if isinstance(dst, CargoLockInSource):
with open(dst.path, "wb") as fdst:
shutil.copyfileobj(f, fdst)
f.seek(0)
hashes = {}
lock = tomllib.load(f)
regex = re.compile(r"git\+([^?]+)(\?(rev|tag|branch)=.*)?#(.*)")
git_deps = {}
for pkg in lock["package"]:
if source := pkg.get("source"):
if match := regex.fullmatch(source):
rev = match[4]
if rev not in git_deps:
git_deps[rev] = f"{pkg['name']}-{pkg['version']}", match[1]
for k, v in ThreadPoolExecutor().map(git_prefetch, git_deps.items()):
hashes[k] = v
with fileinput.FileInput(filename, inplace=True) as f:
short = re.compile(r"(\s*)cargoLock\.lockFile\s*=\s*(.+)\s*;\s*")
expanded = re.compile(r"(\s*)lockFile\s*=\s*(.+)\s*;\s*")
for line in f:
if match := short.fullmatch(line):
indent = match[1]
path = match[2]
print(f"{indent}cargoLock = {{")
print(f"{indent} lockFile = {path};")
print_hashes(hashes, f"{indent} ")
print(f"{indent}}};")
for line in f:
print(line, end="")
return
elif match := expanded.fullmatch(line):
indent = match[1]
path = match[2]
print(line, end="")
print_hashes(hashes, indent)
brace = 0
for line in f:
for c in line:
if c == "{":
brace -= 1
if c == "}":
brace += 1
if brace == 1:
print(line, end="")
for line in f:
print(line, end="")
return
else:
print(line, end="")
def update_composer_deps_hash(opts: Options, filename: str, current_hash: str) -> None:
target_hash = nix_prefetch(opts, "composerRepository")
replace_hash(filename, current_hash, target_hash)
2023-08-25 09:53:31 +03:00
def print_hashes(hashes: dict[str, str], indent: str) -> None:
if not hashes:
return
print(f"{indent}outputHashes = {{")
for k, v in hashes.items():
print(f'{indent} "{k}" = "{v}";')
print(f"{indent}}};")
2022-11-10 18:56:55 +03:00
def update_npm_deps_hash(opts: Options, filename: str, current_hash: str) -> None:
2023-01-05 03:55:31 +03:00
target_hash = nix_prefetch(opts, "npmDeps")
2020-03-16 13:06:32 +03:00
replace_hash(filename, current_hash, target_hash)
def update_yarn_deps_hash(opts: Options, filename: str, current_hash: str) -> None:
target_hash = nix_prefetch(opts, "offlineCache")
replace_hash(filename, current_hash, target_hash)
def update_version(
package: Package, version: str, preference: VersionPreference, version_regex: str
) -> bool:
if preference == VersionPreference.FIXED:
new_version = Version(version)
else:
2022-11-24 20:33:58 +03:00
if not package.parsed_url:
raise UpdateError("Could not find a url in the derivations src attribute")
2022-11-29 20:44:09 +03:00
version_prefix = ""
if preference != VersionPreference.BRANCH:
branch = None
2022-11-29 20:44:09 +03:00
if package.rev and package.rev.endswith(package.old_version):
version_prefix = package.rev.removesuffix(package.old_version)
elif version == "branch":
# fallback
branch = "HEAD"
else:
assert version.startswith("branch=")
branch = version[7:]
new_version = fetch_latest_version(
2022-11-29 20:44:09 +03:00
package.parsed_url,
preference,
version_regex,
branch,
package.rev,
version_prefix,
)
2020-12-01 11:30:17 +03:00
package.new_version = new_version
position = package.version_position
if new_version.number == package.old_version and position:
2020-12-01 11:30:17 +03:00
recovered_version = old_version_from_git(
position.file, position.line, new_version.number
2020-12-01 11:30:17 +03:00
)
if recovered_version:
package.old_version = recovered_version
return False
2022-11-24 20:33:58 +03:00
if package.parsed_url:
2023-05-12 19:52:30 +03:00
if package.parsed_url.netloc == "crates.io":
parts = package.parsed_url.path.split("/")
package.diff_url = (
f"https://diff.rs/{parts[4]}/{package.old_version}/{new_version.number}"
)
elif package.parsed_url.netloc == "github.com":
2022-11-24 20:33:58 +03:00
_, owner, repo, *_ = package.parsed_url.path.split("/")
package.diff_url = f"https://github.com/{owner}/{repo.removesuffix('.git')}/compare/{package.rev}...{new_version.rev or new_version.number}"
2023-01-21 23:31:30 +03:00
elif package.parsed_url.netloc in ["codeberg.org", "gitea.com", "notabug.org"]:
_, owner, repo, *_ = package.parsed_url.path.split("/")
package.diff_url = f"https://{package.parsed_url.netloc}/{owner}/{repo}/compare/{package.rev}...{new_version.rev or new_version.number}"
2022-11-24 20:33:58 +03:00
elif GITLAB_API.match(package.parsed_url.geturl()) and package.src_homepage:
package.diff_url = f"{package.src_homepage}-/compare/{package.rev}...{new_version.rev or new_version.number}"
elif package.parsed_url.netloc in ["bitbucket.org", "bitbucket.io"]:
_, owner, repo, *_ = package.parsed_url.path.split("/")
package.diff_url = f"https://{package.parsed_url.netloc}/{owner}/{repo}/branches/compare/{new_version.rev or new_version.number}%0D{package.rev}"
2022-11-24 20:33:58 +03:00
return replace_version(package)
2020-12-01 11:30:17 +03:00
2020-03-24 16:25:17 +03:00
def update(opts: Options) -> Package:
2020-03-23 13:38:04 +03:00
package = eval_attr(opts)
2020-03-16 13:06:32 +03:00
if package.has_update_script and opts.use_update_script:
run(
[
"nix-shell",
path.join(opts.import_path, "maintainers/scripts/update.nix"),
"--argstr",
"package",
opts.attribute,
],
stdout=None,
)
new_package = eval_attr(opts)
package.new_version = Version(new_package.old_version, rev=new_package.rev)
return package
update_hash = True
if opts.version_preference != VersionPreference.SKIP:
update_hash = update_version(
package, opts.version, opts.version_preference, opts.version_regex
)
2020-03-16 13:06:32 +03:00
if package.hash and update_hash:
update_src_hash(opts, package.filename, package.hash)
2020-03-16 13:06:32 +03:00
# if no package.hash was provided we just update the other hashes unconditionally
if update_hash or not package.hash:
2023-04-10 00:11:05 +03:00
if package.go_modules:
update_go_modules_hash(opts, package.filename, package.go_modules)
2020-03-16 13:06:32 +03:00
if package.go_modules_old:
update_go_modules_hash_old(opts, package.filename, package.go_modules_old)
if package.cargo_deps:
update_cargo_deps_hash(opts, package.filename, package.cargo_deps)
2020-03-24 16:25:17 +03:00
if isinstance(package.cargo_lock, CargoLockInSource) or isinstance(
package.cargo_lock, CargoLockInStore
):
update_cargo_lock(opts, package.filename, package.cargo_lock)
if package.composer_deps:
update_composer_deps_hash(opts, package.filename, package.composer_deps)
2022-11-10 18:56:55 +03:00
if package.npm_deps:
update_npm_deps_hash(opts, package.filename, package.npm_deps)
2020-03-24 16:25:17 +03:00
if package.yarn_deps:
update_yarn_deps_hash(opts, package.filename, package.yarn_deps)
2020-03-24 16:25:17 +03:00
return package