The static py-modules list in pyproject.toml drifted from the source
tree each time the root layout changed. An installed wheel then
raised ModuleNotFoundError on import: hermes_state_common, and every
gateway or CLI start failed. The list missed hermes_state_holders,
mini_swe_runner, and the 15 new hermes_state_* modules from this
branch.
setup.py now derives py_modules from the source tree at build time.
setuptools package discovery sees only directories with an
__init__.py, so root single-file modules need py_modules in every
wheel build. setup() kwargs merge with pyproject.toml, and setup.py
is the only legitimate wheel or sdist builder, so the derived list is
the single source of truth.
The nix build is the only wheel consumer. Its source filter keeps
every root .py file, so the build sandbox derives the same set as the
checkout. Editable installs do not read py_modules: build_editable
never runs bdist_wheel.
Verified: wheel and sdist built with HERMES_NIX_BUILD=1 carry all 38
root modules. The guard still blocks builds without the nix env var.
The sealed uv2nix venv from nix build .#default imports
hermes_state_holders, hermes_state_sessions, mini_swe_runner, and
hermes_startup_watchdog.
(cherry picked from commit e23d467b52)
Invariant test updated to pin the derived list (no static py-modules; every root module packaged code imports ships).
100 lines
4.0 KiB
Python
100 lines
4.0 KiB
Python
"""
|
|
setup.py — wheel/sdist build guard.
|
|
|
|
pip/PyPI and Homebrew are no longer supported distribution methods for
|
|
Hermes Agent (see website/docs/getting-started/platform-support.md). The
|
|
wheel would ship without bundled assets (locales, skills, optional-mcps,
|
|
web_dist, tui_dist, plugin manifests) since those are resolved at runtime
|
|
via env-var overrides set by the nix wrapper or the source-checkout layout.
|
|
|
|
This file overrides the ``bdist_wheel`` and ``sdist`` setuptools commands
|
|
to raise an error when run outside a Nix build. The PEP 517
|
|
``build_wheel`` / ``build_sdist`` hooks in
|
|
``setuptools.build_meta`` call these commands internally, so the guard
|
|
fires for ``uv build``, ``pip wheel``, ``python -m build``, and direct
|
|
``setup.py`` invocations alike.
|
|
|
|
The one legitimate consumer of ``build_wheel`` is uv2nix, which calls
|
|
``setuptools.build_meta.build_wheel`` (→ ``bdist_wheel``) inside a Nix
|
|
build sandbox. ``nix/python.nix`` sets ``HERMES_NIX_BUILD=1`` on the
|
|
Hermes package derivation, so only that build may create an artifact.
|
|
|
|
Editable installs (``uv sync``, ``pip install -e .``, ``nix develop``)
|
|
use ``build_editable``, which does NOT call ``bdist_wheel`` — it calls
|
|
``build_ext`` in editable mode. So the guard does not affect development.
|
|
"""
|
|
|
|
import os
|
|
|
|
from setuptools import setup
|
|
from setuptools.command.sdist import sdist
|
|
|
|
_ROOT = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
_IN_NIX_BUILD = os.environ.get("HERMES_NIX_BUILD") == "1"
|
|
|
|
_BLOCK_MESSAGE = (
|
|
"Building wheels or sdists for hermes-agent is not supported.\n"
|
|
"Hermes is distributed via the shell installer, Docker image, or Nix.\n"
|
|
"See: https://hermes-agent.nousresearch.com/docs/getting-started/installation\n"
|
|
"\n"
|
|
"If you are developing, use an editable install instead:\n"
|
|
" uv sync # or: uv pip install -e .\n"
|
|
"\n"
|
|
"If you are building with Nix (uv2nix), this error should not fire —\n"
|
|
"the Hermes Nix derivation sets HERMES_NIX_BUILD=1. If it does, file a bug."
|
|
)
|
|
|
|
|
|
class _GuardedSdist(sdist):
|
|
def run(self, *args, **kwargs):
|
|
if not _IN_NIX_BUILD:
|
|
raise RuntimeError(_BLOCK_MESSAGE)
|
|
return super().run(*args, **kwargs)
|
|
|
|
|
|
cmdclass = {"sdist": _GuardedSdist}
|
|
|
|
# bdist_wheel is only available when the `wheel` package is installed.
|
|
# setuptools.build_meta.build_wheel() calls it internally, so the guard
|
|
# fires for all PEP 517 wheel build paths. Define the subclass only when
|
|
# the import succeeds — otherwise a None base class raises TypeError at
|
|
# class-definition time, before the cmdclass guard can run.
|
|
try:
|
|
from setuptools.command.bdist_wheel import bdist_wheel
|
|
|
|
class _GuardedBdistWheel(bdist_wheel):
|
|
def run(self, *args, **kwargs):
|
|
if not _IN_NIX_BUILD:
|
|
raise RuntimeError(_BLOCK_MESSAGE)
|
|
return super().run(*args, **kwargs)
|
|
|
|
cmdclass["bdist_wheel"] = _GuardedBdistWheel
|
|
except ImportError:
|
|
pass
|
|
|
|
# Root single-file modules (``run_agent``, ``hermes_state``, ``toolsets``...)
|
|
# are invisible to ``packages.find``: that finder sees only directories with an
|
|
# ``__init__.py``. The wheel build needs them on ``py_modules``, so derive the
|
|
# list from the source tree at build time. A static list in ``pyproject.toml``
|
|
# drifted each time the tree layout changed (missing modules broke installed
|
|
# wheels with ``ModuleNotFoundError``), so there is no list to maintain here.
|
|
# ``setup()`` kwargs merge with ``pyproject.toml``, and this file is the only
|
|
# legitimate wheel/sdist builder, so the derived value is the single source.
|
|
# Editable installs do not read it: ``build_editable`` never runs
|
|
# ``bdist_wheel``. The filter source (``nix/lib.nix`` ``pythonSrc``) keeps
|
|
# every root ``.py`` file, so the build sandbox sees the same set of files.
|
|
def _root_py_modules():
|
|
try:
|
|
names = os.listdir(_ROOT)
|
|
except OSError:
|
|
return []
|
|
return sorted(
|
|
name[:-3]
|
|
for name in names
|
|
if name.endswith(".py") and name != "setup.py"
|
|
)
|
|
|
|
|
|
setup(cmdclass=cmdclass, py_modules=_root_py_modules())
|