finished docker manylinux compliation for pypi
This commit is contained in:
@@ -94,11 +94,24 @@ cfg = orisys.export_config("ddjx01", "./config/exported_config.json")
|
||||
- Example entry points: `examples/README.md`
|
||||
- User documentation: `docs/get_started.md`
|
||||
- Packaging helper: `build/build_nuitka.py` (compiled SDK wheel)
|
||||
- Manylinux / PyPI binary wheel (Docker Desktop): `docs/publishing.md`
|
||||
- GUI standalone folder: `build/build_gui_win.bat` → `dist/OrisysDashboard/`
|
||||
- Specialized utilities: `tools/`
|
||||
|
||||
## Publishing (Linux manylinux)
|
||||
|
||||
To build PyPI-compliant CPython 3.10/3.11/3.12 Linux wheels from Windows Docker Desktop:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
||||
```
|
||||
|
||||
Upload candidates land in `dist/repaired/`. Full TestPyPI → PyPI steps:
|
||||
`docs/publishing.md`.
|
||||
|
||||
## Notes for Collaborators
|
||||
|
||||
- Generated build outputs such as `build/`, `dist/`, and `SDK/` are intentionally ignored.
|
||||
- Large or generated tooling outputs should stay under ignored output folders rather than be committed.
|
||||
- If you add new images or arrays that should be versioned, review `.gitignore` first.
|
||||
- Never commit PyPI / TestPyPI API tokens.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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/manylinux_entrypoint.sh"]
|
||||
@@ -0,0 +1,2 @@
|
||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.12
|
||||
@@ -0,0 +1,104 @@
|
||||
# Build PyPI-compliant manylinux wheels for CPython 3.10/3.11/3.12 using Docker Desktop.
|
||||
#
|
||||
# Usage (from repo root, PowerShell):
|
||||
# .\build\build_manylinux_docker.ps1
|
||||
# .\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.11,3.12
|
||||
# .\build\build_manylinux_docker.ps1 -PythonVersions 3.11
|
||||
# .\build\build_manylinux_docker.ps1 -SkipImageBuild
|
||||
#
|
||||
# From cmd.exe:
|
||||
# powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Docker Desktop running in Linux container mode
|
||||
# - Enough disk for the manylinux image (~2GB+)
|
||||
|
||||
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\manylinux_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)
|
||||
|
||||
if (-not $SkipImageBuild) {
|
||||
Write-Host "`n=== Building Docker image ==="
|
||||
docker build -f build/Dockerfile.manylinux -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)."
|
||||
+123
-22
@@ -1,36 +1,80 @@
|
||||
"""Build a Nuitka-compiled orisys wheel and publish it to SDK/{version}/.
|
||||
|
||||
Pipeline:
|
||||
1. Compile src/orisys → .pyd
|
||||
1. Compile src/orisys → extension module (.pyd / .so)
|
||||
2. Stage wheel tree (compiled module + bundled files from pyproject.toml)
|
||||
3. Run bdist_wheel and move output to SDK/{version}/
|
||||
4. Smoke-test imports in a temporary venv
|
||||
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)
|
||||
|
||||
Run from repo root: python build/build_nuitka.py
|
||||
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/build_nuitka.py
|
||||
python build/build_nuitka.py --target pypi-linux
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from nuitka_compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
||||
from paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repo_path
|
||||
from paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repaired_wheel_dir, repo_path
|
||||
from pyproject_utils import bundle_items, extras_require, get_version, install_requires
|
||||
from verify import verify_built_wheel
|
||||
from wheel_build import publish_wheel_to_sdk, run_bdist_wheel, write_wheel_setup
|
||||
from wheel_build import (
|
||||
is_compliant_manylinux_wheel,
|
||||
publish_wheel_to_sdk,
|
||||
repair_linux_wheel,
|
||||
run_bdist_wheel,
|
||||
write_wheel_setup,
|
||||
)
|
||||
from wheel_layout import copy_bundle_items, copy_orisys_blend, package_data_from_bundle
|
||||
|
||||
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) -> None:
|
||||
|
||||
def _reset_dir(path: Path) -> None:
|
||||
"""Remove and recreate a build output directory."""
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
path.mkdir(parents=True)
|
||||
|
||||
|
||||
def build() -> None:
|
||||
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()
|
||||
|
||||
@@ -38,7 +82,7 @@ def build() -> None:
|
||||
_reset_dir(NUITKA_COMPILE_DIR)
|
||||
_reset_dir(NUITKA_WHEEL_DIR)
|
||||
|
||||
print("\n=== Compiling with Nuitka ===")
|
||||
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", "")
|
||||
@@ -71,23 +115,80 @@ def build() -> None:
|
||||
extras_require=extras_require(REPO_ROOT),
|
||||
)
|
||||
|
||||
# --- 3. Build wheel and publish to SDK/ ---
|
||||
# --- 3. Build wheel, repair on Linux, publish to SDK/ ---
|
||||
wheel_path = run_bdist_wheel(NUITKA_WHEEL_DIR)
|
||||
if wheel_path is None:
|
||||
print("\n[ERROR] Wheel not found.")
|
||||
return
|
||||
raise SystemExit("\n[ERROR] Wheel not found after bdist_wheel.")
|
||||
|
||||
final_wheel = publish_wheel_to_sdk(wheel_path, version)
|
||||
print(f"\n[SUCCESS] Wheel created and moved to: {final_wheel}")
|
||||
repair_dir = repaired_wheel_dir()
|
||||
final_candidate = repair_linux_wheel(wheel_path, repair_dir)
|
||||
|
||||
# # --- 4. Verify (isolated venv; does not touch the active environment) ---
|
||||
# print("\n=== Verifying finger mesh/viewer imports ===")
|
||||
# try:
|
||||
# verify_built_wheel(final_wheel)
|
||||
# except subprocess.CalledProcessError as exc:
|
||||
# print(f"\n[WARNING] Wheel verify failed: {exc}")
|
||||
# print("The wheel was built successfully; verification runs in an isolated temp venv.")
|
||||
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__":
|
||||
build()
|
||||
main()
|
||||
|
||||
@@ -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/build_nuitka.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."
|
||||
@@ -19,11 +19,18 @@ NUITKA_INCLUDE_PACKAGES = (
|
||||
)
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,3 +18,8 @@ def repo_path(*parts: str) -> Path:
|
||||
|
||||
def sdk_dir(version: str) -> Path:
|
||||
return REPO_ROOT / "SDK" / version
|
||||
|
||||
|
||||
def repaired_wheel_dir() -> Path:
|
||||
"""Directory for auditwheel-repaired wheels under the Nuitka wheel build tree."""
|
||||
return NUITKA_WHEEL_DIR / "repaired"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import ast
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -41,17 +41,29 @@ def extras_require(repo_root: Path) -> dict[str, list[str]]:
|
||||
|
||||
def _resolve_version_attr(repo_root: Path, attr: str) -> str:
|
||||
"""Resolve a setuptools dynamic version attr (e.g. orisys.__version__)."""
|
||||
src_dir = str(repo_root / "src")
|
||||
if src_dir not in sys.path:
|
||||
sys.path.insert(0, src_dir)
|
||||
module_name, _, attr_name = attr.partition(".")
|
||||
if not attr_name:
|
||||
raise RuntimeError(f"Invalid dynamic version attr: {attr!r}")
|
||||
module = importlib.import_module(module_name)
|
||||
value = module
|
||||
for name in attr_name.split("."):
|
||||
value = getattr(value, name)
|
||||
return str(value)
|
||||
|
||||
# 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:
|
||||
|
||||
+30
-12
@@ -11,6 +11,32 @@ from pathlib import Path
|
||||
|
||||
from paths import GUI_DIST_DIR
|
||||
|
||||
# Public SDK surface that must import from a released wheel.
|
||||
_SMOKE_IMPORT_SCRIPT = """
|
||||
import orisys
|
||||
from orisys import Sensor, list_config, export_config, util, finger
|
||||
from orisys.finger import UVMeshViewer, build_uv_surface_mesh, FingerRuntime
|
||||
from orisys.finger.mesh import UVSurfaceMesh
|
||||
from orisys.finger.viewer import init_uv_mesh_viewer
|
||||
from orisys.hhd import nHHD
|
||||
from orisys.tracker import Tracker
|
||||
|
||||
assert orisys.__version__
|
||||
assert Sensor is not None
|
||||
assert callable(list_config)
|
||||
assert callable(export_config)
|
||||
assert util is not None
|
||||
assert finger is not None
|
||||
assert UVMeshViewer is not None
|
||||
assert build_uv_surface_mesh is not None
|
||||
assert FingerRuntime is not None
|
||||
assert UVSurfaceMesh is not None
|
||||
assert init_uv_mesh_viewer is not None
|
||||
assert nHHD is not None
|
||||
assert Tracker is not None
|
||||
print(f"orisys {orisys.__version__} public API imports OK")
|
||||
"""
|
||||
|
||||
|
||||
def _venv_python(venv_dir: Path) -> Path:
|
||||
if sys.platform == "win32":
|
||||
@@ -19,7 +45,7 @@ def _venv_python(venv_dir: Path) -> Path:
|
||||
|
||||
|
||||
def verify_built_wheel(wheel_path: Path) -> None:
|
||||
"""Install the wheel in a temporary venv and smoke-test finger imports."""
|
||||
"""Install the wheel in a temporary venv and smoke-test public imports."""
|
||||
wheel_path = wheel_path.resolve()
|
||||
with tempfile.TemporaryDirectory(prefix="orisys-wheel-verify-") as tmp:
|
||||
venv_dir = Path(tmp) / "venv"
|
||||
@@ -27,20 +53,12 @@ def verify_built_wheel(wheel_path: Path) -> None:
|
||||
python = _venv_python(venv_dir)
|
||||
|
||||
subprocess.check_call(
|
||||
[python, "-m", "pip", "install", "-q", str(wheel_path)],
|
||||
[python, "-m", "pip", "install", "-q", "--upgrade", "pip"],
|
||||
)
|
||||
subprocess.check_call(
|
||||
[
|
||||
python,
|
||||
"-c",
|
||||
(
|
||||
"from orisys.finger import UVMeshViewer, build_uv_surface_mesh, FingerRuntime; "
|
||||
"from orisys.finger.mesh import UVSurfaceMesh; "
|
||||
"from orisys.finger.viewer import init_uv_mesh_viewer; "
|
||||
"print('finger mesh/viewer imports OK')"
|
||||
),
|
||||
],
|
||||
[python, "-m", "pip", "install", "-q", str(wheel_path)],
|
||||
)
|
||||
subprocess.check_call([python, "-c", _SMOKE_IMPORT_SCRIPT])
|
||||
|
||||
|
||||
def verify_gui_dist(dist_dir: Path | None = None) -> None:
|
||||
|
||||
+74
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -9,6 +10,11 @@ from pathlib import Path
|
||||
|
||||
from 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,
|
||||
@@ -52,11 +58,78 @@ def run_bdist_wheel(wheel_dir: Path) -> Path | None:
|
||||
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.move(str(wheel_path), str(dest_wheel))
|
||||
shutil.copy2(str(wheel_path), str(dest_wheel))
|
||||
return dest_wheel
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,167 @@
|
||||
# Publishing Orisys to PyPI
|
||||
|
||||
This document covers the **Linux manylinux binary wheel** path (Nuitka-compiled
|
||||
`orisys` for CPython 3.10 / 3.11 / 3.12). Windows wheels are still built on
|
||||
Windows hosts; they cannot be produced inside the manylinux container.
|
||||
|
||||
## Why Docker?
|
||||
|
||||
PyPI rejects wheels tagged `manylinux_x86_64` (missing glibc ABI version). A
|
||||
compliant tag looks like:
|
||||
|
||||
```text
|
||||
orisys-0.4.2-cp310-cp310-manylinux_2_28_x86_64.whl
|
||||
orisys-0.4.2-cp311-cp311-manylinux_2_28_x86_64.whl
|
||||
orisys-0.4.2-cp312-cp312-manylinux_2_28_x86_64.whl
|
||||
```
|
||||
|
||||
Those tags are produced by building inside the official manylinux image and
|
||||
running `auditwheel repair`. On Windows, use **Docker Desktop in Linux container
|
||||
mode**.
|
||||
|
||||
> Note: we use `manylinux_2_28_x86_64` (not 2014). Recent NumPy/SciPy no longer
|
||||
> provide manylinux2014 wheels and fail to build from source on that older GCC.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Docker Desktop installed and running (**Linux containers**, not Windows containers).
|
||||
2. Repository checked out locally (this tree is bind-mounted into the container).
|
||||
3. Version bumped in `src/orisys/__init__.py` (`__version__`) before every upload.
|
||||
PyPI never allows re-uploading the same filename/version.
|
||||
4. PyPI / TestPyPI project-scoped API tokens (never commit tokens or `.pypirc` with secrets).
|
||||
|
||||
Quick Docker check:
|
||||
|
||||
```powershell
|
||||
docker version --format "{{.Server.Os}}"
|
||||
# expect: linux
|
||||
docker run --rm hello-world
|
||||
```
|
||||
|
||||
## Build manylinux wheels (3.10 / 3.11 / 3.12)
|
||||
|
||||
From the repository root. Prefer PowerShell, or from **cmd.exe**:
|
||||
|
||||
```bat
|
||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
.\build\build_manylinux_docker.ps1
|
||||
```
|
||||
|
||||
Default builds **all three** ABIs. To build a subset:
|
||||
|
||||
```powershell
|
||||
.\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.12
|
||||
.\build\build_manylinux_docker.ps1 -PythonVersions 3.11
|
||||
```
|
||||
|
||||
What this does:
|
||||
|
||||
1. Builds `orisys-manylinux` from [`build/Dockerfile.manylinux`](../build/Dockerfile.manylinux)
|
||||
(`quay.io/pypa/manylinux_2_28_x86_64` with Nuitka/auditwheel in cp310/cp311/cp312).
|
||||
2. Runs [`build/manylinux_entrypoint.sh`](../build/manylinux_entrypoint.sh), which loops:
|
||||
|
||||
```bash
|
||||
/opt/python/cp310-cp310/bin/python build/build_nuitka.py --target pypi-linux
|
||||
/opt/python/cp311-cp311/bin/python build/build_nuitka.py --target pypi-linux
|
||||
/opt/python/cp312-cp312/bin/python build/build_nuitka.py --target pypi-linux
|
||||
```
|
||||
|
||||
3. For each ABI: Nuitka compile → wheel → `auditwheel repair` → isolated smoke test.
|
||||
4. Artifacts land in:
|
||||
|
||||
- `dist/repaired/orisys-*-manylinux_2_28_x86_64.whl` (upload candidates)
|
||||
- `SDK/<version>/` (same compliant wheels)
|
||||
|
||||
Expect roughly **3×** the wall time of a single-ABI build.
|
||||
|
||||
Rebuild without refreshing the image:
|
||||
|
||||
```powershell
|
||||
.\build\build_manylinux_docker.ps1 -SkipImageBuild
|
||||
```
|
||||
|
||||
Equivalent manual `docker` invocation:
|
||||
|
||||
```powershell
|
||||
docker build -f build/Dockerfile.manylinux -t orisys-manylinux .
|
||||
docker run --rm `
|
||||
-e ORISYS_BUILD_TARGET=pypi-linux `
|
||||
-e ORISYS_PYTHON_TAGS=cp310-cp310,cp311-cp311,cp312-cp312 `
|
||||
-v "${PWD}:/src" `
|
||||
orisys-manylinux
|
||||
```
|
||||
|
||||
### Acceptance checks before upload
|
||||
|
||||
- Each filename contains `cp310` / `cp311` / `cp312` and `manylinux_2_28_x86_64`
|
||||
(or another accepted manylinux tag such as `manylinux_2_17_x86_64`).
|
||||
- Isolated smoke test printed `public API imports OK` for every ABI.
|
||||
- Do **not** upload any wheel still tagged `linux_x86_64` or bare `manylinux_x86_64`.
|
||||
|
||||
Optional re-verify on a Linux machine:
|
||||
|
||||
```bash
|
||||
python build/verify.py dist/repaired/orisys-*-cp310-cp310-manylinux_*.whl
|
||||
```
|
||||
|
||||
## Promote: TestPyPI then PyPI
|
||||
|
||||
Install upload tooling in a clean environment:
|
||||
|
||||
```powershell
|
||||
python -m pip install --upgrade twine
|
||||
```
|
||||
|
||||
### 1. Upload to TestPyPI
|
||||
|
||||
```powershell
|
||||
$env:TWINE_USERNAME = "__token__"
|
||||
$env:TWINE_PASSWORD = "<TestPyPI API token>"
|
||||
|
||||
python -m twine check dist/repaired/*.whl
|
||||
python -m twine upload --repository testpypi dist/repaired/*.whl
|
||||
```
|
||||
|
||||
Install from TestPyPI in a fresh venv (pull runtime deps from real PyPI), testing
|
||||
each Python you care about:
|
||||
|
||||
```powershell
|
||||
py -3.10 -m venv .venv-testpypi310
|
||||
.\.venv-testpypi310\Scripts\Activate.ps1
|
||||
python -m pip install --index-url https://test.pypi.org/simple/ `
|
||||
--extra-index-url https://pypi.org/simple `
|
||||
"orisys==<version>"
|
||||
python -c "import orisys; print(orisys.__version__)"
|
||||
```
|
||||
|
||||
### 2. Upload the same artifacts to PyPI
|
||||
|
||||
Only after TestPyPI install/import succeeds:
|
||||
|
||||
```powershell
|
||||
$env:TWINE_USERNAME = "__token__"
|
||||
$env:TWINE_PASSWORD = "<PyPI API token>"
|
||||
|
||||
python -m twine upload dist/repaired/*.whl
|
||||
```
|
||||
|
||||
Prefer CI **Trusted Publishing** (OIDC) for production uploads so long-lived tokens
|
||||
are not stored on developer machines.
|
||||
|
||||
## Related local targets
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `python build/build_nuitka.py` | Internal SDK wheel on the current host (Windows `.pyd` or Linux `.so`) |
|
||||
| `python build/build_nuitka.py --target pypi-linux` | Linux-only; requires manylinux + CPython 3.10–3.12 |
|
||||
| `.\build\build_nuitka_win.bat` | Windows wheels for py310/py311/py312 conda envs |
|
||||
| `.\build\build_manylinux_docker.ps1` | Linux manylinux wheels for 3.10/3.11/3.12 via Docker |
|
||||
| `python -m build` | Pure-Python / sdist path from `pyproject.toml` (no Nuitka) |
|
||||
|
||||
Windows binary wheels and Linux manylinux wheels must be built on their respective
|
||||
platforms. Never rename a platform tag to bypass PyPI validation.
|
||||
@@ -72,11 +72,11 @@ def main():
|
||||
grid_spacing=20,
|
||||
arrow_scale=1.0
|
||||
)
|
||||
cv2.imshow("形变矢量场", arrows)
|
||||
cv2.imshow("arrow", arrows)
|
||||
|
||||
# 步骤 3.2:显示接触区域和质心
|
||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
cv2.imshow("接触区域与质心", image_with_foe)
|
||||
cv2.imshow("contact", image_with_foe)
|
||||
|
||||
# 步骤 3.3:显示深度图
|
||||
# 将深度值归一化到 0-255 范围
|
||||
@@ -87,7 +87,7 @@ def main():
|
||||
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
||||
|
||||
# 应用伪彩色映射
|
||||
cv2.imshow("深度图", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
cv2.imshow("depth", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
|
||||
# 步骤 3.4:打印关键结果
|
||||
print(
|
||||
|
||||
@@ -72,11 +72,11 @@ def main():
|
||||
grid_spacing=20,
|
||||
arrow_scale=1.0
|
||||
)
|
||||
cv2.imshow("形变矢量场", arrows)
|
||||
cv2.imshow("arrows", arrows)
|
||||
|
||||
# 步骤 3.2:显示接触区域和质心
|
||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
cv2.imshow("接触区域与质心", image_with_foe)
|
||||
cv2.imshow("contact", image_with_foe)
|
||||
|
||||
# 步骤 3.3:显示深度图
|
||||
# 将深度值归一化到 0-255 范围
|
||||
@@ -87,7 +87,7 @@ def main():
|
||||
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
||||
|
||||
# 应用伪彩色映射
|
||||
cv2.imshow("深度图", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
cv2.imshow("depth", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
|
||||
# 步骤 3.4:打印关键结果
|
||||
print(
|
||||
|
||||
+10
-2
@@ -7,6 +7,7 @@ name = "orisys"
|
||||
description = "SDK for Orisys Technology Visuotactile Sensors"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = { text = "Proprietary" }
|
||||
authors = [{ name = "Orisys-Tech" }]
|
||||
maintainers = [
|
||||
{name = "Yijian Zhang", email = "zhangyijian@orisys-tech.cn"}
|
||||
@@ -27,15 +28,22 @@ classifiers = [
|
||||
|
||||
# Indicate who your project is intended for
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: SDL",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"License :: Other/Proprietary License",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
|
||||
# Specify the Python versions you support here.
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Private :: Do Not Upload",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://www.orisys-tech.cn"
|
||||
Documentation = "https://github.com/Orisys-Tech/orisys-sdk"
|
||||
Issues = "https://github.com/Orisys-Tech/orisys-sdk/issues"
|
||||
|
||||
[project.optional-dependencies]
|
||||
cuda12 = ["cupy-cuda12x>=12.3.0"]
|
||||
cuda13 = ["cupy-cuda13x>=13.0.0"]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wheel_build():
|
||||
import sys
|
||||
|
||||
build_dir = REPO_ROOT / "build"
|
||||
if str(build_dir) not in sys.path:
|
||||
sys.path.insert(0, str(build_dir))
|
||||
import wheel_build
|
||||
|
||||
return wheel_build
|
||||
|
||||
|
||||
def test_compliant_manylinux_tags(wheel_build):
|
||||
assert wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-manylinux_2_17_x86_64.whl")
|
||||
)
|
||||
assert wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-manylinux2014_x86_64.whl")
|
||||
)
|
||||
assert wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-manylinux_2_28_x86_64.whl")
|
||||
)
|
||||
|
||||
|
||||
def test_rejects_illegal_manylinux_tags(wheel_build):
|
||||
assert not wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-manylinux_x86_64.whl")
|
||||
)
|
||||
assert not wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-linux_x86_64.whl")
|
||||
)
|
||||
assert not wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-win_amd64.whl")
|
||||
)
|
||||
|
||||
|
||||
def test_pyproject_no_private_classifier():
|
||||
import sys
|
||||
|
||||
build_dir = REPO_ROOT / "build"
|
||||
if str(build_dir) not in sys.path:
|
||||
sys.path.insert(0, str(build_dir))
|
||||
from pyproject_utils import project_metadata
|
||||
|
||||
classifiers = project_metadata(REPO_ROOT).get("classifiers", [])
|
||||
assert "Private :: Do Not Upload" not in classifiers
|
||||
assert "License :: Other/Proprietary License" in classifiers
|
||||
Reference in New Issue
Block a user