fix(packaging): ship bundled skills in wheel

Salvages #23738 by @LeonSGP43. Wheel installs were missing skills/ and
optional-skills/ because pyproject's [tool.setuptools.packages.find]
only includes Python packages — the skills directories don't have
__init__.py so they were silently dropped from the wheel.

Adds setup.py with data_files spec emitting skills/* and optional-skills/*
under hermes_agent-<v>.data/data/, and a get_bundled_skills_dir() helper
in hermes_constants that discovers the wheel-installed location via
sysconfig before falling back to a source-checkout path. tools/skills_sync
uses the helper so 'hermes update' works for pip-installed users.
This commit is contained in:
LeonSGP43
2026-05-18 20:52:29 -07:00
committed by Teknium
parent 5fdcfd851f
commit 3a7ed7be08
3 changed files with 73 additions and 6 deletions

View File

@ -5,6 +5,7 @@ without risk of circular imports.
""" """
import os import os
import sysconfig
from contextvars import ContextVar, Token from contextvars import ContextVar, Token
from pathlib import Path from pathlib import Path
@ -139,6 +140,23 @@ def get_default_hermes_root() -> Path:
return env_path return env_path
def _get_packaged_data_dir(name: str) -> Path | None:
"""Return an installed data-files directory if one exists.
Used to discover bundled skills/optional-skills when Hermes is installed
from a wheel that emitted them via setuptools data_files.
"""
candidates = []
for scheme in ("data", "purelib", "platlib"):
raw = sysconfig.get_path(scheme)
if raw:
candidates.append(Path(raw) / name)
for candidate in candidates:
if candidate.exists():
return candidate
return None
def get_optional_skills_dir(default: Path | None = None) -> Path: def get_optional_skills_dir(default: Path | None = None) -> Path:
"""Return the optional-skills directory, honoring package-manager wrappers. """Return the optional-skills directory, honoring package-manager wrappers.
@ -148,11 +166,34 @@ def get_optional_skills_dir(default: Path | None = None) -> Path:
override = os.getenv("HERMES_OPTIONAL_SKILLS", "").strip() override = os.getenv("HERMES_OPTIONAL_SKILLS", "").strip()
if override: if override:
return Path(override) return Path(override)
packaged = _get_packaged_data_dir("optional-skills")
if packaged is not None:
return packaged
if default is not None: if default is not None:
return default return default
return get_hermes_home() / "optional-skills" return get_hermes_home() / "optional-skills"
def get_bundled_skills_dir(default: Path | None = None) -> Path:
"""Return the bundled skills directory for source and packaged installs.
Resolution order:
1. ``HERMES_BUNDLED_SKILLS`` env var (Nix wrapper / explicit override)
2. Wheel-installed ``<sysconfig data>/skills`` (pip install path)
3. Caller-supplied ``default`` (typically the source-checkout path)
4. ``<HERMES_HOME>/skills`` last-resort
"""
override = os.getenv("HERMES_BUNDLED_SKILLS", "").strip()
if override:
return Path(override)
packaged = _get_packaged_data_dir("skills")
if packaged is not None:
return packaged
if default is not None:
return default
return get_hermes_home() / "skills"
def get_hermes_dir(new_subpath: str, old_name: str) -> Path: def get_hermes_dir(new_subpath: str, old_name: str) -> Path:
"""Resolve a Hermes subdirectory with backward compatibility. """Resolve a Hermes subdirectory with backward compatibility.

28
setup.py Normal file
View File

@ -0,0 +1,28 @@
from __future__ import annotations
from collections import defaultdict
from pathlib import Path
from setuptools import setup
REPO_ROOT = Path(__file__).parent.resolve()
def _data_file_tree(root_name: str) -> list[tuple[str, list[str]]]:
root = REPO_ROOT / root_name
grouped: defaultdict[str, list[str]] = defaultdict(list)
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
rel_path = path.relative_to(REPO_ROOT)
grouped[str(rel_path.parent)].append(str(rel_path))
return sorted(grouped.items())
setup(
data_files=[
*_data_file_tree("skills"),
*_data_file_tree("optional-skills"),
]
)

View File

@ -26,7 +26,7 @@ import logging
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home from hermes_constants import get_bundled_skills_dir, get_hermes_home
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
from utils import atomic_replace from utils import atomic_replace
@ -42,12 +42,10 @@ def _get_bundled_dir() -> Path:
"""Locate the bundled skills/ directory. """Locate the bundled skills/ directory.
Checks HERMES_BUNDLED_SKILLS env var first (set by Nix wrapper), Checks HERMES_BUNDLED_SKILLS env var first (set by Nix wrapper),
then falls back to the relative path from this source file. then a wheel-installed data dir, then falls back to the relative
path from this source file.
""" """
env_override = os.getenv("HERMES_BUNDLED_SKILLS") return get_bundled_skills_dir(Path(__file__).parent.parent / "skills")
if env_override:
return Path(env_override)
return Path(__file__).parent.parent / "skills"
def _read_manifest() -> Dict[str, str]: def _read_manifest() -> Dict[str, str]: