vendor_path.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """Locate the vendored dependency tree matching this interpreter's platform.
  2. ``vendor/`` is split into a platform-independent part and one directory per
  3. supported platform, because three of the vendored packages (pyqwest, protobuf,
  4. protobuf-py-ext) ship compiled extension modules::
  5. vendor/
  6. common/ pure-Python packages, shared by all platforms
  7. platform/
  8. linux-x86_64/ compiled extensions for that platform only
  9. linux-aarch64/
  10. macos-arm64/
  11. Both directories are prepended to ``sys.path`` (platform first), so the
  12. vendored copies win over anything installed system-wide. Run
  13. ``./vendor_chattolib.sh`` to (re)build the tree.
  14. """
  15. from __future__ import annotations
  16. import platform
  17. import sys
  18. from pathlib import Path
  19. from typing import List
  20. VENDOR_DIR = Path(__file__).parent / "vendor"
  21. # Directory name per (system, machine) — keep in sync with PLATFORMS in
  22. # vendor_chattolib.sh.
  23. _MACHINE_ALIASES = {
  24. "x86_64": "x86_64",
  25. "amd64": "x86_64",
  26. "aarch64": "aarch64",
  27. "arm64": "aarch64",
  28. }
  29. def _is_musl() -> bool:
  30. """True when running against musl libc (Alpine) rather than glibc."""
  31. try:
  32. libc, _version = platform.libc_ver()
  33. except OSError:
  34. return False
  35. # glibc reports "glibc"; musl reports an empty string.
  36. return libc != "glibc"
  37. def platform_tag() -> str:
  38. """Return the vendor/platform/ subdirectory name for this interpreter."""
  39. system = platform.system().lower()
  40. machine = _MACHINE_ALIASES.get(platform.machine().lower(), platform.machine().lower())
  41. if system == "linux":
  42. return f"linux-{machine}-musl" if _is_musl() else f"linux-{machine}"
  43. if system == "darwin":
  44. return f"macos-{'arm64' if machine == 'aarch64' else machine}"
  45. if system == "windows":
  46. return f"windows-{'amd64' if machine == 'x86_64' else machine}"
  47. return f"{system}-{machine}"
  48. def available_platforms() -> List[str]:
  49. """Platform directories that were actually vendored into this checkout."""
  50. platform_root = VENDOR_DIR / "platform"
  51. if not platform_root.is_dir():
  52. return []
  53. return sorted(p.name for p in platform_root.iterdir() if p.is_dir())
  54. def setup_vendor_path() -> List[str]:
  55. """Prepend the vendored packages for this platform to ``sys.path``.
  56. Returns the directories that were added. Raises ImportError when this
  57. platform was not vendored — a clear message beats the dlopen error you
  58. would otherwise get from a foreign-architecture .so file.
  59. """
  60. tag = platform_tag()
  61. platform_dir = VENDOR_DIR / "platform" / tag
  62. common_dir = VENDOR_DIR / "common"
  63. # Legacy flat layout (single platform, everything directly in vendor/).
  64. if not platform_dir.is_dir() and not common_dir.is_dir():
  65. if (VENDOR_DIR / "chattolib").is_dir():
  66. paths = [VENDOR_DIR]
  67. else:
  68. raise ImportError(
  69. f"Chatto: no vendored dependencies found in {VENDOR_DIR}. "
  70. f"Run ./vendor_chattolib.sh to build them."
  71. )
  72. elif not platform_dir.is_dir():
  73. raise ImportError(
  74. f"Chatto: no vendored dependencies for platform '{tag}'. "
  75. f"Available: {', '.join(available_platforms()) or 'none'}. "
  76. f"Run 'PLATFORMS={tag} ./vendor_chattolib.sh' to add it."
  77. )
  78. else:
  79. # Platform-specific extensions must shadow anything in common/.
  80. paths = [platform_dir, common_dir]
  81. added = []
  82. for path in paths:
  83. entry = str(path)
  84. if entry not in sys.path:
  85. sys.path.insert(0, entry)
  86. added.append(entry)
  87. return added