vendor_split.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. """Split per-platform install trees into vendor/common/ + vendor/platform/<tag>/.
  2. Called by vendor_chattolib.sh. Each distribution is classified via its
  3. dist-info/WHEEL marker: ``Root-Is-Purelib: true`` means the wheel contains no
  4. compiled code and can be shared across platforms; everything else carries
  5. platform-specific binaries and goes into the per-platform directory.
  6. Files are moved individually (as listed in each dist-info/RECORD) rather than by
  7. top-level directory, so namespace packages shared by several distributions —
  8. ``google/`` today — end up in the right place.
  9. Usage: vendor_split.py <build_dir> <vendor_dir> <platform_tag>...
  10. """
  11. from __future__ import annotations
  12. import csv
  13. import filecmp
  14. import shutil
  15. import sys
  16. from pathlib import Path
  17. from typing import Dict, List, Set, Tuple
  18. def _is_pure(dist_info: Path) -> bool:
  19. wheel = dist_info / "WHEEL"
  20. if not wheel.is_file():
  21. # No marker: assume platform-specific, which is the safe direction —
  22. # the file gets duplicated per platform instead of being shared wrongly.
  23. return False
  24. for line in wheel.read_text(encoding="utf-8").splitlines():
  25. if line.lower().startswith("root-is-purelib:"):
  26. return line.split(":", 1)[1].strip().lower() == "true"
  27. return False
  28. def _record_files(dist_info: Path) -> List[str]:
  29. record = dist_info / "RECORD"
  30. if not record.is_file():
  31. return []
  32. with record.open(encoding="utf-8", newline="") as handle:
  33. return [row[0] for row in csv.reader(handle) if row and row[0]]
  34. def classify(tree: Path) -> Tuple[Set[str], Set[str]]:
  35. """Return (pure_files, binary_files) as paths relative to ``tree``."""
  36. pure: Set[str] = set()
  37. binary: Set[str] = set()
  38. for dist_info in sorted(tree.glob("*.dist-info")):
  39. target = pure if _is_pure(dist_info) else binary
  40. target.update(_record_files(dist_info))
  41. target.add(f"{dist_info.name}/")
  42. return pure, binary
  43. def copy_files(tree: Path, names: Set[str], dest: Path) -> int:
  44. copied = 0
  45. for name in sorted(names):
  46. src = tree / name
  47. if name.endswith("/"):
  48. if src.is_dir():
  49. shutil.copytree(src, dest / name, dirs_exist_ok=True)
  50. copied += 1
  51. continue
  52. if not src.is_file():
  53. continue # RECORD lists entries that --target does not create
  54. out = dest / name
  55. out.parent.mkdir(parents=True, exist_ok=True)
  56. shutil.copy2(src, out)
  57. copied += 1
  58. return copied
  59. def main(argv: List[str]) -> int:
  60. if len(argv) < 4:
  61. print(__doc__, file=sys.stderr)
  62. return 2
  63. build_dir, vendor_dir, tags = Path(argv[1]), Path(argv[2]), argv[3:]
  64. common_dir = vendor_dir / "common"
  65. reference: Dict[str, Path] = {}
  66. for tag in tags:
  67. tree = build_dir / tag
  68. if not tree.is_dir():
  69. print(f"❌ Build-Verzeichnis fehlt: {tree}", file=sys.stderr)
  70. return 1
  71. pure, binary = classify(tree)
  72. overlap = {p for p in pure & binary if not p.endswith("/")}
  73. if overlap:
  74. print(
  75. f"❌ {tag}: Dateien gehören zugleich zu einem puren und einem "
  76. f"binären Paket: {sorted(overlap)[:5]}",
  77. file=sys.stderr,
  78. )
  79. return 1
  80. n_bin = copy_files(tree, binary, vendor_dir / "platform" / tag)
  81. print(f" {tag}: {n_bin} plattformspezifische Dateien")
  82. if not reference:
  83. n_pure = copy_files(tree, pure, common_dir)
  84. reference = {"tag": tag, "files": pure} # type: ignore[dict-item]
  85. print(f" common: {n_pure} plattformunabhängige Dateien (aus {tag})")
  86. else:
  87. # The shared part must really be identical, otherwise common/ would
  88. # silently carry one platform's variant for all of them.
  89. ref_files: Set[str] = reference["files"] # type: ignore[assignment]
  90. if pure != ref_files:
  91. only_here = sorted(pure - ref_files)[:5]
  92. only_ref = sorted(ref_files - pure)[:5]
  93. print(
  94. f"⚠️ {tag}: pure Paketliste weicht von {reference['tag']} ab "
  95. f"(nur hier: {only_here}, nur dort: {only_ref})",
  96. file=sys.stderr,
  97. )
  98. mismatched = [
  99. name
  100. for name in sorted(pure & ref_files)
  101. if not name.endswith("/")
  102. and (tree / name).is_file()
  103. and (common_dir / name).is_file()
  104. and not filecmp.cmp(tree / name, common_dir / name, shallow=False)
  105. ]
  106. if mismatched:
  107. print(
  108. f"⚠️ {tag}: {len(mismatched)} gemeinsame Datei(en) unterscheiden "
  109. f"sich inhaltlich, z.B. {mismatched[:3]}",
  110. file=sys.stderr,
  111. )
  112. return 0
  113. if __name__ == "__main__":
  114. sys.exit(main(sys.argv))