Skip to content

Commit 310cbb5

Browse files
authored
[hnswlib] Fix SIGILL in stubtest by building without -march=native (#16244)
hnswlib is source-only and compiles with -march=native by default. CI restores pip's wheel cache across runners with different CPUs, so a wheel built on one host can hit an illegal instruction on the next. That is the SIGILL (exit -4) with empty output from #16100. Add a general `install-environment` key to [tool.stubtest] that sets environment variables for the pip install step, and use it to pass hnswlib's own HNSWLIB_NO_NATIVE opt-out. This lets the darwin-only workaround from #16125 be dropped so hnswlib is tested on Linux again.
1 parent 7ea6796 commit 310cbb5

4 files changed

Lines changed: 25 additions & 4 deletions

File tree

CONTRIBUTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ when running stubtest. For example: `mypy-plugins = ["mypy_django_plugin.main"]`
235235
* `mypy-plugins-config` (default: `{}`): A dictionary mapping plugin names to their
236236
configuration dictionaries for use by mypy plugins. For example:
237237
`mypy-plugins-config = {"django-stubs" = {"django_settings_module" = "@tests.django_settings"}}`
238+
* `install-environment` (default: `{}`): A dictionary of environment variables
239+
to set while `pip install`ing the package and its dependencies for stubtest.
240+
Useful for packages that read build-time options from the environment, for
241+
example to disable CPU-specific compiler flags that do not survive CI's
242+
shared wheel cache. For example:
243+
`install-environment = { HNSWLIB_NO_NATIVE = "1" }`
238244

239245
`*-dependencies` are usually packages needed to `pip install` the implementation
240246
distribution.

lib/ts_utils/metadata.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ def _is_nested_dict(obj: object) -> TypeGuard[dict[str, dict[str, Any]]]:
5353
return isinstance(obj, dict) and all(isinstance(k, str) and isinstance(v, dict) for k, v in obj.items())
5454

5555

56+
def _is_dict_of_strings(obj: object) -> TypeGuard[dict[str, str]]:
57+
return isinstance(obj, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in obj.items())
58+
59+
5660
@functools.cache
5761
def get_oldest_supported_python() -> str:
5862
with PYPROJECT_PATH.open("rb") as config:
@@ -85,6 +89,7 @@ class StubtestSettings:
8589
stubtest_dependencies: list[str]
8690
mypy_plugins: list[str]
8791
mypy_plugins_config: dict[str, dict[str, Any]]
92+
install_environment: dict[str, str]
8893

8994
def system_requirements_for_platform(self, platform: str) -> list[str]:
9095
assert platform in _STUBTEST_PLATFORM_MAPPING, f"Unrecognised platform {platform!r}"
@@ -110,6 +115,7 @@ def read_stubtest_settings(distribution: str) -> StubtestSettings:
110115
stubtest_dependencies: object = data.get("stubtest-dependencies", [])
111116
mypy_plugins: object = data.get("mypy-plugins", [])
112117
mypy_plugins_config: object = data.get("mypy-plugins-config", {})
118+
install_environment: object = data.get("install-environment", {})
113119

114120
assert type(skip) is bool
115121
assert type(ignore_missing_stub) is bool
@@ -124,6 +130,7 @@ def read_stubtest_settings(distribution: str) -> StubtestSettings:
124130
assert _is_list_of_strings(stubtest_dependencies)
125131
assert _is_list_of_strings(mypy_plugins)
126132
assert _is_nested_dict(mypy_plugins_config)
133+
assert _is_dict_of_strings(install_environment)
127134

128135
unrecognised_platforms = set(ci_platforms) - _STUBTEST_PLATFORM_MAPPING.keys()
129136
assert not unrecognised_platforms, f"Unrecognised ci-platforms specified for {distribution!r}: {unrecognised_platforms}"
@@ -152,6 +159,7 @@ def read_stubtest_settings(distribution: str) -> StubtestSettings:
152159
stubtest_dependencies=stubtest_dependencies,
153160
mypy_plugins=mypy_plugins,
154161
mypy_plugins_config=mypy_plugins_config,
162+
install_environment=install_environment,
155163
)
156164

157165

@@ -227,6 +235,7 @@ def all_dependencies(self) -> list[Requirement]:
227235
"stubtest-dependencies",
228236
"mypy-plugins",
229237
"mypy-plugins-config",
238+
"install-environment",
230239
}
231240
}
232241
_DIST_NAME_RE: Final = re.compile(r"^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$", re.IGNORECASE)

stubs/hnswlib/METADATA.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ upstream-repository = "https://github.com/nmslib/hnswlib"
44
dependencies = ["numpy>=1.21"]
55

66
[tool.stubtest]
7-
# TODO: stubtest fails on Linux because it gets killed with a SIGILL
8-
# for unknown reasons. See https://github.com/python/typeshed/issues/16100
9-
ci-platforms = ["darwin"]
7+
# hnswlib is source-only and compiles with -march=native by default. CI
8+
# restores pip's wheel cache across runners with different CPUs, so a wheel
9+
# built on one machine can SIGILL on the next. See
10+
# https://github.com/python/typeshed/issues/16100
11+
install-environment = { HNSWLIB_NO_NATIVE = "1" }

tests/stubtest_third_party.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,12 @@ def run_stubtest(dist: Path, *, verbose: bool = False, ci_platforms_only: bool =
9191
dists_to_install[:] = dists_to_install[1:]
9292

9393
pip_cmd = [pip_exe, "install", *dists_to_install]
94+
# Some packages read environment variables at build time, e.g. to
95+
# opt out of CPU-specific compiler flags. See `install-environment`
96+
# in CONTRIBUTING.md.
97+
pip_env = os.environ | stubtest_settings.install_environment
9498
try:
95-
subprocess.run(pip_cmd, check=True, capture_output=True)
99+
subprocess.run(pip_cmd, env=pip_env, check=True, capture_output=True)
96100
except subprocess.CalledProcessError as e:
97101
print_command_failure("Failed to install", e)
98102
return False

0 commit comments

Comments
 (0)