168 lines
6.1 KiB
Python
Executable File
168 lines
6.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Update the LibreWolf cask from the latest stable bsys6 release."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from urllib.request import Request, urlopen
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_CASK = ROOT / "Casks" / "librewolf.rb"
|
|
# Forgejo API for https://librewolf.dev/librewolf/bsys6/releases.
|
|
LATEST_RELEASE_API = "https://librewolf.dev/api/v1/repos/librewolf/bsys6/releases/latest"
|
|
PACKAGE_BASE_URL = "https://codeberg.org/api/packages/librewolf/generic/librewolf"
|
|
USER_AGENT = "homebrew-librewolf-release-updater/1.0"
|
|
VERSION_RE = re.compile(r'^ version "([^"]+)"$', re.MULTILINE)
|
|
SHA256_RE = re.compile(
|
|
r'^ sha256 arm:\s+"[0-9a-f]{64}",\n'
|
|
r'\s+intel:\s+"[0-9a-f]{64}",\n'
|
|
r'\s+arm64_linux:\s+"[0-9a-f]{64}",\n'
|
|
r'\s+x86_64_linux:\s+"[0-9a-f]{64}"$',
|
|
re.MULTILINE,
|
|
)
|
|
STABLE_TAG_RE = re.compile(
|
|
r"^(?P<major>\d+)\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?-(?P<build>\d+)$"
|
|
)
|
|
CHECKSUM_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
|
ASSET_SUFFIXES = {
|
|
"arm": "macos-arm64-package.dmg",
|
|
"intel": "macos-x86_64-package.dmg",
|
|
"arm64_linux": "linux-arm64-appimage.AppImage",
|
|
"x86_64_linux": "linux-x86_64-appimage.AppImage",
|
|
}
|
|
|
|
|
|
def fetch(url: str, limit: int) -> bytes:
|
|
request = Request(
|
|
url,
|
|
headers={"Accept": "application/json", "User-Agent": USER_AGENT},
|
|
)
|
|
with urlopen(request, timeout=30) as response:
|
|
body = response.read(limit + 1)
|
|
if len(body) > limit:
|
|
raise ValueError(f"response from {url} exceeds {limit} bytes")
|
|
return body
|
|
|
|
|
|
def latest_stable_release() -> dict[str, Any]:
|
|
release = json.loads(fetch(LATEST_RELEASE_API, 2_000_000))
|
|
if not isinstance(release, dict):
|
|
raise ValueError("latest release API did not return an object")
|
|
if release.get("draft") or release.get("prerelease"):
|
|
raise ValueError("latest release is not stable")
|
|
|
|
raw_tag = release.get("tag_name")
|
|
if not isinstance(raw_tag, str):
|
|
raise ValueError("latest release has no tag_name")
|
|
tag = raw_tag.removeprefix("v")
|
|
if not STABLE_TAG_RE.fullmatch(tag):
|
|
raise ValueError(f"latest release tag is not a stable version: {raw_tag!r}")
|
|
release["stable_tag"] = tag
|
|
return release
|
|
|
|
|
|
def version_key(version: str) -> tuple[int, int, int, int]:
|
|
normalized = version.removeprefix("v").replace(",", "-")
|
|
match = STABLE_TAG_RE.fullmatch(normalized)
|
|
if match is None:
|
|
raise ValueError(f"invalid cask version: {version!r}")
|
|
return (
|
|
int(match["major"]),
|
|
int(match["minor"]),
|
|
int(match["patch"] or 0),
|
|
int(match["build"]),
|
|
)
|
|
|
|
|
|
def release_checksums(release: dict[str, Any]) -> dict[str, str]:
|
|
assets = release.get("assets")
|
|
if not isinstance(assets, list):
|
|
raise ValueError("latest release has no asset list")
|
|
|
|
by_name: dict[str, dict[str, Any]] = {}
|
|
for asset in assets:
|
|
if not isinstance(asset, dict) or not isinstance(asset.get("name"), str):
|
|
continue
|
|
name = asset["name"]
|
|
if name in by_name:
|
|
raise ValueError(f"duplicate release asset: {name}")
|
|
by_name[name] = asset
|
|
|
|
tag = release["stable_tag"]
|
|
checksums: dict[str, str] = {}
|
|
for cask_arch, suffix in ASSET_SUFFIXES.items():
|
|
package_name = f"librewolf-{tag}-{suffix}"
|
|
if package_name not in by_name:
|
|
raise ValueError(f"missing release asset: {package_name}")
|
|
|
|
checksum_name = f"{package_name}.sha256sum"
|
|
if checksum_name not in by_name:
|
|
raise ValueError(f"missing release asset: {checksum_name}")
|
|
checksum_url = f"{PACKAGE_BASE_URL}/{tag}/{checksum_name}"
|
|
|
|
checksum_text = fetch(checksum_url, 4096).decode("ascii").strip()
|
|
checksum = checksum_text.split(maxsplit=1)[0] if checksum_text else ""
|
|
if not CHECKSUM_RE.fullmatch(checksum):
|
|
raise ValueError(f"invalid checksum for {package_name}")
|
|
checksums[cask_arch] = checksum.lower()
|
|
|
|
return checksums
|
|
|
|
|
|
def update_cask(cask_path: Path) -> bool:
|
|
source = cask_path.read_text(encoding="utf-8")
|
|
version_match = VERSION_RE.search(source)
|
|
if version_match is None:
|
|
raise ValueError(f"could not find version stanza in {cask_path}")
|
|
|
|
current_version = version_match.group(1)
|
|
release = latest_stable_release()
|
|
latest_tag = release["stable_tag"]
|
|
latest_key = version_key(latest_tag)
|
|
current_key = version_key(current_version)
|
|
comparison = (latest_key > current_key) - (latest_key < current_key)
|
|
if comparison <= 0:
|
|
state = "current" if comparison == 0 else "newer than the latest stable release"
|
|
print(f"LibreWolf {current_version} is {state}; no update needed")
|
|
return False
|
|
|
|
checksums = release_checksums(release)
|
|
cask_version = latest_tag.replace("-", ",")
|
|
updated, version_count = VERSION_RE.subn(f' version "{cask_version}"', source, count=1)
|
|
sha256_stanza = (
|
|
f' sha256 arm: "{checksums["arm"]}",\n'
|
|
f' intel: "{checksums["intel"]}",\n'
|
|
f' arm64_linux: "{checksums["arm64_linux"]}",\n'
|
|
f' x86_64_linux: "{checksums["x86_64_linux"]}"'
|
|
)
|
|
updated, sha256_count = SHA256_RE.subn(sha256_stanza, updated, count=1)
|
|
if version_count != 1 or sha256_count != 1:
|
|
raise ValueError(f"could not update version and sha256 stanzas in {cask_path}")
|
|
|
|
temporary_path = cask_path.with_suffix(f"{cask_path.suffix}.tmp")
|
|
temporary_path.write_text(updated, encoding="utf-8")
|
|
temporary_path.replace(cask_path)
|
|
print(f"Updated LibreWolf from {current_version} to {cask_version}")
|
|
return True
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--cask", type=Path, default=DEFAULT_CASK)
|
|
args = parser.parse_args()
|
|
try:
|
|
update_cask(args.cask)
|
|
except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as error:
|
|
print(f"error: {error}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|