mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-15 05:09:44 +08:00
The Maturin-based wheel packaging was a historical remnant from when the local gateway launch path and OpenShell CLI were coupled in one binary. The gateway and CLI now ship as standalone artifacts, so the Python distribution should contain only the SDK. Build a single platform-independent setuptools wheel, verify that it cannot contain native code or an openshell entry point, and simplify the release jobs and documentation for SDK-only PyPI installs. Signed-off-by: Simon Scatton <sscatton@nvidia.com>
152 lines
5.3 KiB
Python
152 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from configparser import ConfigParser
|
|
from configparser import Error as ConfigParserError
|
|
from email.parser import Parser
|
|
from pathlib import Path
|
|
from zipfile import BadZipFile, ZipFile
|
|
|
|
PROTO_STEMS = ("datamodel", "inference", "openshell", "options", "sandbox")
|
|
BYTECODE_SUFFIXES = (".pyc", ".pyo")
|
|
NATIVE_SUFFIXES = (".dll", ".dylib", ".exe", ".pyd", ".so")
|
|
|
|
|
|
def _find_wheel(path: Path) -> Path:
|
|
if path.is_file():
|
|
return path
|
|
|
|
wheels = sorted(path.glob("*.whl"))
|
|
if len(wheels) != 1:
|
|
raise ValueError(f"expected exactly one wheel in {path}, found {len(wheels)}")
|
|
return wheels[0]
|
|
|
|
|
|
def _required_files() -> set[str]:
|
|
files = {
|
|
"openshell/__init__.py",
|
|
"openshell/sandbox.py",
|
|
"openshell/py.typed",
|
|
"openshell/_proto/__init__.py",
|
|
}
|
|
for stem in PROTO_STEMS:
|
|
files.add(f"openshell/_proto/{stem}_pb2.py")
|
|
files.add(f"openshell/_proto/{stem}_pb2.pyi")
|
|
files.add(f"openshell/_proto/{stem}_pb2_grpc.py")
|
|
return files
|
|
|
|
|
|
def _single_member(names: set[str], suffix: str) -> str:
|
|
matches = [name for name in names if name.endswith(suffix)]
|
|
if len(matches) != 1:
|
|
raise ValueError(f"expected exactly one {suffix} file, found {len(matches)}")
|
|
return matches[0]
|
|
|
|
|
|
def verify_wheel(path: Path, expected_version: str | None = None) -> Path:
|
|
wheel = _find_wheel(path)
|
|
if not wheel.name.endswith("-py3-none-any.whl"):
|
|
raise ValueError(f"wheel is not platform-independent: {wheel.name}")
|
|
|
|
with ZipFile(wheel) as archive:
|
|
names = set(archive.namelist())
|
|
missing = sorted(_required_files() - names)
|
|
if missing:
|
|
raise ValueError(f"wheel is missing required files: {', '.join(missing)}")
|
|
|
|
scripts = sorted(
|
|
name
|
|
for name in names
|
|
if not name.endswith("/")
|
|
and Path(name).name in {"openshell", "openshell.exe"}
|
|
)
|
|
if scripts:
|
|
raise ValueError(
|
|
f"wheel contains an openshell executable: {', '.join(scripts)}"
|
|
)
|
|
|
|
entry_points = [
|
|
name for name in names if name.endswith(".dist-info/entry_points.txt")
|
|
]
|
|
if len(entry_points) > 1:
|
|
raise ValueError(
|
|
f"expected at most one entry_points.txt file, found {len(entry_points)}"
|
|
)
|
|
if entry_points:
|
|
parser = ConfigParser(interpolation=None)
|
|
try:
|
|
parser.read_string(archive.read(entry_points[0]).decode())
|
|
except (ConfigParserError, UnicodeDecodeError) as error:
|
|
raise ValueError(
|
|
"wheel contains invalid entry point metadata"
|
|
) from error
|
|
if parser.has_option("console_scripts", "openshell"):
|
|
raise ValueError("wheel defines an openshell console script")
|
|
|
|
bytecode_files = sorted(
|
|
name for name in names if name.endswith(BYTECODE_SUFFIXES)
|
|
)
|
|
if bytecode_files:
|
|
raise ValueError(
|
|
f"wheel contains Python bytecode: {', '.join(bytecode_files)}"
|
|
)
|
|
|
|
native_files = sorted(name for name in names if name.endswith(NATIVE_SUFFIXES))
|
|
if native_files:
|
|
raise ValueError(f"wheel contains native files: {', '.join(native_files)}")
|
|
|
|
metadata_name = _single_member(names, ".dist-info/METADATA")
|
|
metadata = Parser().parsestr(archive.read(metadata_name).decode())
|
|
if metadata["Name"] != "openshell":
|
|
raise ValueError(f"unexpected distribution name: {metadata['Name']}")
|
|
|
|
version = metadata["Version"]
|
|
filename_version = wheel.name.removesuffix("-py3-none-any.whl").removeprefix(
|
|
"openshell-"
|
|
)
|
|
if version != filename_version:
|
|
raise ValueError(
|
|
f"wheel filename version {filename_version} does not match metadata {version}"
|
|
)
|
|
if expected_version is not None and version != expected_version:
|
|
raise ValueError(f"expected version {expected_version}, found {version}")
|
|
|
|
wheel_name = _single_member(names, ".dist-info/WHEEL")
|
|
wheel_metadata = Parser().parsestr(archive.read(wheel_name).decode())
|
|
if wheel_metadata.get_all("Tag", []) != ["py3-none-any"]:
|
|
raise ValueError(
|
|
f"unexpected wheel tags: {wheel_metadata.get_all('Tag', [])}"
|
|
)
|
|
if wheel_metadata["Root-Is-Purelib"] != "true":
|
|
raise ValueError("wheel is not marked as pure Python")
|
|
|
|
return wheel
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Verify the OpenShell Python wheel")
|
|
parser.add_argument(
|
|
"path", type=Path, help="Wheel file or directory containing one wheel"
|
|
)
|
|
parser.add_argument("--expected-version")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
wheel = verify_wheel(args.path, args.expected_version)
|
|
except (BadZipFile, OSError, ValueError) as error:
|
|
print(f"ERROR: {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Verified {wheel}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|