Skip to content

Commit a297e84

Browse files
authored
Merge pull request #85 from python-project-templates/fix/abi-and-artifact-targets
Fix ABI tags and executable artifact handling
2 parents 2a2bbbf + 38664a0 commit a297e84

2 files changed

Lines changed: 72 additions & 21 deletions

File tree

‎hatch_rs/structs.py‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -280,16 +280,15 @@ def python_extension_name(
280280
platform = _normalize_platform(platform or environ.get("HATCH_RUST_PLATFORM", sys_platform))
281281
module_name = source_stem.removeprefix("lib")
282282
major, minor = python_version or (version_info.major, version_info.minor)
283+
is_freethreaded = _resolve_free_threaded(free_threaded, python_version=(major, minor)) if abi3 else False
284+
if abi3 and is_freethreaded and (major, minor) < (3, 15):
285+
raise ValueError(f"ABI3 free-threaded builds require Python 3.15 or newer, got {major}.{minor}.")
283286
if platform == "win32":
284287
return f"{module_name}.pyd"
285288
if platform == "emscripten":
286289
return f"{module_name}.cpython-{major}{minor}-wasm32-emscripten.so"
287290
if abi3:
288-
return (
289-
f"{module_name}.abi3t.so"
290-
if _resolve_free_threaded(free_threaded, python_version=(major, minor)) and (major, minor) >= (3, 15)
291-
else f"{module_name}.abi3.so"
292-
)
291+
return f"{module_name}.abi3t.so" if is_freethreaded else f"{module_name}.abi3.so"
293292
return f"{module_name}.so"
294293

295294

@@ -399,13 +398,12 @@ def wheel_tag(
399398
resolved = resolved_target or _resolve_target(target, platform=platform, machine=machine)
400399
version = python_version or (version_info.major, version_info.minor)
401400
is_freethreaded = _resolve_free_threaded(free_threaded, python_version=version)
401+
if abi3 and is_freethreaded and version < (3, 15):
402+
raise ValueError(f"ABI3 free-threaded builds require Python 3.15 or newer, got {version[0]}.{version[1]}.")
402403
if resolved.platform == "emscripten":
403404
abis = [f"cp{version[0]}{version[1]}"]
404405
elif abi3:
405-
if is_freethreaded:
406-
abis = ["abi3t"] if version >= (3, 15) else [f"cp{version[0]}{version[1]}t"]
407-
else:
408-
abis = ["abi3"]
406+
abis = ["abi3.abi3t"] if is_freethreaded else ["abi3"]
409407
elif is_freethreaded:
410408
abis = [f"cp{version[0]}{version[1]}t"]
411409
else:
@@ -831,7 +829,7 @@ def _is_python_extension_artifact(self, artifact: RustArtifactConfig) -> bool:
831829
return artifact.destination is not None and "{python_extension_name}" in artifact.destination
832830

833831
def _is_executable_artifact(self, artifact: RustArtifactConfig) -> bool:
834-
return artifact.cargo_target_kind in ("bin", "example")
832+
return artifact.cargo_target_kind in ("bin", "example", "test", "bench")
835833

836834
def _artifact_role(self, artifact: RustArtifactConfig) -> str:
837835
if self._is_generated_artifact(artifact):
@@ -1116,7 +1114,7 @@ def _find_exact_artifact(self, target_path: Path, expected_name: str, *, search_
11161114
deps_dir = target_path / "deps"
11171115
if deps_dir.is_dir() and (search_deps or not candidates):
11181116
for candidate in deps_dir.glob(f"*{expected_path.suffix}"):
1119-
if candidate.is_file() and _cargo_artifact_stem(candidate) == expected_path.stem:
1117+
if candidate.is_file() and candidate.suffix == expected_path.suffix and _cargo_artifact_stem(candidate) == expected_path.stem:
11201118
candidates.append(candidate)
11211119

11221120
if not candidates:
@@ -1138,6 +1136,7 @@ def _format_destination(
11381136
source: Path,
11391137
shared_library: str = "",
11401138
import_library: str = "",
1139+
executable: str = "",
11411140
) -> Path:
11421141
artifact = planned_artifact.artifact
11431142
extension_stem = artifact.python_extension_name or _cargo_artifact_stem(source)
@@ -1155,7 +1154,7 @@ def _format_destination(
11551154
"shared_library": shared_library,
11561155
"import_library": import_library,
11571156
"python_extension_name": python_extension,
1158-
"executable": source.name,
1157+
"executable": executable or source.name,
11591158
}
11601159
try:
11611160
rendered = template.format(**values)
@@ -1423,7 +1422,12 @@ def _copy_executable(self, planned_artifact: PlannedArtifact, *, build_root: Pat
14231422
executable = executable_name(name, platform=planned_artifact.resolved_target.platform)
14241423
source = self._find_exact_artifact(target_path, executable, search_deps=artifact.search_deps, artifact=artifact)
14251424
template = artifact.destination or self._default_executable_destination(artifact)
1426-
distribution_path = self._format_destination(template, planned_artifact=planned_artifact, source=source)
1425+
distribution_path = self._format_destination(
1426+
template,
1427+
planned_artifact=planned_artifact,
1428+
source=source,
1429+
executable=executable,
1430+
)
14271431
return self._place_artifact(
14281432
source,
14291433
distribution_path,

‎hatch_rs/tests/test_structs.py‎

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,19 +103,15 @@ def test_resolve_target_triple_rejects_macos_universal2_as_rust_target():
103103
),
104104
(
105105
{"abi3": True, "free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 15)},
106-
"cp315-abi3t-linux_x86_64",
106+
"cp315-abi3.abi3t-linux_x86_64",
107107
),
108108
(
109109
{"abi3": True, "free_threaded": True, "target": "aarch64-apple-darwin", "python_version": (3, 15)},
110-
"cp315-abi3t-macosx_11_0_arm64",
110+
"cp315-abi3.abi3t-macosx_11_0_arm64",
111111
),
112112
(
113113
{"abi3": True, "free_threaded": True, "target": "x86_64-pc-windows-msvc", "python_version": (3, 15)},
114-
"cp315-abi3t-win_amd64",
115-
),
116-
(
117-
{"abi3": True, "free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 13)},
118-
"cp313-cp313t-linux_x86_64",
114+
"cp315-abi3.abi3t-win_amd64",
119115
),
120116
(
121117
{"free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 15)},
@@ -132,7 +128,18 @@ def test_wheel_tag_uses_packaging_tags(monkeypatch: pytest.MonkeyPatch, kwargs:
132128

133129
def test_wheel_tag_uses_free_threaded_env(monkeypatch: pytest.MonkeyPatch):
134130
monkeypatch.setenv("HATCH_RUST_FREE_THREADED", "1")
135-
assert wheel_tag(abi3=True, target="aarch64-apple-darwin", python_version=(3, 15)) == "cp315-abi3t-macosx_11_0_arm64"
131+
assert wheel_tag(abi3=True, target="aarch64-apple-darwin", python_version=(3, 15)) == "cp315-abi3.abi3t-macosx_11_0_arm64"
132+
133+
134+
@pytest.mark.parametrize("python_version", [(3, 13), (3, 14)])
135+
def test_wheel_tag_rejects_abi3_free_threaded_before_python_315(python_version: tuple[int, int]):
136+
with pytest.raises(ValueError, match="ABI3 free-threaded builds require Python 3.15 or newer"):
137+
wheel_tag(
138+
abi3=True,
139+
free_threaded=True,
140+
target="x86_64-unknown-linux-gnu",
141+
python_version=python_version,
142+
)
136143

137144

138145
def test_wheel_tag_rejects_free_threaded_for_python_under_313():
@@ -209,6 +216,18 @@ def test_python_extension_name_rejects_free_threaded_for_python_under_313():
209216
python_extension_name("libproject", abi3=True, free_threaded=True, platform="linux", python_version=(3, 12))
210217

211218

219+
@pytest.mark.parametrize("python_version", [(3, 13), (3, 14)])
220+
def test_python_extension_name_rejects_abi3_free_threaded_before_python_315(python_version: tuple[int, int]):
221+
with pytest.raises(ValueError, match="ABI3 free-threaded builds require Python 3.15 or newer"):
222+
python_extension_name(
223+
"libproject",
224+
abi3=True,
225+
free_threaded=True,
226+
platform="linux",
227+
python_version=python_version,
228+
)
229+
230+
212231
def test_build_plan_copies_emscripten_python_extension(tmp_path):
213232
plan = HatchRustBuildPlan(
214233
module="project",
@@ -366,6 +385,34 @@ def test_build_plan_does_not_disable_crt_static_for_musl_executable(tmp_path):
366385
assert "target-feature=-crt-static" not in invocation.env.get("RUSTFLAGS", "")
367386

368387

388+
@pytest.mark.parametrize("cargo_target_kind", ["test", "bench"])
389+
def test_build_plan_handles_test_and_bench_as_executables(tmp_path, cargo_target_kind: str):
390+
plan = HatchRustBuildPlan(
391+
module="project",
392+
path=tmp_path,
393+
target="x86_64-unknown-linux-musl",
394+
artifacts=[
395+
RustArtifactConfig(
396+
name="check",
397+
cargo_target="check",
398+
cargo_target_kind=cargo_target_kind,
399+
)
400+
],
401+
)
402+
403+
assert plan.generate() == [f"cargo rustc --{cargo_target_kind} check --release --target x86_64-unknown-linux-musl"]
404+
planned_artifact = plan._artifact_plans[0]
405+
deps = tmp_path / "target" / "x86_64-unknown-linux-musl" / "release" / "deps"
406+
deps.mkdir(parents=True)
407+
source = deps / "check-0123456789abcdef"
408+
source.write_bytes(b"executable")
409+
source.with_suffix(".d").write_text("dependency metadata")
410+
411+
plan._copy_outputs(planned_artifact, build_root=tmp_path)
412+
413+
assert (tmp_path / "project" / "check").read_bytes() == b"executable"
414+
415+
369416
def test_build_plan_uses_debug_profile_for_editable_install(tmp_path):
370417
plan = HatchRustBuildPlan(
371418
module="project",

0 commit comments

Comments
 (0)