mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 18:07:24 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
"""Gradle: `gradle/libs.versions.toml` -> Maven coordinates, and the wrapper.
|
|
|
|
The version catalog is the one place a Gradle build should declare versions,
|
|
which makes it a manifest the gate can read without evaluating Kotlin DSL.
|
|
A build may have several (`settings.gradle.kts` can `from(files(...))` any
|
|
`*.versions.toml`; a large app can have several), so the suffix is what is matched.
|
|
Three shapes appear in `[libraries]` and `[plugins]`:
|
|
|
|
foo = "group:artifact:1.2.3" # inline string
|
|
foo = { module = "group:artifact", version.ref = "x" }
|
|
foo = { group = "g", name = "a", version = "1.2.3" }
|
|
plug = { id = "com.example.plugin", version.ref = "x" }
|
|
|
|
A library with no version is managed by a BOM (`junit-platform-launcher`
|
|
in a typical app) and is skipped: the BOM's own entry is the pin that counts.
|
|
A rich version table (`{ strictly = "1.2" }`) is read as the constraint it
|
|
spells and lands as unpinned when it is a range, like every other ecosystem.
|
|
|
|
The line reported is the `[versions]` entry when a `version.ref` is used,
|
|
because that is where the fix goes; several libraries sharing one ref each
|
|
report the same line, which is one finding per stale artifact, not noise.
|
|
|
|
`gradle-wrapper.properties` names the Gradle distribution; that is the
|
|
toolchain pin, judged against services.gradle.org like Flutter against its
|
|
release feed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import tomllib
|
|
|
|
from dep_freshness._tables import GITCOMMIT, JITPACK_GROUP_PREFIX, MAVEN, TOOLCHAIN
|
|
from dep_freshness.models import Dep
|
|
from dep_freshness.parsers._lines import index
|
|
from dep_freshness.versions import exact_pin
|
|
|
|
CATALOG_SUFFIX = ".versions.toml" # libs.versions.toml, but also sy.versions.toml
|
|
WRAPPER_NAME = "gradle-wrapper.properties"
|
|
_DISTRIBUTION = re.compile(
|
|
r"gradle-(?P<version>\d+(?:\.\d+)*(?:-[0-9A-Za-z.-]+?)?)-(?:bin|all)\.zip"
|
|
)
|
|
# JitPack accepts any commit as a version; 7 to 40 hex digits and nothing
|
|
# else is one. `1.0.0` never matches (dots), and a tag like `v2.3.0` has a
|
|
# letter outside a-f.
|
|
_COMMIT = re.compile(r"^[0-9a-f]{7,40}$")
|
|
|
|
|
|
def _commit_pin(name: str, version: str) -> bool:
|
|
"""True for `com.github.*` at a commit hash: a gitcommit dep, not maven."""
|
|
return name.startswith(JITPACK_GROUP_PREFIX) and bool(_COMMIT.match(version))
|
|
|
|
|
|
def _rich(value: Any) -> str:
|
|
"""The version string a catalog value spells, for any of its shapes."""
|
|
if isinstance(value, dict):
|
|
for key in ("strictly", "require", "prefer"):
|
|
if value.get(key):
|
|
return str(value[key])
|
|
return ""
|
|
return str(value or "")
|
|
|
|
|
|
def _coordinates(entry: Any, plugin: bool) -> tuple[str, str] | None:
|
|
"""`(name, inline_version)` for a library or plugin entry, or None."""
|
|
if isinstance(entry, str):
|
|
parts = entry.split(":")
|
|
if plugin:
|
|
return (parts[0], parts[1] if len(parts) > 1 else "")
|
|
if len(parts) < 2:
|
|
return None
|
|
return (f"{parts[0]}:{parts[1]}", parts[2] if len(parts) > 2 else "")
|
|
if not isinstance(entry, dict):
|
|
return None
|
|
if plugin:
|
|
name = str(entry.get("id") or "")
|
|
elif entry.get("module"):
|
|
name = str(entry["module"])
|
|
else:
|
|
name = f"{entry.get('group', '')}:{entry.get('name', '')}"
|
|
if not name.strip(":"):
|
|
return None
|
|
version = entry.get("version")
|
|
if isinstance(version, dict) and version.get("ref"):
|
|
return (name, f"ref:{version['ref']}")
|
|
return (name, _rich(version))
|
|
|
|
|
|
def parse_catalog(path: Path) -> list[Dep]:
|
|
"""Every versioned library and plugin in a Gradle version catalog."""
|
|
try:
|
|
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, tomllib.TOMLDecodeError):
|
|
return []
|
|
versions = {k: _rich(v) for k, v in (data.get("versions") or {}).items()}
|
|
lines = index(path)
|
|
deps: list[Dep] = []
|
|
for section, plugin in (("libraries", False), ("plugins", True)):
|
|
for alias, entry in (data.get(section) or {}).items():
|
|
coords = _coordinates(entry, plugin)
|
|
if coords is None:
|
|
continue
|
|
name, version = coords
|
|
line = lines.get(str(alias), 0)
|
|
if version.startswith("ref:"):
|
|
ref = version[4:]
|
|
version = versions.get(ref, "")
|
|
line = lines.get(ref, line)
|
|
if not version:
|
|
continue # BOM-managed, or a dangling ref the build itself rejects
|
|
if plugin:
|
|
name = f"{name}:{name}.gradle.plugin"
|
|
ecosystem, pinned = MAVEN, exact_pin(version)
|
|
if _commit_pin(name, version):
|
|
ecosystem, pinned = GITCOMMIT, version.strip()
|
|
deps.append(
|
|
Dep(
|
|
ecosystem=ecosystem,
|
|
name=name,
|
|
constraint=version,
|
|
path=path,
|
|
line=line,
|
|
pinned=pinned,
|
|
)
|
|
)
|
|
return deps
|
|
|
|
|
|
def parse_wrapper(path: Path) -> list[Dep]:
|
|
"""The Gradle distribution the wrapper downloads, as a toolchain pin."""
|
|
try:
|
|
text = path.read_text(encoding="utf-8")
|
|
except OSError:
|
|
return []
|
|
for number, line in enumerate(text.splitlines(), start=1):
|
|
if not line.startswith("distributionUrl="):
|
|
continue
|
|
match = _DISTRIBUTION.search(line)
|
|
version = match.group("version") if match else ""
|
|
return [
|
|
Dep(
|
|
ecosystem=TOOLCHAIN,
|
|
name="gradle",
|
|
constraint=version,
|
|
path=path,
|
|
line=number,
|
|
pinned=exact_pin(version),
|
|
)
|
|
]
|
|
return []
|
|
|
|
|
|
def is_catalog(path: Path) -> bool:
|
|
"""True for `libs.versions.toml` and any sibling catalog a build declares."""
|
|
return path.name.endswith(CATALOG_SUFFIX)
|