vendor_path.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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. The two directories enter ``sys.path`` differently, on purpose:
  12. * ``platform/<tag>`` is **prepended**. It holds the compiled extensions that
  13. must match the vendored chattolib (pyqwest, protobuf, protobuf-py-ext);
  14. falling back to a host copy of a different version would break the generated
  15. protobuf code.
  16. * ``common`` is **appended**. Most of it (httpx, anyio, httpcore, h11, idna,
  17. certifi, typing_extensions) is also shipped by the Hermes agent itself, and
  18. a plugin has no business shadowing the host's pinned versions — Hermes pins
  19. certifi exactly, and we would otherwise substitute our own CA bundle for the
  20. whole process. The host's copies win; ours only serve as a fallback, which
  21. keeps out-of-process use (the cron sender) working on its own.
  22. Run ``./vendor_chattolib.sh`` to (re)build the tree.
  23. """
  24. from __future__ import annotations
  25. import platform
  26. import sys
  27. from pathlib import Path
  28. VENDOR_DIR = Path(__file__).parent / "vendor"
  29. # Directory name per (system, machine) — keep in sync with PLATFORMS in
  30. # vendor_chattolib.sh.
  31. _MACHINE_ALIASES = {
  32. "x86_64": "x86_64",
  33. "amd64": "x86_64",
  34. "aarch64": "aarch64",
  35. "arm64": "aarch64",
  36. }
  37. def _is_musl() -> bool:
  38. """True when running against musl libc (Alpine) rather than glibc."""
  39. try:
  40. libc, _version = platform.libc_ver()
  41. except OSError:
  42. return False
  43. # glibc reports "glibc"; musl reports an empty string.
  44. return libc != "glibc"
  45. def platform_tag() -> str:
  46. """Return the vendor/platform/ subdirectory name for this interpreter."""
  47. system = platform.system().lower()
  48. machine = _MACHINE_ALIASES.get(
  49. platform.machine().lower(), platform.machine().lower()
  50. )
  51. if system == "linux":
  52. return f"linux-{machine}-musl" if _is_musl() else f"linux-{machine}"
  53. if system == "darwin":
  54. return f"macos-{'arm64' if machine == 'aarch64' else machine}"
  55. if system == "windows":
  56. return f"windows-{'amd64' if machine == 'x86_64' else machine}"
  57. return f"{system}-{machine}"
  58. def available_platforms() -> list[str]:
  59. """Platform directories that were actually vendored into this checkout."""
  60. platform_root = VENDOR_DIR / "platform"
  61. if not platform_root.is_dir():
  62. return []
  63. return sorted(p.name for p in platform_root.iterdir() if p.is_dir())
  64. def setup_vendor_path() -> list[str]:
  65. """Put the vendored packages for this platform on ``sys.path``.
  66. The compiled extensions are prepended, the pure-Python packages appended;
  67. see the module docstring for why. Returns the directories that were added.
  68. Raises ImportError when this platform was not vendored — a clear message
  69. beats the dlopen error you would otherwise get from a foreign-architecture
  70. .so file.
  71. """
  72. tag = platform_tag()
  73. platform_dir = VENDOR_DIR / "platform" / tag
  74. common_dir = VENDOR_DIR / "common"
  75. # Legacy flat layout (single platform, everything directly in vendor/).
  76. if not platform_dir.is_dir() and not common_dir.is_dir():
  77. if (VENDOR_DIR / "chattolib").is_dir():
  78. prepend, append = [VENDOR_DIR], []
  79. else:
  80. raise ImportError(
  81. f"Chatto: no vendored dependencies found in {VENDOR_DIR}. "
  82. f"Run ./vendor_chattolib.sh to build them."
  83. )
  84. elif not platform_dir.is_dir():
  85. raise ImportError(
  86. f"Chatto: no vendored dependencies for platform '{tag}'. "
  87. f"Available: {', '.join(available_platforms()) or 'none'}. "
  88. f"Run 'PLATFORMS={tag} ./vendor_chattolib.sh' to add it."
  89. )
  90. else:
  91. prepend, append = [platform_dir], [common_dir]
  92. added = []
  93. for path in prepend:
  94. entry = str(path)
  95. if entry not in sys.path:
  96. sys.path.insert(0, entry)
  97. added.append(entry)
  98. for path in append:
  99. entry = str(path)
  100. if entry not in sys.path:
  101. sys.path.append(entry)
  102. added.append(entry)
  103. return added