1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
|
"""Custom Hatchling build hook to compile packmol binary."""
import os
import shutil
import subprocess
from pathlib import Path
from typing import Any
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface):
"""Build hook to compile packmol from Fortran source."""
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
"""Compile packmol and copy binary to package.
Args:
version: The version of the package being built.
build_data: Dictionary of build configuration data.
"""
# Find gfortran compiler
gfortran = self._find_gfortran()
# Verify source files exist
src_dir = Path("src")
if not src_dir.exists():
raise RuntimeError(
f"Source directory not found: {src_dir}. "
"Ensure packmol Fortran source files are in the src/ directory."
)
# Create binaries output directory
binaries_dir = Path("python/packmol/binaries")
binaries_dir.mkdir(parents=True, exist_ok=True)
# Clean previous builds
print("Cleaning previous build artifacts...")
subprocess.run(["make", "clean"], check=False, cwd=".")
# Compile packmol
print(f"Compiling packmol with {gfortran}...")
result = subprocess.run(
["make", f"FORTRAN={gfortran}"],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(
f"Failed to compile packmol:\n{result.stderr}\n{result.stdout}"
)
# Find compiled binary
binary_name = "packmol.exe" if os.name == "nt" else "packmol"
binary_path = Path(binary_name)
if not binary_path.exists():
raise RuntimeError(
f"Compiled binary not found: {binary_path}. "
"Compilation may have failed."
)
# Copy binary to package
target_path = binaries_dir / binary_name
print(f"Copying {binary_path} to {target_path}...")
shutil.copy2(binary_path, target_path)
# Ensure binary is executable
target_path.chmod(0o755)
print(f"Successfully built packmol binary: {target_path}")
# Mark wheel as platform-specific
build_data["pure_python"] = False
build_data["infer_tag"] = True
def _find_gfortran(self) -> str:
"""Find gfortran compiler.
Returns:
Path to gfortran executable.
Raises:
RuntimeError: If gfortran is not found.
"""
# Try plain gfortran first
if shutil.which("gfortran"):
return "gfortran"
# Try versioned gfortran (e.g., gfortran-13, gfortran-14)
for version in range(20, 10, -1):
gfortran_versioned = f"gfortran-{version}"
if shutil.which(gfortran_versioned):
return gfortran_versioned
# Check common Homebrew paths on macOS
homebrew_paths = [
"/opt/homebrew/bin/gfortran",
"/usr/local/bin/gfortran",
]
for path in homebrew_paths:
if Path(path).exists():
return path
# Not found
raise RuntimeError("gfortran compiler not found.")
|