diff --git a/hatch_rs/structs.py b/hatch_rs/structs.py index f135a56..5b45ff8 100644 --- a/hatch_rs/structs.py +++ b/hatch_rs/structs.py @@ -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__ = ( @@ -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 = { @@ -147,7 +157,7 @@ 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" @@ -155,7 +165,7 @@ def _normalize_platform(platform: str) -> str: 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 @@ -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") @@ -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. diff --git a/hatch_rs/tests/test_structs.py b/hatch_rs/tests/test_structs.py index d97f742..70126fd 100644 --- a/hatch_rs/tests/test_structs.py +++ b/hatch_rs/tests/test_structs.py @@ -6,7 +6,15 @@ 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) @@ -14,6 +22,7 @@ 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( @@ -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") @@ -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",