161 lines
6.0 KiB
Python
161 lines
6.0 KiB
Python
"""Read project metadata from pyproject.toml for packaging scripts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import importlib.util
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _load_wheel_layout_module():
|
|
layout_path = _BUILD_ROOT / "wheel" / "layout.py"
|
|
spec = importlib.util.spec_from_file_location("_orisys_build_wheel_layout", layout_path)
|
|
if spec is None or spec.loader is None:
|
|
raise ImportError(f"Cannot load wheel layout helper from {layout_path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _load_toml(text: str) -> dict:
|
|
if sys.version_info >= (3, 11):
|
|
import tomllib
|
|
|
|
return tomllib.loads(text)
|
|
try:
|
|
import tomli
|
|
except ImportError:
|
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "tomli", "-q"])
|
|
import tomli
|
|
return tomli.loads(text)
|
|
|
|
|
|
def load_pyproject(repo_root: Path) -> dict:
|
|
path = repo_root / "pyproject.toml"
|
|
return _load_toml(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def project_metadata(repo_root: Path) -> dict:
|
|
return load_pyproject(repo_root)["project"]
|
|
|
|
|
|
def install_requires(repo_root: Path) -> list[str]:
|
|
return list(project_metadata(repo_root)["dependencies"])
|
|
|
|
|
|
def extras_require(repo_root: Path) -> dict[str, list[str]]:
|
|
optional = project_metadata(repo_root).get("optional-dependencies", {})
|
|
return {name: list(requirements) for name, requirements in optional.items()}
|
|
|
|
|
|
def _resolve_version_attr(repo_root: Path, attr: str) -> str:
|
|
"""Resolve a setuptools dynamic version attr (e.g. orisys.__version__)."""
|
|
module_name, _, attr_name = attr.partition(".")
|
|
if not attr_name:
|
|
raise RuntimeError(f"Invalid dynamic version attr: {attr!r}")
|
|
|
|
# Avoid importing package modules here: __init__ may import heavy runtime deps
|
|
# (e.g., cv2/numpy) that are not required just to read __version__.
|
|
rel_module = Path(*module_name.split("."))
|
|
module_file = repo_root / "src" / rel_module.with_suffix(".py")
|
|
if not module_file.is_file():
|
|
module_file = repo_root / "src" / rel_module / "__init__.py"
|
|
if not module_file.is_file():
|
|
raise RuntimeError(f"Cannot resolve module file for dynamic version attr: {attr!r}")
|
|
|
|
tree = ast.parse(module_file.read_text(encoding="utf-8"), filename=str(module_file))
|
|
target_name = attr_name.split(".")[0]
|
|
for node in tree.body:
|
|
if isinstance(node, ast.Assign):
|
|
for target in node.targets:
|
|
if isinstance(target, ast.Name) and target.id == target_name:
|
|
value = ast.literal_eval(node.value)
|
|
return str(value)
|
|
|
|
raise RuntimeError(f"Attribute {target_name!r} not found in {module_file}")
|
|
|
|
|
|
def get_version(repo_root: Path) -> str:
|
|
"""Return project version from pyproject.toml (static or dynamic)."""
|
|
project = project_metadata(repo_root)
|
|
if "version" in project:
|
|
return str(project["version"])
|
|
|
|
dynamic = load_pyproject(repo_root).get("tool", {}).get("setuptools", {}).get("dynamic", {})
|
|
version_cfg = dynamic.get("version")
|
|
if isinstance(version_cfg, dict) and "attr" in version_cfg:
|
|
return _resolve_version_attr(repo_root, version_cfg["attr"])
|
|
|
|
raise RuntimeError("Project version is not defined in pyproject.toml")
|
|
|
|
|
|
def bundle_items(repo_root: Path) -> list[dict]:
|
|
"""Return bundle rules from [[tool.orisys.bundle.items]] in pyproject.toml."""
|
|
items = load_pyproject(repo_root).get("tool", {}).get("orisys", {}).get("bundle", {}).get("items")
|
|
if not items:
|
|
raise RuntimeError("Missing [[tool.orisys.bundle.items]] in pyproject.toml")
|
|
return list(items)
|
|
|
|
|
|
def bundle_data_files(repo_root: Path) -> list[tuple[str, list[str]]]:
|
|
"""Map bundle items marked ``dev_install=true`` to install destinations.
|
|
|
|
Kept for tests / tooling. Prefer :func:`stage_dev_bundle_into` for pip
|
|
installs so files land inside ``site-packages/orisys/``.
|
|
"""
|
|
resolve_bundle_source_files = _load_wheel_layout_module().resolve_bundle_source_files
|
|
|
|
grouped: dict[str, list[str]] = {}
|
|
for item in bundle_items(repo_root):
|
|
if not item.get("dev_install", False):
|
|
continue
|
|
|
|
src_dir = repo_root / item["source"]
|
|
base_dest = f"orisys/{item['target']}".replace("\\", "/")
|
|
|
|
for src_file in resolve_bundle_source_files(repo_root, item):
|
|
rel_src = src_file.relative_to(repo_root).as_posix()
|
|
if item.get("preserve_tree", False):
|
|
rel_parent = src_file.relative_to(src_dir).parent.as_posix()
|
|
dest = base_dest if rel_parent == "." else f"{base_dest}/{rel_parent}"
|
|
else:
|
|
dest = base_dest
|
|
grouped.setdefault(dest, []).append(rel_src)
|
|
|
|
return [(dest, sorted(files)) for dest, files in sorted(grouped.items())]
|
|
|
|
|
|
def stage_dev_bundle_into(repo_root: Path, pkg_dir: Path) -> list[str]:
|
|
"""Copy ``dev_install`` bundle files under ``pkg_dir`` (the orisys package).
|
|
|
|
Returns package-relative paths that were written (e.g. ``sensors/box/...``).
|
|
"""
|
|
resolve_bundle_source_files = _load_wheel_layout_module().resolve_bundle_source_files
|
|
packaged: list[str] = []
|
|
|
|
for item in bundle_items(repo_root):
|
|
if not item.get("dev_install", False):
|
|
continue
|
|
|
|
src_dir = repo_root / item["source"]
|
|
dst_dir = pkg_dir / item["target"]
|
|
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for src_file in resolve_bundle_source_files(repo_root, item):
|
|
if item.get("preserve_tree", False):
|
|
rel = src_file.relative_to(src_dir)
|
|
dst_file = dst_dir / rel
|
|
else:
|
|
rel = Path(src_file.name)
|
|
dst_file = dst_dir / src_file.name
|
|
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(src_file, dst_file)
|
|
packaged.append(f"{item['target']}/{rel.as_posix()}")
|
|
|
|
return packaged
|