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:
Igor Raits 2020-05-30 18:20:23 +02:00
parent 850b1ab5a2
commit d1ee7ed95c
No known key found for this signature in database
GPG key ID: 115D5AB89C5C1E1E

176
tools/fedora-helper.py Executable file
View 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()