reorganized build directory
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Nuitka-compiled SDK wheel packaging."""
|
||||
@@ -0,0 +1,199 @@
|
||||
"""Build a Nuitka-compiled orisys wheel and publish it to SDK/{version}/.
|
||||
|
||||
Pipeline:
|
||||
1. Compile src/orisys → extension module (.pyd / .so)
|
||||
2. Stage wheel tree (compiled module + bundled files from pyproject.toml)
|
||||
3. Run bdist_wheel; on Linux, auditwheel-repair to a manylinux tag
|
||||
4. Smoke-test imports in a temporary venv (required for --target pypi-linux)
|
||||
|
||||
Targets:
|
||||
internal – local/SDK delivery (default). Linux still repairs with auditwheel.
|
||||
pypi-linux – PyPI binary wheel; requires Linux + CPython 3.10/3.11/3.12 + compliant tag.
|
||||
|
||||
Run from repo root:
|
||||
python build/run.py wheel
|
||||
python build/run.py wheel --target pypi-linux
|
||||
python build/wheel/build.py --target pypi-linux
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(_BUILD_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_BUILD_ROOT))
|
||||
|
||||
from common.paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repaired_wheel_dir, repo_path
|
||||
from common.pyproject_utils import bundle_items, extras_require, get_version, install_requires
|
||||
from common.verify.wheel import verify_built_wheel
|
||||
from wheel.compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
||||
from wheel.layout import copy_bundle_items, copy_orisys_blend, package_data_from_bundle
|
||||
from wheel.package import (
|
||||
is_compliant_manylinux_wheel,
|
||||
publish_wheel_to_sdk,
|
||||
repair_linux_wheel,
|
||||
run_bdist_wheel,
|
||||
write_wheel_setup,
|
||||
)
|
||||
|
||||
VALID_TARGETS = ("internal", "pypi-linux")
|
||||
# ABI tags that manylinux images ship under /opt/python/cpXYY-cpXYY.
|
||||
PYPI_LINUX_PYTHON_VERSIONS = ((3, 10), (3, 11), (3, 12))
|
||||
|
||||
|
||||
def _reset_dir(path: Path) -> None:
|
||||
"""Remove and recreate a build output directory."""
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
path.mkdir(parents=True)
|
||||
|
||||
|
||||
def _validate_pypi_linux_environment() -> None:
|
||||
if sys.platform != "linux":
|
||||
raise SystemExit(
|
||||
"[ERROR] --target pypi-linux must run inside a Linux manylinux container "
|
||||
"(for example quay.io/pypa/manylinux_2_28_x86_64 via Docker Desktop)."
|
||||
)
|
||||
if sys.version_info[:2] not in PYPI_LINUX_PYTHON_VERSIONS:
|
||||
supported = ", ".join(f"{major}.{minor}" for major, minor in PYPI_LINUX_PYTHON_VERSIONS)
|
||||
raise SystemExit(
|
||||
f"[ERROR] --target pypi-linux requires CPython {supported}, got "
|
||||
f"{sys.version_info.major}.{sys.version_info.minor}. "
|
||||
"Use /opt/python/cp310-cp310 (or cp311/cp312) in the manylinux_2_28 image."
|
||||
)
|
||||
|
||||
|
||||
def build(*, target: str = "internal", verify: bool | None = None) -> Path:
|
||||
if target not in VALID_TARGETS:
|
||||
raise SystemExit(f"[ERROR] Unknown target {target!r}; choose from {VALID_TARGETS}")
|
||||
|
||||
if target == "pypi-linux":
|
||||
_validate_pypi_linux_environment()
|
||||
|
||||
# pypi-linux always verifies; internal opts in with --verify.
|
||||
if verify is None:
|
||||
verify = target == "pypi-linux"
|
||||
|
||||
# --- 1. Compile ---
|
||||
install_nuitka()
|
||||
|
||||
version = get_version(REPO_ROOT)
|
||||
_reset_dir(NUITKA_COMPILE_DIR)
|
||||
_reset_dir(NUITKA_WHEEL_DIR)
|
||||
|
||||
print(f"\n=== Compiling with Nuitka (target={target}, version={version}) ===")
|
||||
env = os.environ.copy()
|
||||
src_dir = repo_path("src")
|
||||
env["PYTHONPATH"] = src_dir.as_posix() + os.pathsep + env.get("PYTHONPATH", "")
|
||||
ext_file = compile_orisys(NUITKA_COMPILE_DIR, env)
|
||||
|
||||
# --- 2. Stage wheel contents ---
|
||||
print("\n=== Preparing Wheel Structure ===")
|
||||
pkg_dir = NUITKA_WHEEL_DIR / "wheel_root" / "orisys"
|
||||
pkg_dir.mkdir(parents=True)
|
||||
|
||||
ext_suffix = copy_compiled_orisys(pkg_dir, ext_file)
|
||||
|
||||
wheel_root = NUITKA_WHEEL_DIR / "wheel_root"
|
||||
blend_count = copy_orisys_blend(REPO_ROOT, wheel_root)
|
||||
print(f"Staged orisys_blend Python modules: {blend_count}")
|
||||
|
||||
# Bundle rules live in pyproject.toml → [[tool.orisys.bundle.items]]
|
||||
bundle = bundle_items(REPO_ROOT)
|
||||
copied_counts = copy_bundle_items(REPO_ROOT, pkg_dir, bundle)
|
||||
print(f"Copied {copied_counts['configs']} JSON config files")
|
||||
print(f"Copied {copied_counts['examples']} example files to orisys/examples/")
|
||||
print(f"Copied {copied_counts['docs']} doc files to orisys/docs/")
|
||||
print(f"Copied {copied_counts['assets']} finger asset files to orisys/assets/")
|
||||
|
||||
write_wheel_setup(
|
||||
NUITKA_WHEEL_DIR,
|
||||
version=version,
|
||||
package_data=package_data_from_bundle(ext_suffix, bundle),
|
||||
install_requires=install_requires(REPO_ROOT),
|
||||
extras_require=extras_require(REPO_ROOT),
|
||||
)
|
||||
|
||||
# --- 3. Build wheel, repair on Linux, publish to SDK/ ---
|
||||
wheel_path = run_bdist_wheel(NUITKA_WHEEL_DIR)
|
||||
if wheel_path is None:
|
||||
raise SystemExit("\n[ERROR] Wheel not found after bdist_wheel.")
|
||||
|
||||
repair_dir = repaired_wheel_dir()
|
||||
final_candidate = repair_linux_wheel(wheel_path, repair_dir)
|
||||
|
||||
if target == "pypi-linux" and not is_compliant_manylinux_wheel(final_candidate):
|
||||
raise SystemExit(
|
||||
f"[ERROR] Wheel is not PyPI-compliant: {final_candidate.name}. "
|
||||
"Expected a manylinux_2_17_x86_64 (or newer) platform tag."
|
||||
)
|
||||
|
||||
# Keep a durable copy under dist/repaired for upload tooling.
|
||||
dist_repaired = REPO_ROOT / "dist" / "repaired"
|
||||
dist_repaired.mkdir(parents=True, exist_ok=True)
|
||||
dist_copy = dist_repaired / final_candidate.name
|
||||
if dist_copy.resolve() != final_candidate.resolve():
|
||||
shutil.copy2(final_candidate, dist_copy)
|
||||
|
||||
final_wheel = publish_wheel_to_sdk(final_candidate, version)
|
||||
print(f"\n[SUCCESS] Wheel created and copied to: {final_wheel}")
|
||||
print(f"[SUCCESS] Upload candidate also at: {dist_copy}")
|
||||
|
||||
# --- 4. Verify (isolated venv; does not touch the active environment) ---
|
||||
if verify:
|
||||
print("\n=== Verifying wheel imports in isolated venv ===")
|
||||
try:
|
||||
verify_built_wheel(final_wheel)
|
||||
print("[SUCCESS] Wheel smoke test passed.")
|
||||
except subprocess.CalledProcessError as exc:
|
||||
if target == "pypi-linux":
|
||||
raise SystemExit(
|
||||
f"[ERROR] Wheel verify failed (required for pypi-linux): {exc}"
|
||||
) from exc
|
||||
print(f"\n[WARNING] Wheel verify failed: {exc}")
|
||||
print("The wheel was built successfully; verification runs in an isolated temp venv.")
|
||||
else:
|
||||
print("\n[INFO] Skipping wheel verify (pass --verify to enable).")
|
||||
|
||||
return final_wheel
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
parser = argparse.ArgumentParser(description="Build a Nuitka-compiled orisys wheel.")
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
choices=VALID_TARGETS,
|
||||
default="internal",
|
||||
help="internal: SDK delivery; pypi-linux: manylinux PyPI binary wheel",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify",
|
||||
action="store_true",
|
||||
help="Run isolated venv smoke test (always on for --target pypi-linux)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-verify",
|
||||
action="store_true",
|
||||
help="Skip smoke test when using --target internal",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
verify: bool | None
|
||||
if args.target == "pypi-linux":
|
||||
verify = True
|
||||
elif args.verify:
|
||||
verify = True
|
||||
elif args.skip_verify:
|
||||
verify = False
|
||||
else:
|
||||
verify = None
|
||||
build(target=args.target, verify=verify)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Nuitka compilation of the orisys package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from importlib.machinery import EXTENSION_SUFFIXES
|
||||
from pathlib import Path
|
||||
|
||||
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(_BUILD_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_BUILD_ROOT))
|
||||
|
||||
from common.paths import repo_path
|
||||
|
||||
NUITKA_INCLUDE_PACKAGES = (
|
||||
"orisys",
|
||||
"orisys.finger",
|
||||
"orisys.unroll",
|
||||
"orisys.hhd",
|
||||
"orisys.hhd.solvers",
|
||||
)
|
||||
|
||||
NUITKA_NOFOLLOW_IMPORTS = (
|
||||
# Keep as runtime deps (shipped via wheel install_requires / extras).
|
||||
"numpy",
|
||||
"scipy",
|
||||
"cv2",
|
||||
"numba",
|
||||
"orisys_blend",
|
||||
"cupy",
|
||||
"cupyx",
|
||||
"pyopencl",
|
||||
"open3d",
|
||||
"PyQt5",
|
||||
"pyqtgraph",
|
||||
)
|
||||
|
||||
|
||||
def install_nuitka() -> None:
|
||||
print("Checking Nuitka installation...")
|
||||
try:
|
||||
import nuitka # noqa: F401
|
||||
print("Nuitka is installed.")
|
||||
except ImportError:
|
||||
print("Installing Nuitka...")
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "pip", "install", "nuitka", "ordered-set", "zstandard"]
|
||||
)
|
||||
|
||||
|
||||
def find_compiled_extension(output_dir: Path, module_name: str) -> Path:
|
||||
candidates = []
|
||||
for suffix in EXTENSION_SUFFIXES:
|
||||
candidates.extend(output_dir.glob(f"{module_name}*{suffix}"))
|
||||
if not candidates:
|
||||
all_files = ", ".join(p.name for p in output_dir.iterdir()) if output_dir.exists() else "(none)"
|
||||
raise RuntimeError(
|
||||
f"Nuitka did not produce a compiled extension for {module_name}. Found: {all_files}"
|
||||
)
|
||||
return sorted(candidates, key=lambda p: len(p.name))[0]
|
||||
|
||||
|
||||
def compile_orisys(build_root: Path, env: dict[str, str]) -> Path:
|
||||
module_output_dir = build_root / "orisys"
|
||||
if module_output_dir.exists():
|
||||
shutil.rmtree(module_output_dir)
|
||||
module_output_dir.mkdir(parents=True)
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"nuitka",
|
||||
"--module",
|
||||
"--output-dir=" + str(module_output_dir),
|
||||
"--remove-output",
|
||||
"--no-pyi-file",
|
||||
repo_path("src/orisys").as_posix(),
|
||||
]
|
||||
for package_name in NUITKA_INCLUDE_PACKAGES:
|
||||
cmd.append(f"--include-package={package_name}")
|
||||
for import_name in NUITKA_NOFOLLOW_IMPORTS:
|
||||
cmd.append(f"--nofollow-import-to={import_name}")
|
||||
|
||||
print(f"Running: {' '.join(cmd)}")
|
||||
subprocess.check_call(cmd, env=env)
|
||||
|
||||
ext_file = find_compiled_extension(module_output_dir, "orisys")
|
||||
print(f"Generated compiled extension for orisys: {ext_file}")
|
||||
return ext_file
|
||||
|
||||
|
||||
def copy_compiled_orisys(pkg_dir: Path, ext_file: Path) -> str:
|
||||
ext_suffix = "".join(ext_file.suffixes)
|
||||
dest_ext = pkg_dir / f"__init__{ext_suffix}"
|
||||
shutil.copy(ext_file, dest_ext)
|
||||
print(f"Copied compiled module orisys to {dest_ext}")
|
||||
return ext_suffix
|
||||
@@ -0,0 +1,26 @@
|
||||
# manylinux_2_28: modern glibc baseline with binary wheels for numpy/scipy/opencv.
|
||||
# Prefer this over manylinux2014 — recent NumPy requires GCC >= 10.3 and will try
|
||||
# (and fail) to build from source on the older image.
|
||||
FROM quay.io/pypa/manylinux_2_28_x86_64:latest
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
PIP_ROOT_USER_ACTION=ignore
|
||||
|
||||
# Only install compile/repair tooling here. Runtime deps (numpy/scipy/opencv/numba)
|
||||
# are declared in the wheel metadata and installed by pip when users install orisys.
|
||||
# Nuitka is configured with --nofollow-import-to for those packages.
|
||||
RUN for PYBIN in \
|
||||
/opt/python/cp310-cp310/bin \
|
||||
/opt/python/cp311-cp311/bin \
|
||||
/opt/python/cp312-cp312/bin; do \
|
||||
echo "=== Installing build tooling into ${PYBIN} ===" && \
|
||||
"${PYBIN}/python" -m pip install --upgrade pip setuptools wheel && \
|
||||
"${PYBIN}/python" -m pip install \
|
||||
"nuitka>=2.0" \
|
||||
ordered-set \
|
||||
zstandard \
|
||||
auditwheel; \
|
||||
done
|
||||
|
||||
WORKDIR /src
|
||||
ENTRYPOINT ["/bin/bash", "/src/build/wheel/docker/entrypoint.sh"]
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs inside quay.io/pypa/manylinux_2_28_x86_64 with the repo mounted at /src.
|
||||
# Builds one Nuitka wheel per CPython ABI listed in ORISYS_PYTHON_TAGS.
|
||||
set -euo pipefail
|
||||
|
||||
TARGET="${ORISYS_BUILD_TARGET:-pypi-linux}"
|
||||
# Comma-separated manylinux tags, e.g. "cp310-cp310,cp311-cp311,cp312-cp312"
|
||||
PYTHON_TAGS="${ORISYS_PYTHON_TAGS:-cp310-cp310,cp311-cp311,cp312-cp312}"
|
||||
|
||||
cd /src
|
||||
|
||||
echo "=== manylinux entrypoint ==="
|
||||
echo "Target: ${TARGET}"
|
||||
echo "Python tags: ${PYTHON_TAGS}"
|
||||
uname -a
|
||||
|
||||
IFS=',' read -r -a TAG_ARRAY <<< "${PYTHON_TAGS}"
|
||||
FAILED=0
|
||||
BUILT=()
|
||||
|
||||
for tag in "${TAG_ARRAY[@]}"; do
|
||||
tag="$(echo "${tag}" | tr -d '[:space:]')"
|
||||
if [[ -z "${tag}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
PYBIN="/opt/python/${tag}/bin"
|
||||
if [[ ! -x "${PYBIN}/python" ]]; then
|
||||
echo "[ERROR] Python not found for tag ${tag}: ${PYBIN}/python" >&2
|
||||
FAILED=1
|
||||
continue
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Building orisys wheel: ${tag}"
|
||||
echo "Python: $("${PYBIN}/python" -V)"
|
||||
echo "========================================"
|
||||
|
||||
export PATH="${PYBIN}:${PATH}"
|
||||
export PYTHONPATH="/src/src${PYTHONPATH:+:${PYTHONPATH}}"
|
||||
|
||||
"${PYBIN}/python" -m pip install -q --upgrade pip setuptools wheel auditwheel
|
||||
|
||||
if "${PYBIN}/python" build/wheel/build.py --target "${TARGET}"; then
|
||||
BUILT+=("${tag}")
|
||||
else
|
||||
echo "[ERROR] Build failed for ${tag}" >&2
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Build artifacts ==="
|
||||
ls -la /src/dist/repaired/ || true
|
||||
ls -la /src/SDK/*/ || true
|
||||
|
||||
if [[ ${#BUILT[@]} -gt 0 ]]; then
|
||||
echo "Built tags: ${BUILT[*]}"
|
||||
fi
|
||||
|
||||
if [[ "${FAILED}" -ne 0 ]]; then
|
||||
echo "[ERROR] One or more Python ABI builds failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[SUCCESS] All requested manylinux wheels built."
|
||||
@@ -0,0 +1,100 @@
|
||||
# Build PyPI-compliant manylinux wheels for CPython 3.10/3.11/3.12 using Docker Desktop.
|
||||
#
|
||||
# Usage (from repo root, PowerShell):
|
||||
# .\build\wheel\docker\run.ps1
|
||||
# .\build\build_manylinux_docker.ps1 # compatibility wrapper
|
||||
#
|
||||
# From cmd.exe:
|
||||
# powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
||||
|
||||
param(
|
||||
[ValidateSet("pypi-linux", "internal")]
|
||||
[string]$Target = "pypi-linux",
|
||||
[string]$PythonVersions = "3.10,3.11,3.12",
|
||||
[switch]$SkipImageBuild,
|
||||
[string]$ImageName = "orisys-manylinux"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Convert-ToManylinuxTag {
|
||||
param([string]$Version)
|
||||
$v = $Version.Trim()
|
||||
switch ($v) {
|
||||
"3.10" { return "cp310-cp310" }
|
||||
"3.11" { return "cp311-cp311" }
|
||||
"3.12" { return "cp312-cp312" }
|
||||
"cp310-cp310" { return "cp310-cp310" }
|
||||
"cp311-cp311" { return "cp311-cp311" }
|
||||
"cp312-cp312" { return "cp312-cp312" }
|
||||
default {
|
||||
throw "Unsupported Python version '$Version'. Use 3.10, 3.11, and/or 3.12."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path
|
||||
Set-Location $RepoRoot
|
||||
|
||||
$tags = @()
|
||||
foreach ($part in ($PythonVersions -split ",")) {
|
||||
if ([string]::IsNullOrWhiteSpace($part)) { continue }
|
||||
$tags += (Convert-ToManylinuxTag $part)
|
||||
}
|
||||
if ($tags.Count -eq 0) {
|
||||
throw "No Python versions specified."
|
||||
}
|
||||
$pythonTags = [string]::Join(",", $tags)
|
||||
|
||||
Write-Host "=== Orisys manylinux Docker build ==="
|
||||
Write-Host "Repo: $RepoRoot"
|
||||
Write-Host "Target: $Target"
|
||||
Write-Host "Image: $ImageName"
|
||||
Write-Host "Python tags: $pythonTags"
|
||||
|
||||
# Docker Desktop must be in Linux container mode for manylinux.
|
||||
$dockerInfo = docker version --format "{{.Server.Os}}" 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker is not available. Start Docker Desktop and retry."
|
||||
}
|
||||
if ($dockerInfo -ne "linux") {
|
||||
throw "Docker server OS is '$dockerInfo'. Switch Docker Desktop to Linux containers."
|
||||
}
|
||||
|
||||
$entrypoint = Join-Path $RepoRoot "build\wheel\docker\entrypoint.sh"
|
||||
# Normalize CRLF -> LF so bash in the container can execute the script.
|
||||
$raw = [System.IO.File]::ReadAllText($entrypoint)
|
||||
$normalized = $raw -replace "`r`n", "`n" -replace "`r", "`n"
|
||||
[System.IO.File]::WriteAllText($entrypoint, $normalized)
|
||||
|
||||
$dockerfile = "build/wheel/docker/Dockerfile.manylinux"
|
||||
|
||||
if (-not $SkipImageBuild) {
|
||||
Write-Host "`n=== Building Docker image ==="
|
||||
docker build -f $dockerfile -t $ImageName .
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "docker build failed"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n=== Running manylinux container ==="
|
||||
# Mount the repo so artifacts land under dist/repaired and SDK/<version>/ on the host.
|
||||
docker run --rm `
|
||||
-e "ORISYS_BUILD_TARGET=$Target" `
|
||||
-e "ORISYS_PYTHON_TAGS=$pythonTags" `
|
||||
-v "${RepoRoot}:/src" `
|
||||
$ImageName
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "manylinux container build failed"
|
||||
}
|
||||
|
||||
Write-Host "`n=== Host artifacts ==="
|
||||
$repaired = Join-Path $RepoRoot "dist\repaired"
|
||||
if (Test-Path $repaired) {
|
||||
Get-ChildItem $repaired -Filter *.whl | ForEach-Object { Write-Host $_.FullName }
|
||||
} else {
|
||||
Write-Warning "No dist/repaired directory found."
|
||||
}
|
||||
|
||||
Write-Host "`nNext steps: see docs/publishing.md (TestPyPI then PyPI)."
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Copy bundled non-code files into the Nuitka wheel tree."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def resolve_bundle_source_files(repo_root: Path, item: dict) -> list[Path]:
|
||||
"""Resolve explicit files and/or glob patterns for one bundle item."""
|
||||
src_dir = repo_root / item["source"]
|
||||
patterns = item.get("patterns", [])
|
||||
files = item.get("files", [])
|
||||
if not patterns and not files:
|
||||
raise ValueError(f"Bundle item {item['name']!r} requires patterns and/or files")
|
||||
|
||||
seen: set[Path] = set()
|
||||
resolved: list[Path] = []
|
||||
|
||||
def add(path: Path) -> None:
|
||||
key = path.resolve()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
resolved.append(path)
|
||||
|
||||
for pattern in patterns:
|
||||
for path in sorted(src_dir.glob(pattern)):
|
||||
if path.is_file():
|
||||
add(path)
|
||||
|
||||
for name in files:
|
||||
path = src_dir / name
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"Bundle file not found for {item['name']}: {path}")
|
||||
add(path)
|
||||
|
||||
return resolved
|
||||
|
||||
|
||||
def package_data_paths_for_item(item: dict) -> list[str]:
|
||||
"""Return setuptools package_data paths relative to the orisys package."""
|
||||
target = str(item["target"]).replace("\\", "/")
|
||||
entries = [f"{target}/{pattern}" for pattern in item.get("patterns", [])]
|
||||
entries.extend(f"{target}/{name}" for name in item.get("files", []))
|
||||
return entries
|
||||
|
||||
|
||||
def data_file_entries_for_item(item: dict) -> list[str]:
|
||||
"""Return setuptools data_files paths relative to the repo root."""
|
||||
source = str(item["source"]).replace("\\", "/")
|
||||
entries = [f"{source}/{pattern}" for pattern in item.get("patterns", [])]
|
||||
entries.extend(f"{source}/{name}" for name in item.get("files", []))
|
||||
return entries
|
||||
|
||||
|
||||
def copy_bundle_items(repo_root: Path, pkg_dir: Path, items: list[dict]) -> dict[str, int]:
|
||||
copied_counts: dict[str, int] = {}
|
||||
for item in items:
|
||||
name = item["name"]
|
||||
src_dir = repo_root / item["source"]
|
||||
dst_dir = pkg_dir / item["target"]
|
||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied = 0
|
||||
for src_file in resolve_bundle_source_files(repo_root, item):
|
||||
if item.get("preserve_tree", False):
|
||||
dst_file = dst_dir / src_file.relative_to(src_dir)
|
||||
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
dst_file = dst_dir / src_file.name
|
||||
shutil.copy(src_file, dst_file)
|
||||
copied += 1
|
||||
print(f"Copied {name}: {src_file}")
|
||||
|
||||
if name == "examples":
|
||||
(dst_dir / "__init__.py").write_text("# Examples for orisys package\n", encoding="utf-8")
|
||||
|
||||
copied_counts[name] = copied
|
||||
return copied_counts
|
||||
|
||||
|
||||
def package_data_from_bundle(ext_suffix: str, items: list[dict]) -> dict[str, list[str]]:
|
||||
package_data: dict[str, list[str]] = {
|
||||
"orisys": [f"*{ext_suffix}"],
|
||||
"orisys.examples": ["*.py", "README.md", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"],
|
||||
"orisys_blend": ["*.py"],
|
||||
}
|
||||
for item in items:
|
||||
if item["name"] == "examples":
|
||||
continue
|
||||
package_data["orisys"].extend(package_data_paths_for_item(item))
|
||||
return package_data
|
||||
|
||||
|
||||
def copy_orisys_blend(repo_root: Path, wheel_root: Path) -> int:
|
||||
"""Copy pure-Python orisys_blend (Numba kernels) next to the Nuitka orisys .pyd."""
|
||||
src = repo_root / "src" / "orisys_blend"
|
||||
dst = wheel_root / "orisys_blend"
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
count = sum(1 for p in dst.rglob("*.py") if p.is_file())
|
||||
print(f"Copied orisys_blend ({count} .py files) to {dst}")
|
||||
return count
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Assemble and build the Nuitka wheel with setuptools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(_BUILD_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_BUILD_ROOT))
|
||||
|
||||
from common.paths import sdk_dir
|
||||
|
||||
# PyPI rejects bare tags like "manylinux_x86_64"; require a glibc ABI version.
|
||||
_MANYLINUX_TAG_RE = re.compile(
|
||||
r"(?:manylinux2014|manylinux_2_(?:17|24|27|28|31|34|35))_x86_64"
|
||||
)
|
||||
|
||||
|
||||
def write_wheel_setup(
|
||||
wheel_dir: Path,
|
||||
*,
|
||||
version: str,
|
||||
package_data: dict[str, list[str]],
|
||||
install_requires: list[str],
|
||||
extras_require: dict[str, list[str]],
|
||||
) -> Path:
|
||||
setup_py_path = wheel_dir / "setup.py"
|
||||
setup_content = f"""
|
||||
from setuptools import setup, Distribution
|
||||
|
||||
class BinaryDistribution(Distribution):
|
||||
def has_ext_modules(self):
|
||||
return True
|
||||
|
||||
setup(
|
||||
name="orisys",
|
||||
version="{version}",
|
||||
description="Orisys - Optical Tactile Sensor SDK (Nuitka Compiled)",
|
||||
author="Orisys",
|
||||
packages=['orisys', 'orisys.examples', 'orisys_blend'],
|
||||
package_dir={{'': 'wheel_root'}},
|
||||
package_data={package_data},
|
||||
include_package_data=True,
|
||||
install_requires={install_requires!r},
|
||||
extras_require={extras_require!r},
|
||||
distclass=BinaryDistribution,
|
||||
zip_safe=False,
|
||||
)
|
||||
"""
|
||||
setup_py_path.write_text(setup_content, encoding="utf-8")
|
||||
return setup_py_path
|
||||
|
||||
|
||||
def run_bdist_wheel(wheel_dir: Path) -> Path | None:
|
||||
print("\n=== Building Wheel ===")
|
||||
subprocess.check_call([sys.executable, "setup.py", "bdist_wheel"], cwd=wheel_dir)
|
||||
wheels = sorted((wheel_dir / "dist").glob("*.whl"))
|
||||
return wheels[0] if wheels else None
|
||||
|
||||
|
||||
def _ensure_auditwheel() -> None:
|
||||
try:
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "auditwheel", "--version"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("Installing auditwheel...")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "auditwheel", "-q"])
|
||||
|
||||
|
||||
def is_compliant_manylinux_wheel(wheel_path: Path) -> bool:
|
||||
"""True if the wheel filename uses a PyPI-accepted manylinux x86_64 tag."""
|
||||
return bool(_MANYLINUX_TAG_RE.search(wheel_path.name))
|
||||
|
||||
|
||||
def repair_linux_wheel(wheel_path: Path, repair_dir: Path) -> Path:
|
||||
"""Run auditwheel show/repair and return a PyPI-compliant manylinux wheel.
|
||||
|
||||
Raises RuntimeError if repair fails or the result still lacks a valid tag.
|
||||
On non-Linux platforms the input wheel is returned unchanged.
|
||||
"""
|
||||
if sys.platform != "linux":
|
||||
print("Skipping auditwheel repair (not running on Linux).")
|
||||
return wheel_path
|
||||
|
||||
_ensure_auditwheel()
|
||||
repair_dir = repair_dir.resolve()
|
||||
if repair_dir.exists():
|
||||
shutil.rmtree(repair_dir)
|
||||
repair_dir.mkdir(parents=True)
|
||||
|
||||
print("\n=== auditwheel show ===")
|
||||
subprocess.check_call([sys.executable, "-m", "auditwheel", "show", str(wheel_path)])
|
||||
|
||||
print("\n=== auditwheel repair ===")
|
||||
subprocess.check_call(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"auditwheel",
|
||||
"repair",
|
||||
str(wheel_path),
|
||||
"--wheel-dir",
|
||||
str(repair_dir),
|
||||
]
|
||||
)
|
||||
|
||||
repaired = sorted(repair_dir.glob("*.whl"))
|
||||
if not repaired:
|
||||
raise RuntimeError(f"auditwheel repair produced no wheels in {repair_dir}")
|
||||
|
||||
compliant = [path for path in repaired if is_compliant_manylinux_wheel(path)]
|
||||
if not compliant:
|
||||
names = ", ".join(path.name for path in repaired)
|
||||
raise RuntimeError(
|
||||
"auditwheel repair did not produce a PyPI-compliant manylinux tag. "
|
||||
f"Got: {names}. Rebuild inside quay.io/pypa/manylinux_2_28_x86_64 "
|
||||
"(or manylinux2014_x86_64) instead of a generic Linux host."
|
||||
)
|
||||
|
||||
final = compliant[0]
|
||||
print(f"Repaired wheel: {final.name}")
|
||||
return final
|
||||
|
||||
|
||||
def publish_wheel_to_sdk(wheel_path: Path, version: str) -> Path:
|
||||
sdk_version_dir = sdk_dir(version)
|
||||
sdk_version_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest_wheel = sdk_version_dir / wheel_path.name
|
||||
if dest_wheel.exists():
|
||||
dest_wheel.unlink()
|
||||
shutil.copy2(str(wheel_path), str(dest_wheel))
|
||||
return dest_wheel
|
||||
@@ -0,0 +1,70 @@
|
||||
@echo off
|
||||
REM Build Nuitka wheels for Python 3.10, 3.11, and 3.12.
|
||||
REM Expects conda envs named py310, py311, py312 (or set CONDA_ENVS_ROOT).
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
cd /d "%~dp0\..\.."
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Could not change to repository root.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not defined CONDA_ENVS_ROOT (
|
||||
if exist "%USERPROFILE%\anaconda3\envs" (
|
||||
set "CONDA_ENVS_ROOT=%USERPROFILE%\anaconda3\envs"
|
||||
) else if exist "%USERPROFILE%\miniconda3\envs" (
|
||||
set "CONDA_ENVS_ROOT=%USERPROFILE%\miniconda3\envs"
|
||||
) else if exist "%LOCALAPPDATA%\anaconda3\envs" (
|
||||
set "CONDA_ENVS_ROOT=%LOCALAPPDATA%\anaconda3\envs"
|
||||
)
|
||||
)
|
||||
|
||||
where conda >nul 2>&1
|
||||
set "HAS_CONDA=0"
|
||||
if not errorlevel 1 set "HAS_CONDA=1"
|
||||
|
||||
set FAILED=0
|
||||
for %%E in (py310 py311 py312) do (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Building orisys wheel: %%E
|
||||
echo ========================================
|
||||
|
||||
set "OK=0"
|
||||
if !HAS_CONDA! equ 1 (
|
||||
call conda run --no-capture-output -n %%E python build\wheel\build.py
|
||||
if not errorlevel 1 set "OK=1"
|
||||
)
|
||||
|
||||
if !OK! equ 0 (
|
||||
if not defined CONDA_ENVS_ROOT (
|
||||
echo ERROR: Could not find conda env %%E. Set CONDA_ENVS_ROOT or add conda to PATH.
|
||||
) else (
|
||||
set "PYEXE=!CONDA_ENVS_ROOT!\%%E\python.exe"
|
||||
if not exist "!PYEXE!" (
|
||||
echo ERROR: Python not found: !PYEXE!
|
||||
) else (
|
||||
"!PYEXE!" build\wheel\build.py
|
||||
if not errorlevel 1 set "OK=1"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if !OK! equ 0 set FAILED=1
|
||||
)
|
||||
|
||||
echo.
|
||||
if !FAILED! equ 1 (
|
||||
echo One or more wheel builds failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo SUCCESS: all wheels built under SDK\
|
||||
for /f "delims=" %%V in ('dir /b /ad SDK 2^>nul') do (
|
||||
for %%W in ("SDK\%%V\orisys-*-cp310-*.whl" "SDK\%%V\orisys-*-cp311-*.whl" "SDK\%%V\orisys-*-cp312-*.whl") do (
|
||||
if exist %%W echo %%~nxW
|
||||
)
|
||||
)
|
||||
|
||||
endlocal
|
||||
exit /b 0
|
||||
Reference in New Issue
Block a user