Skip to content

Commit f63bf62

Browse files
authored
Merge pull request #84 from python-project-templates/fix/pr-82-musllinux
Add musllinux and Alpine build support
2 parents 5feeccb + db17889 commit f63bf62

2 files changed

Lines changed: 174 additions & 7 deletions

File tree

‎hatch_rs/structs.py‎

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from tempfile import TemporaryDirectory
1616
from typing import Any, Literal
1717

18-
from packaging.tags import cpython_tags, mac_platforms
18+
from packaging.tags import cpython_tags, mac_platforms, platform_tags
1919
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
2020

2121
__all__ = (
@@ -102,13 +102,18 @@ class CopiedArtifact:
102102
"i686": "i686-unknown-linux-musl",
103103
"aarch64": "aarch64-unknown-linux-musl",
104104
"armv7": "armv7-unknown-linux-musleabihf",
105+
"loongarch64": "loongarch64-unknown-linux-musl",
106+
"ppc64le": "powerpc64le-unknown-linux-musl",
107+
"s390x": "s390x-unknown-linux-musl",
108+
"riscv64": "riscv64gc-unknown-linux-musl",
105109
}
106110

107111
WHEEL_ARCHES = {
108112
"x86_64": "x86_64",
109113
"i686": "i686",
110114
"aarch64": "aarch64",
111115
"armv7": "armv7l",
116+
"loongarch64": "loongarch64",
112117
"ppc64le": "ppc64le",
113118
"s390x": "s390x",
114119
"riscv64": "riscv64",
@@ -122,6 +127,16 @@ def _env_truthy(value: str | None) -> bool:
122127
return value.strip().lower() not in ("", "0", "false", "no", "off")
123128

124129

130+
def _is_musl_linux() -> bool:
131+
"""Return True if running in a musl Linux environment or container."""
132+
auditwheel = environ.get("AUDITWHEEL_PLAT", "")
133+
if auditwheel.startswith("musllinux"):
134+
return True
135+
if sys_platform == "linux":
136+
return any(tag.startswith("musllinux") for tag in platform_tags())
137+
return False
138+
139+
125140
def _normalize_machine(machine: str) -> str:
126141
normalized = machine.lower().replace("-", "_")
127142
aliases = {
@@ -147,15 +162,15 @@ def _normalize_platform(platform: str) -> str:
147162
return "win32"
148163
if normalized.startswith("macosx") or normalized == "darwin":
149164
return "darwin"
150-
if normalized.startswith(("linux", "manylinux", "musllinux")):
165+
if normalized.startswith(("linux", "manylinux", "musl")):
151166
return "linux"
152167
if normalized.startswith(("emscripten", "pyemscripten")):
153168
return "emscripten"
154169
return normalized
155170

156171

157172
def _linux_targets_for_platform(platform: str) -> dict[str, str]:
158-
if platform.lower().startswith("musllinux"):
173+
if platform.lower().startswith("musl"):
159174
return LINUX_MUSL_TARGETS
160175
return LINUX_GNU_TARGETS
161176

@@ -236,7 +251,9 @@ def python_extension_name(
236251

237252

238253
def _resolve_target(target: str | None = None, *, platform: str | None = None, machine: str | None = None) -> ResolvedTarget:
239-
raw_platform = platform or environ.get("HATCH_RUST_PLATFORM", sys_platform)
254+
raw_platform = platform or environ.get("HATCH_RUST_PLATFORM")
255+
if raw_platform is None:
256+
raw_platform = "musllinux" if _is_musl_linux() else sys_platform
240257
platform = _normalize_platform(raw_platform)
241258
machine = _normalize_machine(machine or environ.get("HATCH_RUST_MACHINE", platform_machine()))
242259
target = target or environ.get("CARGO_BUILD_TARGET")
@@ -283,6 +300,12 @@ def _linux_wheel_platform(resolved_target: ResolvedTarget, platform_tag: str | N
283300
if arch is None:
284301
raise _unsupported_machine("Linux wheel", resolved_target.machine, WHEEL_ARCHES)
285302
if "musl" in resolved_target.triple:
303+
detected_platform = next(
304+
(tag for tag in platform_tags() if tag.startswith("musllinux_") and tag.endswith(f"_{arch}")),
305+
None,
306+
)
307+
if detected_platform is not None:
308+
return detected_platform
286309
return f"musllinux_1_2_{arch}"
287310
return f"linux_{arch}"
288311

@@ -857,13 +880,36 @@ def _build_artifact_plan(self, artifact: RustArtifactConfig, *, global_target: s
857880
build_command.append("--frozen")
858881
build_command.extend(self._artifact_cargo_args(artifact))
859882

883+
user_rustc_args = self._artifact_rustc_args(artifact)
884+
user_crate_types = []
885+
for index, argument in enumerate(user_rustc_args):
886+
if argument == "--crate-type" and index + 1 < len(user_rustc_args):
887+
user_crate_types.extend(user_rustc_args[index + 1].split(","))
888+
elif argument.startswith("--crate-type="):
889+
user_crate_types.extend(argument.split("=", 1)[1].split(","))
890+
crate_types = {crate_type.strip() for crate_type in (user_crate_types or artifact.crate_type.split(","))}
891+
860892
rustc_args = []
861893
if self._is_python_extension_artifact(artifact) and "apple" in resolved_target.triple:
862894
rustc_args.extend(("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"))
863-
rustc_args.extend(self._artifact_rustc_args(artifact))
895+
if (
896+
"musl" in resolved_target.triple
897+
and not self._is_executable_artifact(artifact)
898+
and (not crate_types.isdisjoint(("cdylib", "dylib", "proc-macro")) or self._is_python_extension_artifact(artifact))
899+
):
900+
target_features = (
901+
feature.strip().lstrip("+-")
902+
for argument in user_rustc_args
903+
if argument.startswith(("target-feature=", "-Ctarget-feature=", "--codegen=target-feature="))
904+
for feature in argument.split("target-feature=", 1)[1].split(",")
905+
)
906+
if "crt-static" not in target_features:
907+
rustc_args.extend(("-C", "target-feature=-crt-static"))
908+
rustc_args.extend(user_rustc_args)
864909
# Executables (bin/example) are not crate-type artifacts; injecting
865910
# --crate-type would build them as a library instead of a binary.
866-
if "--crate-type" not in rustc_args and not self._is_executable_artifact(artifact):
911+
has_crate_type_arg = any(argument == "--crate-type" or argument.startswith("--crate-type=") for argument in user_rustc_args)
912+
if not has_crate_type_arg and not self._is_executable_artifact(artifact):
867913
rustc_args.extend(("--crate-type", artifact.crate_type))
868914
if rustc_args:
869915
build_command.append("--")

‎hatch_rs/tests/test_structs.py‎

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@
66

77
import pytest
88

9-
from hatch_rs.structs import HatchRustBuildPlan, executable_name, python_extension_name, resolve_target_triple, shared_library_name, wheel_tag
9+
from hatch_rs.structs import (
10+
HatchRustBuildPlan,
11+
RustArtifactConfig,
12+
executable_name,
13+
python_extension_name,
14+
resolve_target_triple,
15+
shared_library_name,
16+
wheel_tag,
17+
)
1018

1119

1220
@pytest.fixture(autouse=True)
1321
def clear_cargo_target_dir(monkeypatch: pytest.MonkeyPatch) -> None:
1422
monkeypatch.delenv("CARGO_TARGET_DIR", raising=False)
1523
monkeypatch.delenv("CARGO_BUILD_TARGET", raising=False)
1624
monkeypatch.delenv("PYODIDE_ABI_VERSION", raising=False)
25+
monkeypatch.delenv("AUDITWHEEL_PLAT", raising=False)
1726

1827

1928
@pytest.mark.parametrize(
@@ -53,6 +62,23 @@ def test_resolve_target_triple_uses_cargo_build_target(monkeypatch: pytest.Monke
5362
assert resolve_target_triple(platform="linux", machine="x86_64") == "wasm32-unknown-emscripten"
5463

5564

65+
@pytest.mark.parametrize(
66+
("machine", "expected"),
67+
[
68+
("x86_64", "x86_64-unknown-linux-musl"),
69+
("aarch64", "aarch64-unknown-linux-musl"),
70+
("loongarch64", "loongarch64-unknown-linux-musl"),
71+
("ppc64le", "powerpc64le-unknown-linux-musl"),
72+
("s390x", "s390x-unknown-linux-musl"),
73+
("riscv64", "riscv64gc-unknown-linux-musl"),
74+
],
75+
)
76+
def test_resolve_target_triple_uses_musllinux_auditwheel_plat(monkeypatch: pytest.MonkeyPatch, machine: str, expected: str):
77+
monkeypatch.setenv("AUDITWHEEL_PLAT", f"musllinux_1_2_{machine}")
78+
79+
assert resolve_target_triple(machine=machine) == expected
80+
81+
5682
def test_resolve_target_triple_rejects_wheel_platform_tag_as_rust_target():
5783
with pytest.raises(ValueError, match="manylinux.*wheel platform tags"):
5884
resolve_target_triple("x86_64-manylinux_2_28")
@@ -84,6 +110,12 @@ def test_wheel_tag_uses_auditwheel_platform(monkeypatch: pytest.MonkeyPatch):
84110
assert wheel_tag(target="x86_64-unknown-linux-gnu", python_version=(3, 11)) == "cp311-cp311-manylinux_2_28_x86_64"
85111

86112

113+
def test_wheel_tag_uses_detected_musllinux_version(monkeypatch: pytest.MonkeyPatch):
114+
monkeypatch.setattr("hatch_rs.structs.platform_tags", lambda: iter(["linux_x86_64", "musllinux_1_1_x86_64"]))
115+
116+
assert wheel_tag(target="x86_64-unknown-linux-musl", python_version=(3, 11)) == "cp311-cp311-musllinux_1_1_x86_64"
117+
118+
87119
def test_wheel_tag_uses_pyodide_abi(monkeypatch: pytest.MonkeyPatch):
88120
monkeypatch.setenv("PYODIDE_ABI_VERSION", "2026_0")
89121

@@ -192,6 +224,95 @@ def test_build_plan_generates_cargo_invocation(tmp_path):
192224
]
193225

194226

227+
def test_build_plan_generates_musl_cdylib_flags(tmp_path):
228+
plan = HatchRustBuildPlan(module="project", path=tmp_path, target="x86_64-unknown-linux-musl")
229+
230+
assert plan.generate() == ["cargo rustc --release --target x86_64-unknown-linux-musl -- -C target-feature=-crt-static --crate-type cdylib"]
231+
232+
233+
def test_build_plan_preserves_explicit_crt_static_feature_for_musl(tmp_path):
234+
plan = HatchRustBuildPlan(
235+
module="project",
236+
path=tmp_path,
237+
target="x86_64-unknown-linux-musl",
238+
rustc_args=["-C", "target-feature=+crt-static"],
239+
)
240+
241+
assert plan.generate() == ["cargo rustc --release --target x86_64-unknown-linux-musl -- -C target-feature=+crt-static --crate-type cdylib"]
242+
243+
244+
def test_build_plan_combines_musl_crt_flag_with_custom_target_features(tmp_path):
245+
plan = HatchRustBuildPlan(
246+
module="project",
247+
path=tmp_path,
248+
target="x86_64-unknown-linux-musl",
249+
rustc_args=["-C", "target-feature=+sse2"],
250+
)
251+
252+
assert plan.generate() == [
253+
"cargo rustc --release --target x86_64-unknown-linux-musl -- -C target-feature=-crt-static -C target-feature=+sse2 --crate-type cdylib"
254+
]
255+
256+
257+
def test_build_plan_detects_cdylib_in_multiple_crate_types(tmp_path):
258+
plan = HatchRustBuildPlan(
259+
module="project",
260+
path=tmp_path,
261+
target="x86_64-unknown-linux-musl",
262+
artifacts=[{"name": "project", "crate-type": "staticlib,cdylib"}],
263+
)
264+
265+
assert plan.generate() == [
266+
"cargo rustc --release --target x86_64-unknown-linux-musl -- -C target-feature=-crt-static --crate-type staticlib,cdylib"
267+
]
268+
269+
270+
@pytest.mark.parametrize("crate_type", ["dylib", "proc-macro"])
271+
def test_build_plan_disables_static_crt_for_other_dynamic_crate_types(tmp_path, crate_type: str):
272+
plan = HatchRustBuildPlan(
273+
module="project",
274+
path=tmp_path,
275+
target="x86_64-unknown-linux-musl",
276+
artifacts=[{"name": "project", "crate-type": crate_type}],
277+
)
278+
279+
assert "target-feature=-crt-static" in plan.generate()[0]
280+
281+
282+
@pytest.mark.parametrize("rustc_args", [["--crate-type", "staticlib"], ["--crate-type=staticlib"]])
283+
def test_build_plan_preserves_static_crt_for_explicit_staticlib(tmp_path, rustc_args: list[str]):
284+
plan = HatchRustBuildPlan(
285+
module="project",
286+
path=tmp_path,
287+
target="x86_64-unknown-linux-musl",
288+
artifacts=[{"name": "project", "rustc-args": rustc_args}],
289+
)
290+
291+
command = plan.generate()[0]
292+
assert "target-feature=-crt-static" not in command
293+
assert command.count("--crate-type") == 1
294+
295+
296+
def test_build_plan_does_not_disable_crt_static_for_musl_executable(tmp_path):
297+
plan = HatchRustBuildPlan(
298+
module="project",
299+
path=tmp_path,
300+
target="x86_64-unknown-linux-musl",
301+
artifacts=[
302+
RustArtifactConfig(
303+
name="mycli",
304+
cargo_target="mycli",
305+
cargo_target_kind="bin",
306+
install_scheme="shared-scripts",
307+
)
308+
],
309+
)
310+
311+
assert plan.generate() == ["cargo rustc --bin mycli --release --target x86_64-unknown-linux-musl"]
312+
invocation = plan.cargo_invocations[0]
313+
assert "target-feature=-crt-static" not in invocation.env.get("RUSTFLAGS", "")
314+
315+
195316
def test_build_plan_uses_debug_profile_for_editable_install(tmp_path):
196317
plan = HatchRustBuildPlan(
197318
module="project",

0 commit comments

Comments
 (0)