|
| 1 | +#!/usr/bin/python3 |
| 2 | + |
| 3 | +# vim: set ts=4 sw=4 et: |
| 4 | +""" |
| 5 | +Usage: |
| 6 | +
|
| 7 | +./parse-py-metadata.py -S "$DESTDIR/$py3_sitelib" provides -v "$version" |
| 8 | +
|
| 9 | + extract the names of top-level packages from: |
| 10 | + - $DESTDIR/$py3_sitelib/*.dist-info/METADATA |
| 11 | + - $DESTDIR/$py3_sitelib/*.egg-info/PKG-INFO |
| 12 | +
|
| 13 | +./parse-py-metadata.py -S "$DESTDIR/$py3_sitelib" [-s] [-C] depends -e "extra1 extra2 ..." |
| 14 | + -D "$XBPS_STATEDIR/$pkgname-rdeps" -V <( xbps-query -R -p provides -s "py3:" ) |
| 15 | +
|
| 16 | + check that the dependencies of a package match what's listed in the python |
| 17 | + package metadata, using the virtual package provides entries generated by |
| 18 | + `parse-py-metadata.py provides`. |
| 19 | +
|
| 20 | +This script requires python3-packaging-bootstrap to be installed in the chroot |
| 21 | +to run (which should be taken care of by the python3-module and python3-pep517 |
| 22 | +build styles). |
| 23 | +""" |
| 24 | + |
| 25 | +import argparse |
| 26 | +from pathlib import Path |
| 27 | +from sys import stderr |
| 28 | +from typing import TYPE_CHECKING |
| 29 | + |
| 30 | +if TYPE_CHECKING: |
| 31 | + from packaging.metadata import Metadata |
| 32 | + from packaging.requirements import Requirement |
| 33 | + from packaging.utils import canonicalize_name |
| 34 | + |
| 35 | + |
| 36 | +def msg_err(msg: str, *, nocolor: bool = False, strict: bool = False): |
| 37 | + if nocolor: |
| 38 | + print(msg, flush=True) |
| 39 | + else: |
| 40 | + color = "31" if strict else "33" |
| 41 | + print(f"\033[1m\033[{color}m{msg}\033[m", file=stderr, flush=True) |
| 42 | + |
| 43 | + |
| 44 | +def vpkgname(val: "str | Requirement", *, version: str | None = None) -> str: |
| 45 | + sfx = "" |
| 46 | + if version is not None: |
| 47 | + sfx = f"-{version}" |
| 48 | + if isinstance(val, Requirement): |
| 49 | + name = val.name |
| 50 | + else: |
| 51 | + name = val |
| 52 | + return f"py3:{canonicalize_name(name)}{sfx}" |
| 53 | + |
| 54 | + |
| 55 | +def getpkgname(pkgver: str) -> str: |
| 56 | + return pkgver.rpartition("-")[0] |
| 57 | + |
| 58 | + |
| 59 | +def getpkgversion(pkgver: str) -> str: |
| 60 | + return pkgver.rpartition("-")[2] |
| 61 | + |
| 62 | + |
| 63 | +def getpkgdepname(pkgdep: str) -> str: |
| 64 | + if "<" in pkgdep: |
| 65 | + return pkgdep.partition("<")[0] |
| 66 | + elif ">" in pkgdep: |
| 67 | + return pkgdep.partition(">")[0] |
| 68 | + else: |
| 69 | + return pkgdep.rpartition("-")[0] |
| 70 | + |
| 71 | + |
| 72 | +def match_markers(req: "Requirement", extras: set[str]) -> bool: |
| 73 | + # unconditional requirement |
| 74 | + if req.marker is None: |
| 75 | + return True |
| 76 | + |
| 77 | + # check the requirement for each extra we want and without any extras |
| 78 | + if extras: |
| 79 | + return req.marker.evaluate() and any(req.marker.evaluate({"extra": e}) for e in extras) |
| 80 | + |
| 81 | + return req.marker.evaluate() |
| 82 | + |
| 83 | + |
| 84 | +def find_metadata_files(sitepkgs: Path) -> list[Path]: |
| 85 | + metafiles = list(sitepkgs.glob("*.dist-info/METADATA")) |
| 86 | + metafiles.extend(sitepkgs.glob("*.egg-info/PKG-INFO")) |
| 87 | + return metafiles |
| 88 | + |
| 89 | + |
| 90 | +def parse_provides(args): |
| 91 | + out = set() |
| 92 | + |
| 93 | + for metafile in find_metadata_files(args.sitepkgs): |
| 94 | + with metafile.open() as f: |
| 95 | + raw = f.read() |
| 96 | + |
| 97 | + meta = Metadata.from_email(raw, validate=False) |
| 98 | + |
| 99 | + out.add(vpkgname(meta.name, version=getpkgversion(args.pkgver))) |
| 100 | + if meta.provides_dist is not None: |
| 101 | + out.update(map(lambda n: vpkgname(n, version=getpkgversion(args.pkgver)), meta.provides_dist)) |
| 102 | + # deprecated but may be used |
| 103 | + if meta.provides is not None: |
| 104 | + out.update(map(lambda n: vpkgname(n, version=getpkgversion(args.pkgver)), meta.provides)) |
| 105 | + |
| 106 | + print("\n".join(out), flush=True) |
| 107 | + |
| 108 | + |
| 109 | +def parse_depends(args): |
| 110 | + depends = dict() |
| 111 | + vpkgs = dict() |
| 112 | + extras = set(args.extras.split()) |
| 113 | + |
| 114 | + with args.vpkgs.open() as f: |
| 115 | + for ln in f.readlines(): |
| 116 | + if not ln.strip(): |
| 117 | + continue |
| 118 | + pkgver, _, rest = ln.partition(":") |
| 119 | + vpkgvers, _, _ = rest.strip().partition("(") |
| 120 | + pkg = getpkgname(pkgver) |
| 121 | + vpkg = map(getpkgname, vpkgvers.split()) |
| 122 | + for v in vpkg: |
| 123 | + vpkgs[v] = pkg |
| 124 | + |
| 125 | + if args.rdeps.exists(): |
| 126 | + with args.rdeps.open() as f: |
| 127 | + rdeps = list(map(getpkgdepname, f.read().split())) |
| 128 | + else: |
| 129 | + rdeps = [] |
| 130 | + |
| 131 | + for metafile in find_metadata_files(args.sitepkgs): |
| 132 | + with metafile.open() as f: |
| 133 | + raw = f.read() |
| 134 | + |
| 135 | + meta = Metadata.from_email(raw, validate=False) |
| 136 | + |
| 137 | + if meta.requires_dist is not None: |
| 138 | + depends.update(map(lambda p: (vpkgname(p), None), |
| 139 | + filter(lambda r: match_markers(r, extras), meta.requires_dist))) |
| 140 | + # deprecated but may be used |
| 141 | + if meta.requires is not None: |
| 142 | + depends.update(map(lambda p: (vpkgname(p), None), meta.requires)) |
| 143 | + |
| 144 | + err = False |
| 145 | + unknown = False |
| 146 | + missing = [] |
| 147 | + for k in depends.keys(): |
| 148 | + if k in vpkgs.keys(): |
| 149 | + pkgname = vpkgs[k] |
| 150 | + if pkgname in rdeps: |
| 151 | + print(f" PYTHON: {k} <-> {pkgname}", flush=True) |
| 152 | + else: |
| 153 | + msg_err(f" PYTHON: {k} <-> {pkgname} NOT IN depends PLEASE FIX!", |
| 154 | + nocolor=args.nocolor, strict=args.strict) |
| 155 | + missing.append(pkgname) |
| 156 | + err = True |
| 157 | + else: |
| 158 | + msg_err(f" PYTHON: {k} <-> UNKNOWN PKG PLEASE FIX!", |
| 159 | + nocolor=args.nocolor, strict=args.strict) |
| 160 | + unknown = True |
| 161 | + err = True |
| 162 | + |
| 163 | + if missing or unknown: |
| 164 | + msg_err(f"=> {args.pkgver}: missing dependencies detected!", |
| 165 | + nocolor=args.nocolor, strict=args.strict) |
| 166 | + if missing: |
| 167 | + msg_err(f"=> {args.pkgver}: please add these packages to depends: {' '.join(sorted(missing))}", |
| 168 | + nocolor=args.nocolor, strict=args.strict) |
| 169 | + |
| 170 | + if err and args.strict: |
| 171 | + exit(1) |
| 172 | + |
| 173 | + |
| 174 | +if __name__ == "__main__": |
| 175 | + parser = argparse.ArgumentParser() |
| 176 | + parser.add_argument("-S", dest="sitepkgs", type=Path) |
| 177 | + parser.add_argument("-v", dest="pkgver") |
| 178 | + parser.add_argument("-s", dest="strict", action="store_true") |
| 179 | + parser.add_argument("-C", dest="nocolor", action="store_true") |
| 180 | + subparsers = parser.add_subparsers() |
| 181 | + |
| 182 | + prov_parser = subparsers.add_parser("provides") |
| 183 | + prov_parser.set_defaults(func=parse_provides) |
| 184 | + |
| 185 | + deps_parser = subparsers.add_parser("depends") |
| 186 | + deps_parser.add_argument("-e", dest="extras", default="") |
| 187 | + deps_parser.add_argument("-V", dest="vpkgs", type=Path) |
| 188 | + deps_parser.add_argument("-D", dest="rdeps", type=Path) |
| 189 | + deps_parser.set_defaults(func=parse_depends) |
| 190 | + |
| 191 | + args = parser.parse_args() |
| 192 | + |
| 193 | + try: |
| 194 | + from packaging.metadata import Metadata |
| 195 | + from packaging.requirements import Requirement |
| 196 | + from packaging.utils import canonicalize_name |
| 197 | + except ImportError: |
| 198 | + msg_err(f"=> WARNING: {args.pkgver}: missing packaging module!\n" |
| 199 | + f"=> WARNING: {args.pkgver}: please add python3-packaging-bootstrap to hostmakedepends to run this check", |
| 200 | + nocolor=args.nocolor) |
| 201 | + exit(0) |
| 202 | + |
| 203 | + args.func(args) |
0 commit comments