Skip to content

Commit fd0d29c

Browse files
fix(packaging): guard torch extras on intel macos (#2011)
## Description Closes #1931 Guard the `ml` and `voice` `torch` optional dependencies on macOS x86_64 so `headroom-ai[all]` remains resolvable on Intel Macs where PyTorch does not publish compatible wheels for this version floor. The lockfile metadata is updated with the same markers. ## Type of Change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Refactoring - [ ] Performance improvement - [ ] Test update - [ ] Other ## Changes Made - Added macOS x86_64 environment markers to `torch` in the `ml` and `voice` extras. - Updated `uv.lock` optional dependency metadata to match the guarded extras. - Added a packaging regression test that checks `[all]` keeps `ml` and `voice` while guarding `torch` on macOS x86_64. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Formatting verified (`ruff format --check`) - [ ] Manual testing performed ### Test Output ```text $ python3 -m pytest tests/test_optional_dependencies.py -q collected 1 item tests/test_optional_dependencies.py . [100%] ============================== 1 passed in 0.26s =============================== $ .venv/bin/ruff check tests/test_optional_dependencies.py All checks passed! $ .venv/bin/ruff format --check tests/test_optional_dependencies.py pyproject.toml 1 file already formatted ``` ## Test verification (RED -> GREEN) RED, with the `torch` markers temporarily removed from `pyproject.toml`: ```text tests/test_optional_dependencies.py F [100%] FAILED tests/test_optional_dependencies.py::test_all_extra_does_not_require_torch_on_macos_x86_64 E assert False ``` GREEN, with this patch applied: ```text tests/test_optional_dependencies.py . [100%] ============================== 1 passed in 0.26s =============================== ``` ## Real Behavior Proof - Environment: Linux, Python 3.12.3, pytest 9.1.1, ruff 0.14.14. - Exact command / steps: Removed the environment markers from `torch`, ran the new packaging test, restored the markers, and reran the test plus targeted ruff checks. - Observed result: The test fails without the macOS x86_64 guard and passes once the `ml` and `voice` `torch` requirements are guarded. - Not tested: Full `uv run pytest`, full-project `uv run ruff check .`, full-project `uv run ruff format --check .`, and `uv run mypy headroom` were not run locally for this targeted packaging change. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have added tests that prove my fix is effective - [x] New and existing targeted tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules ## Screenshots (if applicable) N/A ## Additional Notes No new dependency is added; this only narrows when the existing `torch` optional dependency is selected.
1 parent f536aa0 commit fd0d29c

3 files changed

Lines changed: 123 additions & 16 deletions

File tree

pyproject.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ code = [
9999
# (The legacy [llmlingua] extra was removed in 0.9.x — no live code path used it.
100100
# Use [ml] for the supported ML compression dependencies.)
101101
ml = [
102-
"torch>=2.12.1",
102+
# PyTorch does not publish wheels for macOS 15 x86_64 at this floor, which
103+
# makes `headroom-ai[all]` unsatisfiable on Intel Macs (#1931).
104+
"torch>=2.12.1; sys_platform != 'darwin' or platform_machine != 'x86_64'",
103105
"transformers>=4.30.0,<6.0",
104106
# transformers >= 5.x requires huggingface-hub >= 1.5.0,<2.0; pinning
105107
# the floor here prevents Kompress from silently falling back to
@@ -114,7 +116,7 @@ ml = [
114116
# [memory]) fail on any machine without a compiler — see #1368.
115117
memory = [
116118
"sqlite-vec>=0.1.6",
117-
"sentence-transformers>=2.2.0,<6.0",
119+
"sentence-transformers>=2.2.0,<6.0; sys_platform != 'darwin' or platform_machine != 'x86_64'",
118120
]
119121
# Optional HNSW vector backend. Needs a C++ toolchain to build hnswlib, so it is
120122
# kept out of [memory] and [all]; opt in with `pip install headroom-ai[vector]`
@@ -212,7 +214,7 @@ mcp = [
212214
voice = [
213215
"onnxruntime>=1.16.0",
214216
"transformers>=4.30.0,<6.0",
215-
"torch>=2.12.1",
217+
"torch>=2.12.1; sys_platform != 'darwin' or platform_machine != 'x86_64'",
216218
]
217219
# Voice training (includes voice deps + training extras)
218220
voice-train = [
@@ -223,7 +225,7 @@ voice-train = [
223225
# Evaluation framework
224226
evals = [
225227
"datasets>=2.14.0",
226-
"sentence-transformers>=2.2.0,<6.0",
228+
"sentence-transformers>=2.2.0,<6.0; sys_platform != 'darwin' or platform_machine != 'x86_64'",
227229
"numpy>=1.24.0",
228230
"scikit-learn>=1.3.0",
229231
"anthropic>=0.18.0",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from packaging.markers import default_environment
6+
from packaging.requirements import Requirement
7+
8+
try:
9+
import tomllib
10+
except ModuleNotFoundError: # pragma: no cover - Python 3.10 fallback
11+
import tomli as tomllib # type: ignore[no-redef]
12+
13+
14+
ROOT = Path(__file__).resolve().parents[1]
15+
ALL_EXTRA = "all"
16+
HEADROOM_PACKAGE_NAME = "headroom-ai"
17+
MACOS_X86_64_TORCH_GUARD = "sys_platform != 'darwin' or platform_machine != 'x86_64'"
18+
MACOS_X86_64_SYS_PLATFORM = "darwin"
19+
MACOS_X86_64_PLATFORM_MACHINE = "x86_64"
20+
PYPROJECT_FILE = "pyproject.toml"
21+
SYS_PLATFORM_MARKER = "sys_platform"
22+
PLATFORM_MACHINE_MARKER = "platform_machine"
23+
TORCH_PACKAGE_NAME = "torch"
24+
TORCH_TRANSITIVE_PACKAGE_NAMES = frozenset({"sentence-transformers"})
25+
UV_LOCK_FILE = "uv.lock"
26+
27+
28+
def _selected_dependency_names_for_extra(
29+
optional_deps: dict[str, list[str]],
30+
extra_name: str,
31+
environment: dict[str, str],
32+
visited: set[str] | None = None,
33+
) -> set[str]:
34+
selected: set[str] = set()
35+
visited = visited or set()
36+
if extra_name in visited:
37+
return selected
38+
visited.add(extra_name)
39+
40+
for dependency in optional_deps[extra_name]:
41+
requirement = Requirement(dependency)
42+
if requirement.marker is not None and not requirement.marker.evaluate(environment):
43+
continue
44+
if requirement.name == HEADROOM_PACKAGE_NAME:
45+
for nested_extra in requirement.extras:
46+
selected.update(
47+
_selected_dependency_names_for_extra(
48+
optional_deps,
49+
nested_extra,
50+
environment,
51+
visited,
52+
)
53+
)
54+
else:
55+
selected.add(requirement.name)
56+
57+
return selected
58+
59+
60+
def _locked_dependency_names(package_name: str) -> set[str]:
61+
lock = tomllib.loads((ROOT / UV_LOCK_FILE).read_text(encoding="utf-8"))
62+
for package in lock["package"]:
63+
if package["name"] == package_name:
64+
return {dependency["name"] for dependency in package.get("dependencies", [])}
65+
raise AssertionError(f"{package_name} not found in {UV_LOCK_FILE}")
66+
67+
68+
def test_all_extra_does_not_require_torch_on_macos_x86_64() -> None:
69+
"""Keep `headroom-ai[all]` resolvable where PyTorch publishes no wheel."""
70+
71+
pyproject = tomllib.loads((ROOT / PYPROJECT_FILE).read_text(encoding="utf-8"))
72+
optional_deps = pyproject["project"]["optional-dependencies"]
73+
macos_x86_64_environment = default_environment()
74+
macos_x86_64_environment.update(
75+
{
76+
SYS_PLATFORM_MARKER: MACOS_X86_64_SYS_PLATFORM,
77+
PLATFORM_MACHINE_MARKER: MACOS_X86_64_PLATFORM_MACHINE,
78+
}
79+
)
80+
81+
assert "ml" in optional_deps[ALL_EXTRA][0]
82+
assert "voice" in optional_deps[ALL_EXTRA][0]
83+
84+
torch_deps = [
85+
dep
86+
for extra_name in ("ml", "voice")
87+
for dep in optional_deps[extra_name]
88+
if dep.startswith(TORCH_PACKAGE_NAME)
89+
]
90+
selected_all_dependency_names = _selected_dependency_names_for_extra(
91+
optional_deps,
92+
ALL_EXTRA,
93+
macos_x86_64_environment,
94+
)
95+
locked_torch_transitive_dependency_names = {
96+
package_name
97+
for package_name in TORCH_TRANSITIVE_PACKAGE_NAMES
98+
if TORCH_PACKAGE_NAME in _locked_dependency_names(package_name)
99+
}
100+
101+
assert torch_deps
102+
assert all(MACOS_X86_64_TORCH_GUARD in dep for dep in torch_deps)
103+
assert locked_torch_transitive_dependency_names
104+
assert TORCH_PACKAGE_NAME not in selected_all_dependency_names
105+
assert selected_all_dependency_names.isdisjoint(locked_torch_transitive_dependency_names)

uv.lock

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)