Files
2026-07-13 13:22:34 +08:00

141 lines
3.9 KiB
Python

import argparse
import contextlib
import shutil
import subprocess
import sys
import zipfile
from collections.abc import Generator
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Package:
# name of the package on PyPI.
pypi_name: str
# type of the package, one of "dev", "skinny", "tracing", "release"
type: str
# path to the package relative to the root of the repository
build_path: str
DEV = Package("mlflow", "dev", ".")
RELEASE = Package("mlflow", "release", ".")
SKINNY = Package("mlflow-skinny", "skinny", "libs/skinny")
TRACING = Package("mlflow-tracing", "tracing", "libs/tracing")
PACKAGES = [
DEV,
SKINNY,
RELEASE,
TRACING,
]
JS_BUILD_DIR = Path("mlflow/server/js/build")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Build MLflow package.")
parser.add_argument(
"--package-type",
help="Package type to build. Default is 'dev'.",
choices=[p.type for p in PACKAGES],
default="dev",
)
parser.add_argument(
"--sha",
help="If specified, include the SHA in the wheel name as a build tag.",
)
return parser.parse_args()
@contextlib.contextmanager
def restore_changes() -> Generator[None, None, None]:
try:
yield
finally:
subprocess.check_call([
"git",
"restore",
"README.md",
"pyproject.toml",
])
def validate_ui_assets_pre_build(package: Package) -> None:
if package != RELEASE:
return
if not JS_BUILD_DIR.exists() or not any(JS_BUILD_DIR.iterdir()):
raise RuntimeError("Build the UI first before building the release package.")
def validate_ui_assets_post_build(wheel_path: Path, package: Package) -> None:
ui_asset_prefix = f"{JS_BUILD_DIR.as_posix()}/"
with zipfile.ZipFile(wheel_path) as zf:
has_ui_assets = any(name.startswith(ui_asset_prefix) for name in zf.namelist())
if package == RELEASE:
if not has_ui_assets:
raise RuntimeError(
f"UI assets are missing from the release wheel: {wheel_path}. "
"Build the UI first before building the release package."
)
elif package in (SKINNY, TRACING):
if has_ui_assets:
raise RuntimeError(
f"UI assets should not be included in the {package.type} wheel: {wheel_path}."
)
def main() -> None:
args = parse_args()
# Initialize submodules (e.g., mlflow/assistant/skills)
subprocess.check_call(["git", "submodule", "update", "--init", "--recursive"])
# Clean up build artifacts generated by previous builds
paths_to_clean_up = ["build"]
for pkg in PACKAGES:
paths_to_clean_up += [
f"{pkg.build_path}/dist",
f"{pkg.build_path}/{pkg.pypi_name}.egg_info",
]
for path in map(Path, paths_to_clean_up):
if not path.exists():
continue
if path.is_file():
path.unlink()
else:
shutil.rmtree(path)
package = next(p for p in PACKAGES if p.type == args.package_type)
validate_ui_assets_pre_build(package)
with restore_changes():
pyproject = Path("pyproject.toml")
if package == RELEASE:
pyproject.write_text(Path("pyproject.release.toml").read_text())
DIST_DIR = Path("dist").resolve()
DIST_DIR.mkdir(exist_ok=True)
subprocess.check_call([
sys.executable,
"-m",
"build",
package.build_path,
"--outdir",
DIST_DIR,
])
wheel = next(DIST_DIR.glob("mlflow*.whl"))
validate_ui_assets_post_build(wheel, package)
if args.sha:
name, version, rest = wheel.name.split("-", 2)
build_tag = f"0.sha.{args.sha}" # build tag must start with a digit
wheel.rename(wheel.with_name(f"{name}-{version}-{build_tag}-{rest}"))
if __name__ == "__main__":
main()