249 lines
7.2 KiB
Python
Executable File
249 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# /// script
|
|
# requires-python = ">=3.9"
|
|
# dependencies = []
|
|
# ///
|
|
"""
|
|
Build Ray manylinux wheels locally using raymake.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from ci.build.build_common import (
|
|
BuildError,
|
|
detect_host_arch,
|
|
find_ray_root,
|
|
get_git_commit,
|
|
log,
|
|
parse_file,
|
|
)
|
|
|
|
# Configuration Constants
|
|
SUPPORTED_PYTHON_VERSIONS = ("3.10", "3.11", "3.12", "3.13")
|
|
RAYMAKE_SPEC = "ci/docker/ray-wheel.wanda.yaml"
|
|
RAYCI_REGISTRY = "cr.ray.io/rayproject"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BuildConfig:
|
|
python_version: str
|
|
output_dir: Path
|
|
ray_root: Path
|
|
hosttype: str
|
|
arch_suffix: str
|
|
raymake_version: str
|
|
manylinux_version: str
|
|
commit: str
|
|
no_java: bool = False
|
|
no_dashboard: bool = False
|
|
|
|
@property
|
|
def build_env(self) -> dict[str, str]:
|
|
java_image = (
|
|
"scratch"
|
|
if self.no_java
|
|
else f"{RAYCI_REGISTRY}/ray-java-build{self.arch_suffix}"
|
|
)
|
|
dashboard_image = (
|
|
"scratch"
|
|
if self.no_dashboard
|
|
else f"{RAYCI_REGISTRY}/ray-dashboard{self.arch_suffix}"
|
|
)
|
|
|
|
return {
|
|
"PYTHON_VERSION": self.python_version,
|
|
"MANYLINUX_VERSION": self.manylinux_version,
|
|
"HOSTTYPE": self.hosttype,
|
|
"ARCH_SUFFIX": self.arch_suffix,
|
|
"JDK_SUFFIX": "" if self.no_java else "-jdk",
|
|
"BUILDKITE_COMMIT": self.commit,
|
|
"IS_LOCAL_BUILD": "true",
|
|
"RAY_JAVA_IMAGE": java_image,
|
|
"RAY_DASHBOARD_IMAGE": dashboard_image,
|
|
}
|
|
|
|
@classmethod
|
|
def from_env(
|
|
cls,
|
|
python_version: str,
|
|
output_dir: str,
|
|
*,
|
|
no_java: bool = False,
|
|
no_dashboard: bool = False,
|
|
) -> BuildConfig:
|
|
root = find_ray_root()
|
|
host, suffix = cls._detect_platform()
|
|
|
|
rayciversion_path = root / ".rayciversion"
|
|
if not rayciversion_path.exists():
|
|
raise BuildError(f"Missing {rayciversion_path}")
|
|
raymake_version = rayciversion_path.read_text().strip()
|
|
manylinux_version = parse_file(
|
|
root / "rayci.env", r'MANYLINUX_VERSION=["\']?([^"\'\s]+)'
|
|
)
|
|
commit = get_git_commit(root)
|
|
|
|
return cls(
|
|
python_version=python_version,
|
|
output_dir=Path(output_dir).resolve(),
|
|
ray_root=root,
|
|
hosttype=host,
|
|
arch_suffix=suffix,
|
|
raymake_version=raymake_version,
|
|
manylinux_version=manylinux_version,
|
|
commit=commit,
|
|
no_java=no_java,
|
|
no_dashboard=no_dashboard,
|
|
)
|
|
|
|
@staticmethod
|
|
def _detect_platform() -> tuple[str, str]:
|
|
"""Return (hosttype, arch_suffix) for current platform."""
|
|
arch = detect_host_arch()
|
|
suffix = "" if arch == "x86_64" else f"-{arch}"
|
|
return arch, suffix
|
|
|
|
|
|
class WheelBuilder:
|
|
def __init__(self, config: BuildConfig):
|
|
self.config = config
|
|
|
|
def build(self) -> list[Path]:
|
|
if not shutil.which("raymake"):
|
|
raise BuildError("raymake not found. Run via ./build-wheel.sh")
|
|
|
|
excluded = []
|
|
if self.config.no_java:
|
|
excluded.append("java")
|
|
if self.config.no_dashboard:
|
|
excluded.append("dashboard")
|
|
|
|
log.info("Build configuration:")
|
|
summary = {
|
|
"Python": self.config.python_version,
|
|
"Arch": self.config.hosttype,
|
|
"Commit": self.config.commit,
|
|
"Raymake": self.config.raymake_version,
|
|
"Manylinux": self.config.manylinux_version,
|
|
"Excluded": ", ".join(excluded) if excluded else "none",
|
|
"Ray Root": self.config.ray_root,
|
|
"Output Dir": self.config.output_dir,
|
|
}
|
|
print("-" * 50)
|
|
for k, v in summary.items():
|
|
print(f"{k:<12}: {v}")
|
|
print("-" * 50)
|
|
|
|
cmd = [
|
|
"raymake",
|
|
"--artifacts_dir",
|
|
str(self.config.output_dir),
|
|
str(self.config.ray_root / RAYMAKE_SPEC),
|
|
]
|
|
|
|
log.info(f"Running raymake: {RAYMAKE_SPEC}")
|
|
log.info(f"Build environment: {self.config.build_env}")
|
|
proc = subprocess.Popen(cmd, env={**os.environ, **self.config.build_env})
|
|
try:
|
|
if proc.wait() != 0:
|
|
raise BuildError("raymake failed")
|
|
except KeyboardInterrupt:
|
|
# We don't need to do much here; the child received the SIGINT too.
|
|
# Just wait a moment for it to exit to avoid the "No such process" error.
|
|
try:
|
|
proc.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
raise
|
|
|
|
return list(self.config.output_dir.glob("*.whl"))
|
|
|
|
|
|
def main():
|
|
# Override the program name to match the root script name
|
|
parser = argparse.ArgumentParser(
|
|
prog="./build-wheel.sh",
|
|
description="Build Ray wheel using a manylinux base image.",
|
|
epilog="Example: ./build-wheel.sh 3.13",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"python_version",
|
|
choices=SUPPORTED_PYTHON_VERSIONS,
|
|
metavar="PYTHON_VERSION",
|
|
help="Target Python version: %(choices)s",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"output_dir",
|
|
nargs="?",
|
|
default=".whl",
|
|
help="Directory to store wheels (default: .whl)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--no-java",
|
|
action="store_true",
|
|
default=False,
|
|
help="Build wheel without Java JARs",
|
|
)
|
|
parser.add_argument(
|
|
"--no-dashboard",
|
|
action="store_true",
|
|
default=False,
|
|
help="Build wheel without the dashboard",
|
|
)
|
|
|
|
if len(sys.argv) == 1:
|
|
parser.print_help()
|
|
sys.exit(0)
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
builder = WheelBuilder(
|
|
BuildConfig.from_env(
|
|
args.python_version,
|
|
args.output_dir,
|
|
no_java=args.no_java,
|
|
no_dashboard=args.no_dashboard,
|
|
)
|
|
)
|
|
output_dir = builder.config.output_dir
|
|
if output_dir.exists():
|
|
if not output_dir.is_dir():
|
|
raise BuildError(
|
|
f"Output path '{output_dir}' exists and is not a directory."
|
|
)
|
|
existing_wheels = list(output_dir.glob("*.whl"))
|
|
if existing_wheels:
|
|
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
backup_dir = output_dir / f"old_{ts}"
|
|
backup_dir.mkdir()
|
|
for whl in existing_wheels:
|
|
whl.rename(backup_dir / whl.name)
|
|
log.info(f"Moved existing wheels to {backup_dir}")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
wheels = builder.build()
|
|
log.info(f"Success! Wheels saved to {output_dir}")
|
|
log.info("To install, run:")
|
|
for w in wheels:
|
|
print(f" pip install {w}")
|
|
except BuildError as e:
|
|
log.error(e)
|
|
sys.exit(1)
|
|
except KeyboardInterrupt:
|
|
sys.exit(130)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|