finished docker manylinux compliation for pypi

This commit is contained in:
ykcheungab
2026-08-03 17:07:54 +08:00
parent 3117ddd5b9
commit d7c5845646
31 changed files with 709 additions and 52 deletions
+13
View File
@@ -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.
+26
View File
@@ -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"]
+2
View File
@@ -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
+104
View File
@@ -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
View File
@@ -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()
+67
View File
@@ -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."
+7
View File
@@ -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",
)
+5
View File
@@ -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"
+20 -8
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
import importlib
import ast
import subprocess
import sys
from pathlib import Path
@@ -41,18 +41,30 @@ 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)
# 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)."""
+30 -12
View File
@@ -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
View File
@@ -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
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+167
View File
@@ -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.
+3 -3
View File
@@ -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(
+3 -3
View File
@@ -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
View File
@@ -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"]
+54
View File
@@ -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