Skip to content

Commit 2e5342b

Browse files
authored
Merge pull request #14 from python-project-templates/tkp/init
Starting on implementation
2 parents 7222f8f + aaef04d commit 2e5342b

File tree

12 files changed

+551
-60
lines changed

12 files changed

+551
-60
lines changed

hatch_rs/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
__version__ = "0.1.0"
1+
__version__ = "0.1.7"
2+
3+
from .hooks import hatch_register_build_hook
4+
from .plugin import HatchRustBuildHook
5+
from .structs import *

hatch_rs/plugin.py

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,110 @@
11
from __future__ import annotations
22

3-
import typing as t
4-
from dataclasses import dataclass, field
3+
from logging import getLogger
4+
from os import getenv
5+
from typing import Any
56

6-
from hatchling.builders.config import BuilderConfig
77
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
88

9+
from .structs import HatchRustBuildConfig, HatchRustBuildPlan
10+
from .utils import import_string
911

10-
@dataclass
11-
class HatchRustBuildConfig(BuilderConfig):
12-
"""Build config values for Hatch Rust Builder."""
13-
14-
toolchain: str | None = None
15-
build_kwargs: t.Mapping[str, str] = field(default_factory=dict)
16-
editable_build_kwargs: t.Mapping[str, str] = field(default_factory=dict)
17-
ensured_targets: list[str] = field(default_factory=list)
18-
skip_if_exists: list[str] = field(default_factory=list)
19-
optional_editable_build: str = ""
12+
__all__ = ("HatchRustBuildHook",)
2013

2114

2215
class HatchRustBuildHook(BuildHookInterface[HatchRustBuildConfig]):
2316
"""The hatch-rust build hook."""
2417

25-
PLUGIN_NAME = "hatch-rust"
18+
PLUGIN_NAME = "hatch-rs"
19+
_logger = getLogger(__name__)
2620

27-
def initialize(self, version: str, build_data: dict[str, t.Any]) -> None:
21+
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
2822
"""Initialize the plugin."""
29-
return
23+
# Log some basic information
24+
project_name = self.metadata.config["project"]["name"]
25+
self._logger.info("Initializing hatch-rs plugin version %s", version)
26+
self._logger.info(f"Running hatch-rs: {project_name}")
27+
28+
# Only run if creating wheel
29+
# TODO: Add support for specify sdist-plan
30+
if self.target_name != "wheel":
31+
self._logger.info("ignoring target name %s", self.target_name)
32+
return
33+
34+
# Skip if SKIP_HATCH_RUST is set
35+
# TODO: Support CLI once https://github.com/pypa/hatch/pull/1743
36+
if getenv("SKIP_HATCH_RUST"):
37+
self._logger.info("Skipping the build hook since SKIP_HATCH_RUST was set")
38+
return
39+
40+
# Get build config class or use default
41+
build_config_class = import_string(self.config["build-config-class"]) if "build-config-class" in self.config else HatchRustBuildConfig
42+
43+
# Instantiate build config
44+
config = build_config_class(name=project_name, **self.config)
45+
46+
# Get build plan class or use default
47+
build_plan_class = import_string(self.config["build-plan-class"]) if "build-plan-class" in self.config else HatchRustBuildPlan
48+
49+
# Instantiate builder
50+
build_plan = build_plan_class(**config.model_dump())
51+
52+
# Generate commands
53+
build_plan.generate()
54+
55+
# Log commands if in verbose mode
56+
if config.verbose:
57+
for command in build_plan.commands:
58+
self._logger.warning(command)
59+
60+
# Execute build plan
61+
build_plan.execute()
62+
63+
# Perform any cleanup actions
64+
build_plan.cleanup()
65+
66+
# if build_plan.libraries:
67+
# # force include libraries
68+
# # for library in build_plan.libraries:
69+
# # name = library.get_qualified_name(build_plan.platform.platform)
70+
# # build_data["force_include"][name] = name
71+
72+
# build_data["pure_python"] = False
73+
# machine = platform_machine()
74+
# version_major = version_info.major
75+
# version_minor = version_info.minor
76+
# if "darwin" in sys_platform:
77+
# os_name = "macosx_11_0"
78+
# elif "linux" in sys_platform:
79+
# os_name = "linux"
80+
# else:
81+
# os_name = "win"
82+
# if all([lib.py_limited_api for lib in build_plan.libraries]):
83+
# build_data["tag"] = f"cp{version_major}{version_minor}-abi3-{os_name}_{machine}"
84+
# else:
85+
# build_data["tag"] = f"cp{version_major}{version_minor}-cp{version_major}{version_minor}-{os_name}_{machine}"
86+
# else:
87+
# build_data["pure_python"] = False
88+
# machine = platform_machine()
89+
# version_major = version_info.major
90+
# version_minor = version_info.minor
91+
# # TODO abi3
92+
# if "darwin" in sys_platform:
93+
# os_name = "macosx_11_0"
94+
# elif "linux" in sys_platform:
95+
# os_name = "linux"
96+
# else:
97+
# os_name = "win"
98+
# build_data["tag"] = f"cp{version_major}{version_minor}-cp{version_major}{version_minor}-{os_name}_{machine}"
99+
100+
# # force include libraries
101+
# for path in Path(".").rglob("*"):
102+
# if path.is_dir():
103+
# continue
104+
# if str(path).startswith(str(build_plan.cmake.build)) or str(path).startswith("dist"):
105+
# continue
106+
# if path.suffix in (".pyd", ".dll", ".so", ".dylib"):
107+
# build_data["force_include"][str(path)] = str(path)
108+
109+
# for path in build_data["force_include"]:
110+
# self._logger.warning(f"Force include: {path}")

hatch_rs/structs.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
from __future__ import annotations
2+
3+
from os import chdir, curdir, environ, system as system_call
4+
from pathlib import Path
5+
from platform import machine as platform_machine
6+
from shutil import which
7+
from sys import platform as sys_platform
8+
from typing import List, Literal, Optional
9+
10+
from pydantic import BaseModel, Field, field_validator
11+
12+
__all__ = (
13+
"HatchRustBuildConfig",
14+
"HatchRustBuildPlan",
15+
)
16+
17+
BuildType = Literal["debug", "release"]
18+
CompilerToolchain = Literal["gcc", "clang", "msvc"]
19+
Language = Literal["c", "c++"]
20+
Binding = Literal["cpython", "pybind11", "nanobind", "generic"]
21+
Platform = Literal["linux", "darwin", "win32"]
22+
23+
24+
class HatchRustBuildConfig(BaseModel):
25+
"""Build config values for Hatch Rust Builder."""
26+
27+
verbose: Optional[bool] = Field(default=False)
28+
name: Optional[str] = Field(default=None)
29+
30+
module: str = Field(description="Python module name for the Rust extension.")
31+
path: Optional[Path] = Field(default=None, description="Path to the project root directory.")
32+
33+
target: Optional[str] = Field(
34+
default=None,
35+
description="Target platform for the build. If not specified, it will be determined automatically.",
36+
)
37+
38+
# Validate path
39+
@field_validator("path", mode="before")
40+
@classmethod
41+
def validate_path(cls, path: Optional[Path]) -> Path:
42+
if path is None:
43+
return Path.cwd()
44+
if not isinstance(path, Path):
45+
path = Path(path)
46+
if not path.is_dir():
47+
raise ValueError(f"Path '{path}' is not a valid directory.")
48+
return path
49+
50+
51+
class HatchRustBuildPlan(HatchRustBuildConfig):
52+
build_type: BuildType = "release"
53+
commands: List[str] = Field(default_factory=list)
54+
55+
def generate(self):
56+
self.commands = []
57+
58+
# Construct build command
59+
platform = environ.get("HATCH_RUST_PLATFORM", sys_platform)
60+
machine = environ.get("HATCH_RUST_MACHINE", platform_machine())
61+
62+
build_command = "cargo rustc"
63+
64+
if self.build_type == "release":
65+
build_command += " --release"
66+
67+
if not self.target:
68+
if platform == "win32":
69+
if machine == "x86_64":
70+
self.target = "x86_64-pc-windows-msvc"
71+
elif machine == "i686":
72+
self.target = "i686-pc-windows-msvc"
73+
elif machine in ("arm64", "aarch64"):
74+
self.target = "aarch64-pc-windows-msvc"
75+
else:
76+
raise ValueError(f"Unsupported machine type: {machine} for Windows platform")
77+
elif platform == "darwin":
78+
if machine == "x86_64":
79+
self.target = "x86_64-apple-darwin"
80+
elif machine in ("arm64", "aarch64"):
81+
self.target = "aarch64-apple-darwin"
82+
else:
83+
raise ValueError(f"Unsupported machine type: {machine} for macOS platform")
84+
elif platform == "linux":
85+
if machine == "x86_64":
86+
self.target = "x86_64-unknown-linux-gnu"
87+
elif machine == "i686":
88+
self.target = "i686-unknown-linux-gnu"
89+
elif machine in ("arm64", "aarch64"):
90+
self.target = "aarch64-unknown-linux-gnu"
91+
else:
92+
raise ValueError(f"Unsupported machine type: {machine} for Linux platform")
93+
else:
94+
raise ValueError(f"Unsupported platform: {platform}")
95+
build_command += f" --target {self.target}"
96+
97+
if "apple" in build_command:
98+
build_command += " -- -C link-arg=-undefined -C link-arg=dynamic_lookup"
99+
100+
self.commands.append(build_command)
101+
102+
# Add copy commands after build
103+
return self.commands
104+
105+
def execute(self):
106+
"""Execute the build commands."""
107+
# First navigate to the project path
108+
109+
cwd = Path(curdir).resolve()
110+
chdir(self.path)
111+
112+
for command in self.commands:
113+
system_call(command)
114+
115+
# Go back to original path
116+
chdir(str(cwd))
117+
118+
# After executing commands, grab the build artifacts in the target directory
119+
# and copy them to the current directory.
120+
target_path = Path(self.path) / "target" / self.target / self.build_type
121+
if not target_path.exists():
122+
raise FileNotFoundError(f"Target path '{target_path}' does not exist.")
123+
if not target_path.is_dir():
124+
raise NotADirectoryError(f"Target path '{target_path}' is not a directory.")
125+
126+
platform = environ.get("HATCH_RUST_PLATFORM", sys_platform)
127+
if platform == "win32":
128+
files = list(target_path.glob("*.dll")) + list(target_path.glob("*.pyd"))
129+
elif platform == "linux":
130+
files = list(target_path.glob("*.so"))
131+
elif platform == "darwin":
132+
files = list(target_path.glob("*.dylib"))
133+
else:
134+
raise ValueError(f"Unsupported platform machine: {platform_machine()}")
135+
136+
if not files:
137+
raise FileNotFoundError(f"No build artifacts found in '{target_path}'.")
138+
139+
for file in files:
140+
if not file.is_file():
141+
continue
142+
143+
# Convert the filename to module format
144+
file_name = file.stem.replace("lib", "", 1) # Remove 'lib' prefix if present
145+
146+
# Copy each file to the current directory
147+
if sys_platform == "win32":
148+
copy_command = f"copy {file} {cwd}\\{self.module}\\{file_name}.pyd"
149+
else:
150+
if which("cp") is None:
151+
raise EnvironmentError("cp command not found. Ensure it is installed and available in PATH.")
152+
copy_command = f"cp -f {file} {cwd}/{self.module}/{file_name}.so"
153+
print(copy_command)
154+
system_call(copy_command)
155+
156+
return self.commands
157+
158+
def cleanup(self):
159+
...
160+
# if self.platform.platform == "win32":
161+
# for temp_obj in Path(".").glob("*.obj"):
162+
# temp_obj.unlink()

hatch_rs/tests/test_all.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)