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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ destination = "project/lib/{shared_library}"
Destination templates support `{module}`, `{target}`, `{profile}`, `{name}`,
`{shared_library}`, `{import_library}`, and `{python_extension_name}`.

### Using the Rust debug profile for editable installs

By default rust release profile is used to compile the artifacts and it can
be changed using the `profile` option in `tool.hatch.build.hooks.hatch-rs`
or `tool.hatch.build.hooks.hatch-rs.artifacts` sections.

When installing the Python project in development mode (aka editable installs)
using the command `pip install --editable <package>`, it can be preferred
to use the rust debug profile so the compilation time is faster.

Using the Rust debug profile for editable installs can be enforced by setting the
`editable-debug` option to `true` either in `tool.hatch.build.hooks.hatch-rs`
or `tool.hatch.build.hooks.hatch-rs.artifacts` sections.

### Generated files and headers

Artifacts with `command` run an argv-list command and then validate explicit
Expand Down
2 changes: 2 additions & 0 deletions hatch_rs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
build_plan = build_plan_class(**config.model_dump())

# Generate commands
if isinstance(build_plan, HatchRustBuildPlan):
build_plan.set_editable_install(version == "editable")
build_plan.generate()

# Log commands if in verbose mode
Expand Down
17 changes: 16 additions & 1 deletion hatch_rs/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,11 @@ class RustArtifactConfig(BaseModel):
manifest: Path | None = Field(default=None, description="Path to Cargo.toml, relative to the hook path unless absolute.")
build_type: BuildType | None = Field(default=None, alias="build-type")
profile: str | None = Field(default=None, description="Cargo profile for this artifact.")
editable_debug: bool = Field(
default=False,
alias="editable-debug",
description="Use the Rust debug profile for editable installs.",
)
target: str | None = Field(default=None, description="Rust target triple for this artifact.")
package: str | None = Field(default=None, description="Cargo package selector.")
cargo_target_kind: CargoTargetKind | None = Field(default=None, alias="cargo-target-kind", description="Cargo target selector kind.")
Expand Down Expand Up @@ -576,6 +581,11 @@ class HatchRustBuildConfig(BaseModel):
manifest: Path | None = Field(default=None, description="Path to Cargo.toml, relative to path unless absolute.")
build_type: BuildType = Field(default="release", alias="build-type")
profile: str | None = Field(default=None, description="Cargo profile to build. Overrides build_type when set.")
editable_debug: bool = Field(
default=False,
alias="editable-debug",
description="Use the Rust debug profile for editable installs.",
)
features: list[str] = Field(default_factory=list, description="Cargo features to enable.")
all_features: bool = Field(default=False, alias="all-features", description="Enable all Cargo features.")
no_default_features: bool = Field(default=False, alias="no-default-features", description="Disable Cargo default features.")
Expand Down Expand Up @@ -657,6 +667,7 @@ class HatchRustBuildPlan(HatchRustBuildConfig):
_target_dir: Path | None = PrivateAttr(default=None)
_set_target_dir_env: bool = PrivateAttr(default=False)
_temporary_target_dir: TemporaryDirectory[str] | None = PrivateAttr(default=None)
_editable_install: bool = PrivateAttr(default=False)

@property
def libraries(self) -> list[str]:
Expand Down Expand Up @@ -722,6 +733,8 @@ def _artifact_manifest(self, artifact: RustArtifactConfig) -> Path | None:
return artifact.manifest if artifact.manifest is not None else self.manifest

def _artifact_profile(self, artifact: RustArtifactConfig) -> str:
if self._editable_install and (artifact.editable_debug or self.editable_debug):
return "debug"
build_type = artifact.build_type or self.build_type
return artifact.profile or self.profile or build_type

Expand Down Expand Up @@ -897,6 +910,9 @@ def _build_command_artifact_plan(self, artifact: RustArtifactConfig, *, global_t
set_target_dir_env=set_target_dir_env,
)

def set_editable_install(self, editable_install: bool) -> None:
self._editable_install = editable_install

def generate(self):
if self._temporary_target_dir is not None:
self._temporary_target_dir.cleanup()
Expand All @@ -910,7 +926,6 @@ def generate(self):
self._shared_scripts = {}
self._artifact_manifest_records = []
self._libraries = []

global_target = self.target
for artifact in self._configured_artifacts():
if self._is_generated_artifact(artifact):
Expand Down
40 changes: 40 additions & 0 deletions hatch_rs/tests/test_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from hatch_rs.plugin import HatchRustBuildHook


def test_custom_build_plan_generate_keeps_no_argument_contract(monkeypatch, tmp_path):
generated = []

class CustomBuildPlan:
def __init__(self, **_config):
self.commands = []
self.copied_artifacts = []
self.shared_data = {"source": "destination"}
self.shared_scripts = {}
self.libraries = []

def generate(self):
generated.append(True)

def execute(self):
pass

def cleanup(self):
pass

class Metadata:
def __init__(self):
self.config = {"project": {"name": "project"}}

monkeypatch.setattr("hatch_rs.plugin.import_string", lambda path: CustomBuildPlan)
hook = HatchRustBuildHook(
root=str(tmp_path),
config={"module": "project", "path": str(tmp_path), "build-plan-class": "tests.CustomBuildPlan"},
build_config=None,
metadata=Metadata(),
directory=str(tmp_path),
target_name="wheel",
)

hook.initialize("standard", {})

assert generated == [True]
180 changes: 180 additions & 0 deletions hatch_rs/tests/test_project_editable_debug/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions hatch_rs/tests/test_project_editable_debug/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "project_py"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
name = "extension"
path = "src/lib.rs"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.25", features = ["abi3-py39", "extension-module", "multiple-pymethods"] }

[profile.release]
panic = 'abort'
lto = true
Empty file.
37 changes: 37 additions & 0 deletions hatch_rs/tests/test_project_editable_debug/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[build-system]
requires = ["hatchling>=1.20"]
build-backend = "hatchling.build"

[project]
name = "hatch-rs-test-project-editable-debug"
description = "Basic test project for hatch-rs"
version = "0.1.0"
requires-python = ">=3.9"

[tool.hatch.build]
artifacts = [
"project/*.dll",
"project/*.dylib",
"project/*.so",
]

[tool.hatch.build.sources]
src = "/"

[tool.hatch.build.targets.sdist]
packages = ["project"]

[tool.hatch.build.targets.wheel]
packages = ["project"]

[tool.hatch.build.hooks.hatch-rs]
verbose = true
abi3 = true
module = "project"
target-dir = "target"

[[tool.hatch.build.hooks.hatch-rs.artifacts]]
name = "extension"
manifest = "Cargo.toml"
destination = "project/{python_extension_name}"
editable-debug = true
13 changes: 13 additions & 0 deletions hatch_rs/tests/test_project_editable_debug/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use pyo3::prelude::*;

#[pyfunction]
pub fn hello() -> &'static str {
"A string"
}

#[pymodule]
fn extension(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {
// Example
m.add_function(pyo3::wrap_pyfunction!(hello, m)?)?;
Ok(())
}
Loading