Skip to content
Merged
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
30 changes: 17 additions & 13 deletions hatch_rs/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,16 +280,15 @@ def python_extension_name(
platform = _normalize_platform(platform or environ.get("HATCH_RUST_PLATFORM", sys_platform))
module_name = source_stem.removeprefix("lib")
major, minor = python_version or (version_info.major, version_info.minor)
is_freethreaded = _resolve_free_threaded(free_threaded, python_version=(major, minor)) if abi3 else False
if abi3 and is_freethreaded and (major, minor) < (3, 15):
raise ValueError(f"ABI3 free-threaded builds require Python 3.15 or newer, got {major}.{minor}.")
if platform == "win32":
return f"{module_name}.pyd"
if platform == "emscripten":
return f"{module_name}.cpython-{major}{minor}-wasm32-emscripten.so"
if abi3:
return (
f"{module_name}.abi3t.so"
if _resolve_free_threaded(free_threaded, python_version=(major, minor)) and (major, minor) >= (3, 15)
else f"{module_name}.abi3.so"
)
return f"{module_name}.abi3t.so" if is_freethreaded else f"{module_name}.abi3.so"
return f"{module_name}.so"


Expand Down Expand Up @@ -399,13 +398,12 @@ def wheel_tag(
resolved = resolved_target or _resolve_target(target, platform=platform, machine=machine)
version = python_version or (version_info.major, version_info.minor)
is_freethreaded = _resolve_free_threaded(free_threaded, python_version=version)
if abi3 and is_freethreaded and version < (3, 15):
raise ValueError(f"ABI3 free-threaded builds require Python 3.15 or newer, got {version[0]}.{version[1]}.")
if resolved.platform == "emscripten":
abis = [f"cp{version[0]}{version[1]}"]
elif abi3:
if is_freethreaded:
abis = ["abi3t"] if version >= (3, 15) else [f"cp{version[0]}{version[1]}t"]
else:
abis = ["abi3"]
abis = ["abi3.abi3t"] if is_freethreaded else ["abi3"]
elif is_freethreaded:
abis = [f"cp{version[0]}{version[1]}t"]
else:
Expand Down Expand Up @@ -831,7 +829,7 @@ def _is_python_extension_artifact(self, artifact: RustArtifactConfig) -> bool:
return artifact.destination is not None and "{python_extension_name}" in artifact.destination

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

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

if not candidates:
Expand All @@ -1138,6 +1136,7 @@ def _format_destination(
source: Path,
shared_library: str = "",
import_library: str = "",
executable: str = "",
) -> Path:
artifact = planned_artifact.artifact
extension_stem = artifact.python_extension_name or _cargo_artifact_stem(source)
Expand All @@ -1155,7 +1154,7 @@ def _format_destination(
"shared_library": shared_library,
"import_library": import_library,
"python_extension_name": python_extension,
"executable": source.name,
"executable": executable or source.name,
}
try:
rendered = template.format(**values)
Expand Down Expand Up @@ -1423,7 +1422,12 @@ def _copy_executable(self, planned_artifact: PlannedArtifact, *, build_root: Pat
executable = executable_name(name, platform=planned_artifact.resolved_target.platform)
source = self._find_exact_artifact(target_path, executable, search_deps=artifact.search_deps, artifact=artifact)
template = artifact.destination or self._default_executable_destination(artifact)
distribution_path = self._format_destination(template, planned_artifact=planned_artifact, source=source)
distribution_path = self._format_destination(
template,
planned_artifact=planned_artifact,
source=source,
executable=executable,
)
return self._place_artifact(
source,
distribution_path,
Expand Down
63 changes: 55 additions & 8 deletions hatch_rs/tests/test_structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,15 @@ def test_resolve_target_triple_rejects_macos_universal2_as_rust_target():
),
(
{"abi3": True, "free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 15)},
"cp315-abi3t-linux_x86_64",
"cp315-abi3.abi3t-linux_x86_64",
),
(
{"abi3": True, "free_threaded": True, "target": "aarch64-apple-darwin", "python_version": (3, 15)},
"cp315-abi3t-macosx_11_0_arm64",
"cp315-abi3.abi3t-macosx_11_0_arm64",
),
(
{"abi3": True, "free_threaded": True, "target": "x86_64-pc-windows-msvc", "python_version": (3, 15)},
"cp315-abi3t-win_amd64",
),
(
{"abi3": True, "free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 13)},
"cp313-cp313t-linux_x86_64",
"cp315-abi3.abi3t-win_amd64",
),
(
{"free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 15)},
Expand All @@ -132,7 +128,18 @@ def test_wheel_tag_uses_packaging_tags(monkeypatch: pytest.MonkeyPatch, kwargs:

def test_wheel_tag_uses_free_threaded_env(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("HATCH_RUST_FREE_THREADED", "1")
assert wheel_tag(abi3=True, target="aarch64-apple-darwin", python_version=(3, 15)) == "cp315-abi3t-macosx_11_0_arm64"
assert wheel_tag(abi3=True, target="aarch64-apple-darwin", python_version=(3, 15)) == "cp315-abi3.abi3t-macosx_11_0_arm64"


@pytest.mark.parametrize("python_version", [(3, 13), (3, 14)])
def test_wheel_tag_rejects_abi3_free_threaded_before_python_315(python_version: tuple[int, int]):
with pytest.raises(ValueError, match="ABI3 free-threaded builds require Python 3.15 or newer"):
wheel_tag(
abi3=True,
free_threaded=True,
target="x86_64-unknown-linux-gnu",
python_version=python_version,
)


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


@pytest.mark.parametrize("python_version", [(3, 13), (3, 14)])
def test_python_extension_name_rejects_abi3_free_threaded_before_python_315(python_version: tuple[int, int]):
with pytest.raises(ValueError, match="ABI3 free-threaded builds require Python 3.15 or newer"):
python_extension_name(
"libproject",
abi3=True,
free_threaded=True,
platform="linux",
python_version=python_version,
)


def test_build_plan_copies_emscripten_python_extension(tmp_path):
plan = HatchRustBuildPlan(
module="project",
Expand Down Expand Up @@ -366,6 +385,34 @@ def test_build_plan_does_not_disable_crt_static_for_musl_executable(tmp_path):
assert "target-feature=-crt-static" not in invocation.env.get("RUSTFLAGS", "")


@pytest.mark.parametrize("cargo_target_kind", ["test", "bench"])
def test_build_plan_handles_test_and_bench_as_executables(tmp_path, cargo_target_kind: str):
plan = HatchRustBuildPlan(
module="project",
path=tmp_path,
target="x86_64-unknown-linux-musl",
artifacts=[
RustArtifactConfig(
name="check",
cargo_target="check",
cargo_target_kind=cargo_target_kind,
)
],
)

assert plan.generate() == [f"cargo rustc --{cargo_target_kind} check --release --target x86_64-unknown-linux-musl"]
planned_artifact = plan._artifact_plans[0]
deps = tmp_path / "target" / "x86_64-unknown-linux-musl" / "release" / "deps"
deps.mkdir(parents=True)
source = deps / "check-0123456789abcdef"
source.write_bytes(b"executable")
source.with_suffix(".d").write_text("dependency metadata")

plan._copy_outputs(planned_artifact, build_root=tmp_path)

assert (tmp_path / "project" / "check").read_bytes() == b"executable"


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