vendor_split.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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. def _is_pure(dist_info: Path) -> bool:
  18. wheel = dist_info / "WHEEL"
  19. if not wheel.is_file():
  20. # No marker: assume platform-specific, which is the safe direction —
  21. # the file gets duplicated per platform instead of being shared wrongly.
  22. return False
  23. for line in wheel.read_text(encoding="utf-8").splitlines():
  24. if line.lower().startswith("root-is-purelib:"):
  25. return line.split(":", 1)[1].strip().lower() == "true"
  26. return False
  27. def _record_files(dist_info: Path) -> list[str]:
  28. record = dist_info / "RECORD"
  29. if not record.is_file():
  30. return []
  31. with record.open(encoding="utf-8", newline="") as handle:
  32. return [row[0] for row in csv.reader(handle) if row and row[0]]
  33. def classify(tree: Path) -> tuple[set[str], set[str]]:
  34. """Return (pure_files, binary_files) as paths relative to ``tree``."""
  35. pure: set[str] = set()
  36. binary: set[str] = set()
  37. for dist_info in sorted(tree.glob("*.dist-info")):
  38. target = pure if _is_pure(dist_info) else binary
  39. target.update(_record_files(dist_info))
  40. target.add(f"{dist_info.name}/")
  41. return pure, binary
  42. def copy_files(tree: Path, names: set[str], dest: Path) -> int:
  43. copied = 0
  44. for name in sorted(names):
  45. src = tree / name
  46. if name.endswith("/"):
  47. if src.is_dir():
  48. shutil.copytree(src, dest / name, dirs_exist_ok=True)
  49. copied += 1
  50. continue
  51. if not src.is_file():
  52. continue # RECORD lists entries that --target does not create
  53. out = dest / name
  54. out.parent.mkdir(parents=True, exist_ok=True)
  55. shutil.copy2(src, out)
  56. copied += 1
  57. return copied
  58. def main(argv: list[str]) -> int:
  59. if len(argv) < 4:
  60. print(__doc__, file=sys.stderr)
  61. return 2
  62. build_dir, vendor_dir, tags = Path(argv[1]), Path(argv[2]), argv[3:]
  63. common_dir = vendor_dir / "common"
  64. reference: dict[str, Path] = {}
  65. for tag in tags:
  66. tree = build_dir / tag
  67. if not tree.is_dir():
  68. print(f"❌ Build-Verzeichnis fehlt: {tree}", file=sys.stderr)
  69. return 1
  70. pure, binary = classify(tree)
  71. overlap = {p for p in pure & binary if not p.endswith("/")}
  72. if overlap:
  73. print(
  74. f"❌ {tag}: Dateien gehören zugleich zu einem puren und einem "
  75. f"binären Paket: {sorted(overlap)[:5]}",
  76. file=sys.stderr,
  77. )
  78. return 1
  79. n_bin = copy_files(tree, binary, vendor_dir / "platform" / tag)
  80. print(f" {tag}: {n_bin} plattformspezifische Dateien")
  81. if not reference:
  82. n_pure = copy_files(tree, pure, common_dir)
  83. reference = {"tag": tag, "files": pure} # type: ignore[dict-item]
  84. print(f" common: {n_pure} plattformunabhängige Dateien (aus {tag})")
  85. else:
  86. # The shared part must really be identical, otherwise common/ would
  87. # silently carry one platform's variant for all of them.
  88. ref_files: set[str] = reference["files"] # type: ignore[assignment]
  89. if pure != ref_files:
  90. only_here = sorted(pure - ref_files)[:5]
  91. only_ref = sorted(ref_files - pure)[:5]
  92. print(
  93. f"⚠️ {tag}: pure Paketliste weicht von {reference['tag']} ab "
  94. f"(nur hier: {only_here}, nur dort: {only_ref})",
  95. file=sys.stderr,
  96. )
  97. mismatched = [
  98. name
  99. for name in sorted(pure & ref_files)
  100. if not name.endswith("/")
  101. and (tree / name).is_file()
  102. and (common_dir / name).is_file()
  103. and not filecmp.cmp(tree / name, common_dir / name, shallow=False)
  104. ]
  105. if mismatched:
  106. print(
  107. f"⚠️ {tag}: {len(mismatched)} gemeinsame Datei(en) unterscheiden "
  108. f"sich inhaltlich, z.B. {mismatched[:3]}",
  109. file=sys.stderr,
  110. )
  111. return 0
  112. if __name__ == "__main__":
  113. sys.exit(main(sys.argv))