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
28 changes: 24 additions & 4 deletions hatch_rs/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from tempfile import TemporaryDirectory
from typing import Any, Literal

from packaging.tags import cpython_tags, mac_platforms
from packaging.tags import cpython_tags, mac_platforms, platform_tags
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator

__all__ = (
Expand Down Expand Up @@ -122,6 +122,16 @@ def _env_truthy(value: str | None) -> bool:
return value.strip().lower() not in ("", "0", "false", "no", "off")


def _is_musl_linux() -> bool:
"""Return True if running in a musl Linux environment or container."""
auditwheel = environ.get("AUDITWHEEL_PLAT", "")
if auditwheel.startswith("musllinux"):
return True
if sys_platform == "linux":
return any(tag.startswith("musllinux") for tag in platform_tags())
return False


def _normalize_machine(machine: str) -> str:
normalized = machine.lower().replace("-", "_")
aliases = {
Expand All @@ -147,15 +157,15 @@ def _normalize_platform(platform: str) -> str:
return "win32"
if normalized.startswith("macosx") or normalized == "darwin":
return "darwin"
if normalized.startswith(("linux", "manylinux", "musllinux")):
if normalized.startswith(("linux", "manylinux", "musl")):
return "linux"
if normalized.startswith(("emscripten", "pyemscripten")):
return "emscripten"
return normalized


def _linux_targets_for_platform(platform: str) -> dict[str, str]:
if platform.lower().startswith("musllinux"):
if platform.lower().startswith("musl"):
return LINUX_MUSL_TARGETS
return LINUX_GNU_TARGETS

Expand Down Expand Up @@ -236,7 +246,9 @@ def python_extension_name(


def _resolve_target(target: str | None = None, *, platform: str | None = None, machine: str | None = None) -> ResolvedTarget:
raw_platform = platform or environ.get("HATCH_RUST_PLATFORM", sys_platform)
raw_platform = platform or environ.get("HATCH_RUST_PLATFORM")
if raw_platform is None:
raw_platform = "musllinux" if _is_musl_linux() else sys_platform
platform = _normalize_platform(raw_platform)
machine = _normalize_machine(machine or environ.get("HATCH_RUST_MACHINE", platform_machine()))
target = target or environ.get("CARGO_BUILD_TARGET")
Expand Down Expand Up @@ -860,6 +872,14 @@ def _build_artifact_plan(self, artifact: RustArtifactConfig, *, global_target: s
rustc_args = []
if self._is_python_extension_artifact(artifact) and "apple" in resolved_target.triple:
rustc_args.extend(("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"))
if (
"musl" in resolved_target.triple
and not self._is_executable_artifact(artifact)
and (artifact.crate_type == "cdylib" or self._is_python_extension_artifact(artifact))
):
user_rustc_args = " ".join(self._artifact_rustc_args(artifact))
if "target-feature" not in user_rustc_args:
rustc_args.extend(("-C", "target-feature=-crt-static"))
rustc_args.extend(self._artifact_rustc_args(artifact))
# Executables (bin/example) are not crate-type artifacts; injecting
# --crate-type would build them as a library instead of a binary.
Expand Down
56 changes: 55 additions & 1 deletion hatch_rs/tests/test_structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@

import pytest

from hatch_rs.structs import HatchRustBuildPlan, executable_name, python_extension_name, resolve_target_triple, shared_library_name, wheel_tag
from hatch_rs.structs import (
HatchRustBuildPlan,
RustArtifactConfig,
executable_name,
python_extension_name,
resolve_target_triple,
shared_library_name,
wheel_tag,
)


@pytest.fixture(autouse=True)
def clear_cargo_target_dir(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("CARGO_TARGET_DIR", raising=False)
monkeypatch.delenv("CARGO_BUILD_TARGET", raising=False)
monkeypatch.delenv("PYODIDE_ABI_VERSION", raising=False)
monkeypatch.delenv("AUDITWHEEL_PLAT", raising=False)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -53,6 +62,14 @@ def test_resolve_target_triple_uses_cargo_build_target(monkeypatch: pytest.Monke
assert resolve_target_triple(platform="linux", machine="x86_64") == "wasm32-unknown-emscripten"


def test_resolve_target_triple_uses_musllinux_auditwheel_plat(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("AUDITWHEEL_PLAT", "musllinux_1_2_x86_64")
assert resolve_target_triple(machine="x86_64") == "x86_64-unknown-linux-musl"

monkeypatch.setenv("AUDITWHEEL_PLAT", "musllinux_1_2_aarch64")
assert resolve_target_triple(machine="aarch64") == "aarch64-unknown-linux-musl"


def test_resolve_target_triple_rejects_wheel_platform_tag_as_rust_target():
with pytest.raises(ValueError, match="manylinux.*wheel platform tags"):
resolve_target_triple("x86_64-manylinux_2_28")
Expand Down Expand Up @@ -192,6 +209,43 @@ def test_build_plan_generates_cargo_invocation(tmp_path):
]


def test_build_plan_generates_musl_cdylib_flags(tmp_path):
plan = HatchRustBuildPlan(module="project", path=tmp_path, target="x86_64-unknown-linux-musl")

assert plan.generate() == ["cargo rustc --release --target x86_64-unknown-linux-musl -- -C target-feature=-crt-static --crate-type cdylib"]


def test_build_plan_preserves_custom_target_feature_for_musl(tmp_path):
plan = HatchRustBuildPlan(
module="project",
path=tmp_path,
target="x86_64-unknown-linux-musl",
rustc_args=["-C", "target-feature=+crt-static"],
)

assert plan.generate() == ["cargo rustc --release --target x86_64-unknown-linux-musl -- -C target-feature=+crt-static --crate-type cdylib"]


def test_build_plan_does_not_disable_crt_static_for_musl_executable(tmp_path):
plan = HatchRustBuildPlan(
module="project",
path=tmp_path,
target="x86_64-unknown-linux-musl",
artifacts=[
RustArtifactConfig(
name="mycli",
cargo_target="mycli",
cargo_target_kind="bin",
install_scheme="shared-scripts",
)
],
)

assert plan.generate() == ["cargo rustc --bin mycli --release --target x86_64-unknown-linux-musl"]
invocation = plan.cargo_invocations[0]
assert "target-feature=-crt-static" not in invocation.env.get("RUSTFLAGS", "")


def test_build_plan_uses_debug_profile_for_editable_install(tmp_path):
plan = HatchRustBuildPlan(
module="project",
Expand Down