Add tool that helps with maintenance in Fedora
Most likely there are some missing checks for None and the program may segfault, but this can be fixed in future. Signed-off-by: Igor Raits <ignatenkobrain@fedoraproject.org>
This commit is contained in:
parent
850b1ab5a2
commit
d1ee7ed95c
1 changed files with 176 additions and 0 deletions
176
tools/fedora-helper.py
Executable file
176
tools/fedora-helper.py
Executable file
|
@ -0,0 +1,176 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
from requests.compat import urljoin
|
||||||
|
import solv
|
||||||
|
|
||||||
|
XDG_CACHE_HOME = Path(os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache")))
|
||||||
|
CACHEDIR = XDG_CACHE_HOME / "rust2rpm"
|
||||||
|
|
||||||
|
# Arch that is used for resolving dependencies and such
|
||||||
|
ARCH = "x86_64"
|
||||||
|
# The repo that contains crates
|
||||||
|
REPO_URL = f"https://kojipkgs.fedoraproject.org/repos/f33-build/latest/{ARCH}/"
|
||||||
|
# Just some sane chunk size
|
||||||
|
CHUNK_SIZE = 8192
|
||||||
|
|
||||||
|
|
||||||
|
def _download(fname, chksum=None, uncompress=False):
|
||||||
|
url = urljoin(REPO_URL, fname)
|
||||||
|
with requests.get(urljoin(REPO_URL, fname), stream=True) as r:
|
||||||
|
tmp = tempfile.TemporaryFile()
|
||||||
|
total = int(r.headers["Content-Length"])
|
||||||
|
with click.progressbar(
|
||||||
|
length=total, label=f"Downloading {url}", file=sys.stderr
|
||||||
|
) as pb:
|
||||||
|
for chunk in r.iter_content(chunk_size=CHUNK_SIZE):
|
||||||
|
pb.update(tmp.write(chunk))
|
||||||
|
tmp.seek(0)
|
||||||
|
|
||||||
|
if chksum is not None:
|
||||||
|
fchksum = solv.Chksum(chksum.type)
|
||||||
|
fchksum.add_fd(tmp.fileno())
|
||||||
|
if chksum != fchksum:
|
||||||
|
raise Exception(f"Checksums do not match: {fchksum} != {chksum}")
|
||||||
|
|
||||||
|
return solv.xfopen_fd(os.path.basename(url) if uncompress else None, tmp.fileno())
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument("source", type=click.Path(exists=True))
|
||||||
|
@click.option(
|
||||||
|
"-a",
|
||||||
|
"--add",
|
||||||
|
multiple=True,
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
help="Add local RPMs to the pool that is used in dependency resolution.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-v", "--verbose", is_flag=True, help="Enable verbose mode.",
|
||||||
|
)
|
||||||
|
def get_binary_license(ctx, source, add, verbose):
|
||||||
|
"""Get the resulting license of a binary.
|
||||||
|
|
||||||
|
Since all crates (aka `rust-*-devel`) are linked statically, the resulting
|
||||||
|
binary license should be result of combining source license and all used
|
||||||
|
crates.
|
||||||
|
|
||||||
|
This command resolves dependencies and outputs final license(s).
|
||||||
|
"""
|
||||||
|
os.makedirs(CACHEDIR, exist_ok=True)
|
||||||
|
|
||||||
|
pool = solv.Pool()
|
||||||
|
pool.setarch(ARCH)
|
||||||
|
|
||||||
|
repo = pool.add_repo("upstream")
|
||||||
|
fd = _download("repodata/repomd.xml")
|
||||||
|
chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256)
|
||||||
|
chksum.add("1.1")
|
||||||
|
chksum.add_fd(fd.fileno())
|
||||||
|
cookie = chksum.raw()
|
||||||
|
repo.add_repomdxml(fd)
|
||||||
|
fd.close()
|
||||||
|
|
||||||
|
# Find "primary"
|
||||||
|
di = repo.Dataiterator_meta(
|
||||||
|
solv.REPOSITORY_REPOMD_TYPE, "primary", solv.Dataiterator.SEARCH_STRING
|
||||||
|
)
|
||||||
|
di.prepend_keyname(solv.REPOSITORY_REPOMD)
|
||||||
|
for d in di:
|
||||||
|
dp = d.parentpos()
|
||||||
|
filename = dp.lookup_str(solv.REPOSITORY_REPOMD_LOCATION)
|
||||||
|
checksum = dp.lookup_checksum(solv.REPOSITORY_REPOMD_CHECKSUM)
|
||||||
|
# FIXME: Can filename be None?
|
||||||
|
|
||||||
|
cache_file = CACHEDIR / "upstream.solv"
|
||||||
|
try:
|
||||||
|
with open(cache_file, "rb") as f:
|
||||||
|
clen = len(cookie)
|
||||||
|
f.seek(-clen, os.SEEK_END)
|
||||||
|
fcookie = f.read(clen)
|
||||||
|
if fcookie != cookie:
|
||||||
|
raise IOError("repomd.xml has changed")
|
||||||
|
f.seek(0)
|
||||||
|
fd = solv.xfopen_fd(None, f.fileno())
|
||||||
|
repo.add_solv(fd)
|
||||||
|
fd.close()
|
||||||
|
cached = True
|
||||||
|
except IOError:
|
||||||
|
# Failed to load from cache
|
||||||
|
cached = False
|
||||||
|
|
||||||
|
if not cached:
|
||||||
|
fd = _download(filename, chksum=checksum, uncompress=True)
|
||||||
|
repo.add_rpmmd(fd, None)
|
||||||
|
fd.close()
|
||||||
|
|
||||||
|
os.makedirs(CACHEDIR, exist_ok=True)
|
||||||
|
with tempfile.NamedTemporaryFile(prefix=".newsolv-", dir=CACHEDIR) as tmp:
|
||||||
|
fd = solv.xfopen_fd(None, tmp.fileno())
|
||||||
|
repo.write(fd)
|
||||||
|
fd.flush()
|
||||||
|
fd.write(cookie)
|
||||||
|
fd.close()
|
||||||
|
if repo.iscontiguous():
|
||||||
|
repo.empty()
|
||||||
|
repo.add_solv(tmp.name)
|
||||||
|
os.unlink(cache_file)
|
||||||
|
os.link(tmp.name, cache_file)
|
||||||
|
|
||||||
|
repo_local = pool.add_repo("local")
|
||||||
|
repo_local.priority = 99
|
||||||
|
s = repo.add_rpm(source)
|
||||||
|
for f in add:
|
||||||
|
repo.add_rpm(f)
|
||||||
|
|
||||||
|
pool.addfileprovides()
|
||||||
|
pool.createwhatprovides()
|
||||||
|
|
||||||
|
solver = pool.Solver()
|
||||||
|
solver.set_flag(solv.Solver.SOLVER_FLAG_IGNORE_RECOMMENDED, True)
|
||||||
|
problems = solver.solve(
|
||||||
|
[pool.Job(solv.Job.SOLVER_SOLVABLE | solv.Job.SOLVER_INSTALL, s.id)]
|
||||||
|
)
|
||||||
|
if problems:
|
||||||
|
click.echo("Failed to resolve dependencies:", err=True)
|
||||||
|
for problem in problems:
|
||||||
|
for rule in problem.findallproblemrules():
|
||||||
|
for info in rule.allinfos():
|
||||||
|
click.echo(f" - ", nl=False)
|
||||||
|
click.secho(info.problemstr(), fg="red")
|
||||||
|
ctx.abort()
|
||||||
|
|
||||||
|
licenses = collections.defaultdict(set)
|
||||||
|
for p in solver.transaction().newsolvables():
|
||||||
|
if (
|
||||||
|
not (p.name.startswith("rust-") and p.name.endswith("-devel"))
|
||||||
|
and not p == s
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
licenses[p.lookup_str(solv.SOLVABLE_LICENSE)].add(p)
|
||||||
|
# XXX: This is pure hack and we should optimize license license in much better way
|
||||||
|
if "MIT or ASL 2.0" in licenses and "ASL 2.0 or MIT" in licenses:
|
||||||
|
licenses["MIT or ASL 2.0"].update(licenses.pop("ASL 2.0 or MIT"))
|
||||||
|
|
||||||
|
for license, packages in sorted(licenses.items()):
|
||||||
|
click.echo(f"# {license}")
|
||||||
|
if verbose:
|
||||||
|
for package in packages:
|
||||||
|
click.secho(f"# * {package}", fg="white")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
Loading…
Reference in a new issue