Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -312,29 +312,6 @@ hermes-acp = "acp_adapter.entry:main"
[tool.setuptools]
py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli", "hermes_bootstrap", "hermes_constants", "hermes_state", "hermes_time", "hermes_logging", "utils", "mcp_serve"]

[tool.setuptools.data-files]
# i18n catalogs. locales/ is a bare data directory (no __init__.py), so it is
# neither a package (packages.find) nor package-data (which attaches to a
# package). data-files ships it in the wheel; MANIFEST.in `graft locales`
# ships it in the sdist. Without this, sealed installs (pip wheel, Nix store
# venv) drop the catalogs and gateway/CLI commands surface raw i18n keys like
# `gateway.reset.header_default` (#27632, #35374, #23943).
locales = ["locales/*.yaml"]
# Shipped MCP catalog (optional-mcps/<name>/manifest.yaml). Same bare-data-dir
# case as locales: data-files ships it in the wheel, `graft optional-mcps` in
# MANIFEST.in ships it in the sdist. Without this, `hermes mcp catalog` and the
# dashboard catalog screen come up empty on packaged installs even though the
# manifests exist in the repo (hermes_cli/mcp_catalog.py:_catalog_root resolves
# the packaged dir; list_catalog() returns [] when it's missing).
#
# data-files flattens every glob match into its single target dir, so each
# catalog entry needs its OWN target to preserve the per-entry directory the
# catalog iterates over (a shared `optional-mcps/*/*` glob would collapse all
# manifests into one colliding optional-mcps/manifest.yaml). One target per
# entry; tests/test_packaging_metadata.py enforces an entry per optional-mcps/<name>.
"optional-mcps/linear" = ["optional-mcps/linear/manifest.yaml"]
"optional-mcps/n8n" = ["optional-mcps/n8n/manifest.yaml"]

[tool.setuptools.package-data]
hermes_cli = ["web_dist/**/*", "tui_dist/**/*", "scripts/install.sh", "scripts/install.ps1"]
gateway = ["assets/**/*"]
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,7 @@ def _data_file_tree(root_name: str) -> list[tuple[str, list[str]]]:
data_files=[
*_data_file_tree("skills"),
*_data_file_tree("optional-skills"),
*_data_file_tree("locales"),
*_data_file_tree("optional-mcps"),
]
)
23 changes: 12 additions & 11 deletions tests/test_packaging_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,27 +241,28 @@ def test_locked_starlette_is_not_vulnerable_to_cve_2026_48710():
)


def test_locale_catalogs_ship_in_both_wheel_and_sdist():
def test_locale_catalogs_covered_by_data_file_tree():
"""Regression test for #27632 / #35374 / #23943.

locales/ is a bare data directory (no __init__.py), so it is invisible to
packages.find and to package-data (which attaches to a package). It must be
declared as setuptools data-files (wheel) AND grafted in MANIFEST.in
(sdist). Without both, sealed installs drop the catalogs and gateway/CLI
commands surface raw i18n keys like `gateway.reset.header_default`.
covered by setup.py's ``_data_file_tree('locales')`` call (wheel) AND
grafted in MANIFEST.in (sdist). Without both, sealed installs drop the
catalogs and gateway/CLI commands surface raw i18n keys like
`gateway.reset.header_default`.
"""
data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
data_files = data["tool"]["setuptools"].get("data-files", {})
assert data_files.get("locales") == ["locales/*.yaml"], (
"pyproject [tool.setuptools.data-files] must declare "
'locales = ["locales/*.yaml"] so the wheel ships i18n catalogs'
)

manifest = (REPO_ROOT / "MANIFEST.in").read_text(encoding="utf-8")
assert "graft locales" in manifest, (
"MANIFEST.in must `graft locales` so the sdist ships i18n catalogs"
)

# Verify setup.py calls _data_file_tree('locales') so the wheel includes them.
src = (REPO_ROOT / "setup.py").read_text(encoding="utf-8")
assert '_data_file_tree("locales")' in src or "_data_file_tree('locales')" in src, (
"setup.py must call _data_file_tree('locales') in data_files so the "
"wheel ships i18n catalogs"
)

# Every on-disk catalog has the .yaml extension the globs above match.
on_disk = list((REPO_ROOT / "locales").glob("*.yaml"))
assert on_disk, "expected locales/*.yaml catalogs on disk"
Expand Down
Loading