import subprocess
import sys
import os
import shutil
from pathlib import Path
from typing import List, Optional
if sys.platform == "win32":
import codecs
sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach())
PLATFORMS = {
"linux-x86_64": {
"target": "x86_64-unknown-linux-gnu",
"platform_tag": "manylinux_2_17_x86_64",
},
"linux-aarch64": {
"target": "aarch64-unknown-linux-gnu",
"platform_tag": "manylinux_2_17_aarch64",
},
"linux-x86_64-musl": {
"target": "x86_64-unknown-linux-musl",
"platform_tag": "musllinux_1_1_x86_64",
},
"linux-aarch64-musl": {
"target": "aarch64-unknown-linux-musl",
"platform_tag": "musllinux_1_1_aarch64",
},
"windows-x64": {"target": "x86_64-pc-windows-msvc", "platform_tag": "win_amd64"},
"windows-x86": {"target": "i686-pc-windows-msvc", "platform_tag": "win32"},
"macos-x86_64": {
"target": "x86_64-apple-darwin",
"platform_tag": "macosx_10_12_x86_64",
},
"macos-aarch64": {
"target": "aarch64-apple-darwin",
"platform_tag": "macosx_11_0_arm64",
},
}
def run_command(
cmd: List[str], cwd: Optional[Path] = None
) -> subprocess.CompletedProcess:
print(f"Running: {' '.join(cmd)}")
if cwd:
print(f"Working directory: {cwd}")
encoding = "utf-8" if sys.platform == "win32" else None
env = os.environ.copy()
if sys.platform == "win32":
env["PYTHONIOENCODING"] = "utf-8"
env["PYTHONUTF8"] = "1"
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
encoding=encoding,
errors="replace", env=env,
)
if result.stdout:
print("STDOUT:", result.stdout)
if result.stderr:
print("STDERR:", result.stderr)
return result
def check_prerequisites():
print("Checking prerequisites...")
result = run_command(["maturin", "--version"])
if result.returncode != 0:
print("ERROR: maturin not found. Install with: pip install maturin")
sys.exit(1)
result = run_command(["cargo", "--version"])
if result.returncode != 0:
print("ERROR: cargo not found. Install Rust toolchain.")
sys.exit(1)
print("* Prerequisites check passed")
def install_rust_targets(platforms: List[str]):
print("Installing Rust targets...")
targets_to_install = set()
for platform in platforms:
if platform in PLATFORMS:
targets_to_install.add(PLATFORMS[platform]["target"])
for target in targets_to_install:
print(f"Installing target: {target}")
result = run_command(["rustup", "target", "add", target])
if result.returncode != 0:
print(f"Warning: Failed to install target {target}")
print("* Rust targets installation completed")
def build_wheel(platform: str, output_dir: Path, use_zig: bool = False) -> bool:
if platform not in PLATFORMS:
print(f"ERROR: Unknown platform '{platform}'")
return False
config = PLATFORMS[platform]
target = config["target"]
print(f"\n=== Building wheel for {platform} ({target}) ===")
cmd = [
"maturin",
"build",
"--release",
"--target",
target,
"--out",
str(output_dir),
]
if platform.startswith("windows"):
cmd.append("--find-interpreter")
else:
cmd.extend(["--interpreter", "python3.8"])
if use_zig:
cmd.append("--zig")
if platform.startswith("linux"):
if platform.endswith("-musl"):
cmd.extend(["--compatibility", "musllinux_1_1"])
else:
cmd.extend(["--compatibility", "manylinux_2_17"])
elif platform == "windows-x86":
cmd.extend(["--no-default-features", "--features", "std,python"])
project_root = Path(__file__).parent.parent
result = run_command(cmd, cwd=project_root)
if result.returncode == 0:
print(f"* Successfully built wheel for {platform}")
return True
else:
print(f"* Failed to build wheel for {platform}")
return False
def main():
import argparse
parser = argparse.ArgumentParser(description="Build wheels for multiple platforms")
parser.add_argument(
"--platforms",
nargs="+",
default=["linux-x86_64"],
choices=list(PLATFORMS.keys()),
help="Platforms to build for",
)
parser.add_argument(
"--output-dir",
type=Path,
default=Path("../target/wheels"),
help="Output directory for wheels",
)
parser.add_argument(
"--clean", action="store_true", help="Clean output directory before building"
)
parser.add_argument(
"--install-targets", action="store_true", help="Install required Rust targets"
)
parser.add_argument(
"--zig",
action="store_true",
help="Use Zig for cross-compilation (requires zig to be installed)",
)
args = parser.parse_args()
check_prerequisites()
output_dir = args.output_dir.resolve()
if args.clean and output_dir.exists():
print(f"Cleaning output directory: {output_dir}")
shutil.rmtree(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
print(f"Output directory: {output_dir}")
if args.install_targets:
install_rust_targets(args.platforms)
successful_builds = []
failed_builds = []
for platform in args.platforms:
if build_wheel(platform, output_dir, args.zig):
successful_builds.append(platform)
else:
failed_builds.append(platform)
print(f"\n=== Build Summary ===")
print(
f"Successful builds ({len(successful_builds)}): {', '.join(successful_builds)}"
)
if failed_builds:
print(f"Failed builds ({len(failed_builds)}): {', '.join(failed_builds)}")
print(f"\nWheels output directory: {output_dir}")
wheels = list(output_dir.glob("*.whl"))
if wheels:
print(f"\nGenerated wheels:")
for wheel in wheels:
print(f" - {wheel.name}")
return len(failed_builds) == 0
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)