vendor_path.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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. from typing import List
  29. VENDOR_DIR = Path(__file__).parent / "vendor"
  30. # Directory name per (system, machine) — keep in sync with PLATFORMS in
  31. # vendor_chattolib.sh.
  32. _MACHINE_ALIASES = {
  33. "x86_64": "x86_64",
  34. "amd64": "x86_64",
  35. "aarch64": "aarch64",
  36. "arm64": "aarch64",
  37. }
  38. def _is_musl() -> bool:
  39. """True when running against musl libc (Alpine) rather than glibc."""
  40. try:
  41. libc, _version = platform.libc_ver()
  42. except OSError:
  43. return False
  44. # glibc reports "glibc"; musl reports an empty string.
  45. return libc != "glibc"
  46. def platform_tag() -> str:
  47. """Return the vendor/platform/ subdirectory name for this interpreter."""
  48. system = platform.system().lower()
  49. machine = _MACHINE_ALIASES.get(platform.machine().lower(), platform.machine().lower())
  50. if system == "linux":
  51. return f"linux-{machine}-musl" if _is_musl() else f"linux-{machine}"
  52. if system == "darwin":
  53. return f"macos-{'arm64' if machine == 'aarch64' else machine}"
  54. if system == "windows":
  55. return f"windows-{'amd64' if machine == 'x86_64' else machine}"
  56. return f"{system}-{machine}"
  57. def available_platforms() -> List[str]:
  58. """Platform directories that were actually vendored into this checkout."""
  59. platform_root = VENDOR_DIR / "platform"
  60. if not platform_root.is_dir():
  61. return []
  62. return sorted(p.name for p in platform_root.iterdir() if p.is_dir())
  63. def setup_vendor_path() -> List[str]:
  64. """Put the vendored packages for this platform on ``sys.path``.
  65. The compiled extensions are prepended, the pure-Python packages appended;
  66. see the module docstring for why. Returns the directories that were added.
  67. Raises ImportError when this platform was not vendored — a clear message
  68. beats the dlopen error you would otherwise get from a foreign-architecture
  69. .so file.
  70. """
  71. tag = platform_tag()
  72. platform_dir = VENDOR_DIR / "platform" / tag
  73. common_dir = VENDOR_DIR / "common"
  74. # Legacy flat layout (single platform, everything directly in vendor/).
  75. if not platform_dir.is_dir() and not common_dir.is_dir():
  76. if (VENDOR_DIR / "chattolib").is_dir():
  77. prepend, append = [VENDOR_DIR], []
  78. else:
  79. raise ImportError(
  80. f"Chatto: no vendored dependencies found in {VENDOR_DIR}. "
  81. f"Run ./vendor_chattolib.sh to build them."
  82. )
  83. elif not platform_dir.is_dir():
  84. raise ImportError(
  85. f"Chatto: no vendored dependencies for platform '{tag}'. "
  86. f"Available: {', '.join(available_platforms()) or 'none'}. "
  87. f"Run 'PLATFORMS={tag} ./vendor_chattolib.sh' to add it."
  88. )
  89. else:
  90. prepend, append = [platform_dir], [common_dir]
  91. added = []
  92. for path in prepend:
  93. entry = str(path)
  94. if entry not in sys.path:
  95. sys.path.insert(0, entry)
  96. added.append(entry)
  97. for path in append:
  98. entry = str(path)
  99. if entry not in sys.path:
  100. sys.path.append(entry)
  101. added.append(entry)
  102. return added