"""Split per-platform install trees into vendor/common/ + vendor/platform//. Called by vendor_chattolib.sh. Each distribution is classified via its dist-info/WHEEL marker: ``Root-Is-Purelib: true`` means the wheel contains no compiled code and can be shared across platforms; everything else carries platform-specific binaries and goes into the per-platform directory. Files are moved individually (as listed in each dist-info/RECORD) rather than by top-level directory, so namespace packages shared by several distributions — ``google/`` today — end up in the right place. Usage: vendor_split.py ... """ from __future__ import annotations import csv import filecmp import shutil import sys from pathlib import Path from typing import Dict, List, Set, Tuple def _is_pure(dist_info: Path) -> bool: wheel = dist_info / "WHEEL" if not wheel.is_file(): # No marker: assume platform-specific, which is the safe direction — # the file gets duplicated per platform instead of being shared wrongly. return False for line in wheel.read_text(encoding="utf-8").splitlines(): if line.lower().startswith("root-is-purelib:"): return line.split(":", 1)[1].strip().lower() == "true" return False def _record_files(dist_info: Path) -> List[str]: record = dist_info / "RECORD" if not record.is_file(): return [] with record.open(encoding="utf-8", newline="") as handle: return [row[0] for row in csv.reader(handle) if row and row[0]] def classify(tree: Path) -> Tuple[Set[str], Set[str]]: """Return (pure_files, binary_files) as paths relative to ``tree``.""" pure: Set[str] = set() binary: Set[str] = set() for dist_info in sorted(tree.glob("*.dist-info")): target = pure if _is_pure(dist_info) else binary target.update(_record_files(dist_info)) target.add(f"{dist_info.name}/") return pure, binary def copy_files(tree: Path, names: Set[str], dest: Path) -> int: copied = 0 for name in sorted(names): src = tree / name if name.endswith("/"): if src.is_dir(): shutil.copytree(src, dest / name, dirs_exist_ok=True) copied += 1 continue if not src.is_file(): continue # RECORD lists entries that --target does not create out = dest / name out.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, out) copied += 1 return copied def main(argv: List[str]) -> int: if len(argv) < 4: print(__doc__, file=sys.stderr) return 2 build_dir, vendor_dir, tags = Path(argv[1]), Path(argv[2]), argv[3:] common_dir = vendor_dir / "common" reference: Dict[str, Path] = {} for tag in tags: tree = build_dir / tag if not tree.is_dir(): print(f"❌ Build-Verzeichnis fehlt: {tree}", file=sys.stderr) return 1 pure, binary = classify(tree) overlap = {p for p in pure & binary if not p.endswith("/")} if overlap: print( f"❌ {tag}: Dateien gehören zugleich zu einem puren und einem " f"binären Paket: {sorted(overlap)[:5]}", file=sys.stderr, ) return 1 n_bin = copy_files(tree, binary, vendor_dir / "platform" / tag) print(f" {tag}: {n_bin} plattformspezifische Dateien") if not reference: n_pure = copy_files(tree, pure, common_dir) reference = {"tag": tag, "files": pure} # type: ignore[dict-item] print(f" common: {n_pure} plattformunabhängige Dateien (aus {tag})") else: # The shared part must really be identical, otherwise common/ would # silently carry one platform's variant for all of them. ref_files: Set[str] = reference["files"] # type: ignore[assignment] if pure != ref_files: only_here = sorted(pure - ref_files)[:5] only_ref = sorted(ref_files - pure)[:5] print( f"⚠️ {tag}: pure Paketliste weicht von {reference['tag']} ab " f"(nur hier: {only_here}, nur dort: {only_ref})", file=sys.stderr, ) mismatched = [ name for name in sorted(pure & ref_files) if not name.endswith("/") and (tree / name).is_file() and (common_dir / name).is_file() and not filecmp.cmp(tree / name, common_dir / name, shallow=False) ] if mismatched: print( f"⚠️ {tag}: {len(mismatched)} gemeinsame Datei(en) unterscheiden " f"sich inhaltlich, z.B. {mismatched[:3]}", file=sys.stderr, ) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))