diff --git a/.gitignore b/.gitignore index 0db79d4..dd9df22 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,12 @@ __pycache__/ # Packaging / build outputs /build/nuitka/ -/build/lib/orisys/ +/build/build/ +/build/bdist.*/ +/build/dist/ +/build/lib/ /build/pyinstaller_work/ +/build/*.spec /dist/ /dist_obfuscated/ /dist_pyd/ diff --git a/README.md b/README.md index 5e781a1..1a9f5ae 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Orisys SDK is a Python toolkit for optical tactile sensing. It provides image ca stitching, dense optical flow, Helmholtz-Hodge decomposition, contact/depth estimation, and several runnable examples for camera and video-based workflows. +PyPI: [https://pypi.org/project/orisys/](https://pypi.org/project/orisys/) + ## Repository Layout - `src/orisys/`: installable SDK package @@ -93,9 +95,8 @@ 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/` +- Build launcher: `python build/run.py` (see `build/README.md`) +- Manylinux / PyPI binary wheel: `docs/publishing.md` - Specialized utilities: `tools/` ## Publishing (Linux manylinux) @@ -103,7 +104,7 @@ cfg = orisys.export_config("ddjx01", "./config/exported_config.json") 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 +python build/run.py manylinux ``` Upload candidates land in `dist/repaired/`. Full TestPyPI → PyPI steps: diff --git a/build/OrisysDashboard.spec b/build/OrisysDashboard.spec deleted file mode 100644 index a4add95..0000000 --- a/build/OrisysDashboard.spec +++ /dev/null @@ -1,44 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -a = Analysis( - ['D:\\repo\\orisys-sdk\\examples\\finger_pipeline_3dview.py'], - pathex=[], - binaries=[], - datas=[('D:\\repo\\orisys-sdk\\config', 'config'), ('D:\\repo\\orisys-sdk\\assets', 'assets')], - hiddenimports=['PyQt5', 'pyqtgraph', 'open3d', 'cv2', 'orisys', 'gui', 'pipeline', 'viz', 'gui.app_paths', 'plotly', '_plotly_utils', 'open3d.cpu.pybind'], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=['cupy', 'cupyx', 'pyopencl', 'tkinter', 'pip', 'wheel', 'matplotlib'], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='OrisysDashboard', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) -coll = COLLECT( - exe, - a.binaries, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='OrisysDashboard', -) diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..fce906d --- /dev/null +++ b/build/README.md @@ -0,0 +1,65 @@ +# Build tooling + +Use one launcher from the repo root: + +```bash +python build/run.py +``` + +```text +build/ + run.py << single entrypoint + README.md + common/ shared helpers (paths, pyproject, verify) + wheel/ SDK .whl (Nuitka + manylinux Docker) + gui/ + common/ shared PyInstaller driver + ory_finger/ ORY-FINGER dashboard + ory_mini/ ORY-MINI internal GUI (scaffold) + ory_box/ ORY-BOX internal GUI (scaffold) +``` + +## Commands + +### SDK wheel + +```bash +python build/run.py wheel-win +python build/run.py wheel-manylinux + +# Single Version +python build/run.py wheel +# Inside Docker +python build/run.py wheel --target pypi-linux --verify +``` + +Direct module paths (same behavior): + +```bash +python build/wheel/build.py +build\wheel\win.bat +.\build\wheel\docker\run.ps1 +``` + +### GUI + +```bash +python build/run.py gui finger +python build/run.py gui mini # scaffold / NotImplemented +python build/run.py gui box # scaffold / NotImplemented +``` + +### Verify + +```bash +python build/run.py verify path\to\orisys.whl +python build/run.py verify --gui-dist --product ory_finger +``` + +## Outputs + +- Wheels: `SDK//`, `dist/repaired/` +- Finger GUI: `dist/OrisysDashboard/` +- Future mini/box: `dist/OryMini/`, `dist/OryBox/` + +Generated work files under `build/nuitka/` and `build/pyinstaller_work/` are gitignored. diff --git a/build/build_docker.txt b/build/build_docker.txt deleted file mode 100644 index c69f9ef..0000000 --- a/build/build_docker.txt +++ /dev/null @@ -1,2 +0,0 @@ -powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1 -powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.12 \ No newline at end of file diff --git a/build/build_gui_win_pyinstaller.py b/build/build_gui_win_pyinstaller.py deleted file mode 100644 index cc53708..0000000 --- a/build/build_gui_win_pyinstaller.py +++ /dev/null @@ -1,194 +0,0 @@ -"""Build a CPU-only standalone Windows folder for Orisys 3D dashboard using PyInstaller. - -Usage: - conda activate py310 # or a clean virtualenv with only required packages - pip install pyinstaller - python build/build_gui_win_pyinstaller.py - -Output: - dist/OrisysDashboard/OrisysDashboard.exe (+ bundled Python, DLLs, config/, assets/) -""" - -from __future__ import annotations - -import os -import shutil -import subprocess -import sys -from pathlib import Path - -# ---------- Configuration ---------- -ENTRY_SCRIPT = Path("examples/products/ory_finger/finger_pipeline_3dview.py").resolve() -EXE_NAME = "OrisysDashboard.exe" -DIST_NAME = "OrisysDashboard" # final folder name -repo_root = Path(__file__).parent.parent - -# Packages you want to force-include (PyInstaller often auto-detects them, but explicit avoids misses) -HIDDEN_IMPORTS = [ - "PyQt5", - "pyqtgraph", - "open3d", - "cv2", - "orisys", - "gui", - "pipeline", - "viz", - "gui.app_paths", - "plotly", - "_plotly_utils", - # sometimes needed for open3d: - "open3d.cpu.pybind", # adjust to actual module if needed -] - -# Modules to completely exclude (they won't be imported anyway if absent in venv) -EXCLUDE_MODULES = [ - "cupy", - "cupyx", - "pyopencl", - "tkinter", - # "unittest", - # "email", - # "http", - # "xmlrpc", - # "pkg_resources", - # "setuptools", - "pip", - "wheel", - "matplotlib", # unless you use it - # "scipy", # unless you use it -] - -# Data directories: (source relative to repo root, destination in bundle) -DATA_DIRS = [ - ("config", "config"), - ("assets", "assets"), -] - -# Optional: UPX directory (download from https://upx.github.io) -UPX_DIR = r"C:\tools\upx" # set to None if not available -# ---------- End Configuration ---------- - - -def clean_dist(dist_path: Path) -> None: - if dist_path.exists(): - shutil.rmtree(dist_path) - dist_path.mkdir(parents=True) - - -def build_with_pyinstaller(): - # Ensure we are in a clean, minimal venv - print("Using Python:", sys.executable) - - # Output folder for PyInstaller work files - work_dir = Path("build/pyinstaller_work") - dist_dir = Path("dist") / DIST_NAME - clean_dist(work_dir) - clean_dist(dist_dir) - - # Build PyInstaller command - cmd = [ - sys.executable, "-m", "PyInstaller", - "--onedir", # produce a folder, not a single exe - "--name", DIST_NAME, - f"--distpath={dist_dir.parent}", - f"--workpath={work_dir}", - f"--specpath=build", - "--noconfirm", - "--clean", - "--windowed", # or "--console" if you need terminal - ] - - # Add data files - for src_rel, dest_rel in DATA_DIRS: - src = Path(src_rel).resolve() - if not src.is_dir(): - raise FileNotFoundError(f"Data directory missing: {src}") - cmd.append(f"--add-data={src};{dest_rel}") - - # Logo file if exists - logo = Path("logo.png") - if logo.exists(): - cmd.append(f"--add-data={logo.resolve()};.") - - # Hidden imports - for mod in HIDDEN_IMPORTS: - cmd.append(f"--hidden-import={mod}") - - # Exclude modules - for mod in EXCLUDE_MODULES: - cmd.append(f"--exclude-module={mod}") - - # UPX if available - if UPX_DIR and Path(UPX_DIR).is_dir(): - cmd.append(f"--upx-dir={UPX_DIR}") - - # Entry point - cmd.append(str(ENTRY_SCRIPT)) - - # Environment (same as Nuitka script) - env = os.environ.copy() - env["PYTHONPATH"] = str(Path("src").resolve()) + os.pathsep + str(Path("examples").resolve()) + os.pathsep + env.get("PYTHONPATH", "") - - print("\n=== Running PyInstaller ===") - print("Command:", " ".join(cmd)) - subprocess.check_call(cmd, env=env) - - # After build, copy data directories explicitly - # PyInstaller output folder - dist_dir = Path("dist") / DIST_NAME - final_exe = dist_dir / EXE_NAME - if not final_exe.is_file(): - raise RuntimeError(f"Executable not found at {final_exe}") - print("\n=== Copying data directories ===") - for src_rel, dest_rel in DATA_DIRS: - src = repo_root / src_rel # use an absolute reference to repo root - dest = dist_dir / dest_rel - if not src.is_dir(): - raise FileNotFoundError(f"Data directory not found: {src}") - if dest.exists(): - shutil.rmtree(dest) - shutil.copytree(src, dest, dirs_exist_ok=True) # Python 3.8+ use dirs_exist_ok=True - print(f" {src} -> {dest}") - - # Copy logo - logo_src = repo_root / "logo.png" - if logo_src.is_file(): - shutil.copy2(logo_src, dist_dir / "logo.png") - print(f" Logo copied") - - # Write README (same as Nuitka script) - readme = dist_dir / "README.txt" - readme.write_text( - "\n".join([ - "Orisys Finger Dashboard (standalone)", - "", - "Run:", - f" {EXE_NAME}", - f" {EXE_NAME} --video path\\to\\video.mp4", - f" {EXE_NAME} --config config\\dd02-ov.json", - "", - "Bundled folders:", - " config/ sensor JSON configs", - " assets/ finger mesh and visualization data", - "", - "CPU-only build (no CUDA/CuPy).", - ]), - encoding="utf-8", - ) - - print(f"\n[SUCCESS] Standalone GUI folder: {dist_dir}") - print(f"Executable: {final_exe}") - - # Optional smoke test (same as Nuitka script) - try: - from verify import verify_gui_dist - print("\n=== Smoke test ===") - verify_gui_dist(dist_dir) - except Exception as exc: - print(f"\n[WARNING] Smoke test failed: {exc}") - - return dist_dir - - -if __name__ == "__main__": - build_with_pyinstaller() \ No newline at end of file diff --git a/build/common/__init__.py b/build/common/__init__.py new file mode 100644 index 0000000..0404422 --- /dev/null +++ b/build/common/__init__.py @@ -0,0 +1 @@ +"""Shared build utilities used by wheel and GUI packaging targets.""" diff --git a/build/common/bootstrap.py b/build/common/bootstrap.py new file mode 100644 index 0000000..7606ab0 --- /dev/null +++ b/build/common/bootstrap.py @@ -0,0 +1,15 @@ +"""Ensure ``build/`` is importable as the root for ``common``, ``wheel``, and ``gui``.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parents[1] + + +def ensure_build_root_on_path() -> Path: + root = str(_BUILD_ROOT) + if root not in sys.path: + sys.path.insert(0, root) + return _BUILD_ROOT diff --git a/build/paths.py b/build/common/paths.py similarity index 67% rename from build/paths.py rename to build/common/paths.py index 3fa8cfd..d5163d5 100644 --- a/build/paths.py +++ b/build/common/paths.py @@ -1,15 +1,17 @@ -"""Repository and Nuitka build output paths.""" +"""Repository and packaging output paths.""" from __future__ import annotations from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parent.parent +# build/common/paths.py → parents: common, build, repo +REPO_ROOT = Path(__file__).resolve().parents[2] NUITKA_COMPILE_DIR = REPO_ROOT / "build" / "nuitka" / "compile" NUITKA_WHEEL_DIR = REPO_ROOT / "build" / "nuitka" / "wheel" NUITKA_GUI_DIR = REPO_ROOT / "build" / "nuitka" / "gui" GUI_DIST_DIR = REPO_ROOT / "dist" / "OrisysDashboard" +PYINSTALLER_WORK_DIR = REPO_ROOT / "build" / "pyinstaller_work" def repo_path(*parts: str) -> Path: @@ -23,3 +25,7 @@ def sdk_dir(version: str) -> Path: def repaired_wheel_dir() -> Path: """Directory for auditwheel-repaired wheels under the Nuitka wheel build tree.""" return NUITKA_WHEEL_DIR / "repaired" + + +def gui_dist_dir(dist_name: str) -> Path: + return REPO_ROOT / "dist" / dist_name diff --git a/build/pyproject_utils.py b/build/common/pyproject_utils.py similarity index 95% rename from build/pyproject_utils.py rename to build/common/pyproject_utils.py index 33aa629..9256cde 100644 --- a/build/pyproject_utils.py +++ b/build/common/pyproject_utils.py @@ -7,6 +7,10 @@ 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)) + def _load_toml(text: str) -> dict: if sys.version_info >= (3, 11): @@ -90,7 +94,7 @@ def bundle_items(repo_root: Path) -> list[dict]: def bundle_data_files(repo_root: Path) -> list[tuple[str, list[str]]]: """Map bundle items marked dev_install=true to setuptools data_files entries.""" - from wheel_layout import resolve_bundle_source_files + from wheel.layout import resolve_bundle_source_files grouped: dict[str, list[str]] = {} for item in bundle_items(repo_root): diff --git a/build/common/verify/__init__.py b/build/common/verify/__init__.py new file mode 100644 index 0000000..f9d7fa5 --- /dev/null +++ b/build/common/verify/__init__.py @@ -0,0 +1,6 @@ +"""Post-build smoke tests for wheels and product GUI folders.""" + +from common.verify.gui import GUI_PRODUCTS, verify_gui_dist +from common.verify.wheel import verify_built_wheel + +__all__ = ["GUI_PRODUCTS", "verify_built_wheel", "verify_gui_dist"] diff --git a/build/common/verify/cli.py b/build/common/verify/cli.py new file mode 100644 index 0000000..acf7f29 --- /dev/null +++ b/build/common/verify/cli.py @@ -0,0 +1,34 @@ +"""CLI for packaged artifact verification.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from common.verify import GUI_PRODUCTS, verify_built_wheel, verify_gui_dist + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Verify packaged Orisys artifacts.") + parser.add_argument("--gui-dist", action="store_true", help="Verify a GUI dist folder") + parser.add_argument( + "--product", + choices=sorted(GUI_PRODUCTS), + default="ory_finger", + help="GUI product profile (default: ory_finger)", + ) + parser.add_argument("--dist-dir", type=Path, default=None, help="Override GUI dist folder") + parser.add_argument("wheel", nargs="?", type=Path, help="Wheel path for import smoke test") + args = parser.parse_args(argv) + + if args.gui_dist: + verify_gui_dist(args.dist_dir, product=args.product) + return 0 + if args.wheel is None: + parser.error("Provide a wheel path or pass --gui-dist") + verify_built_wheel(args.wheel) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/build/common/verify/gui.py b/build/common/verify/gui.py new file mode 100644 index 0000000..491a0a9 --- /dev/null +++ b/build/common/verify/gui.py @@ -0,0 +1,67 @@ +"""Smoke tests for standalone GUI product folders.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parents[2] +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + +from common.paths import GUI_DIST_DIR, gui_dist_dir + +# Product profiles for GUI packaging verification. +GUI_PRODUCTS: dict[str, dict] = { + "ory_finger": { + "dist_name": "OrisysDashboard", + "exe_name": "OrisysDashboard.exe", + "required_relpaths": [ + "config/dd02-ov.json", + "assets/finger/30fps-0703/mesh.npz", + "assets/finger/30fps-0703/world_to_cam.npy", + ], + }, + "ory_mini": { + "dist_name": "OryMini", + "exe_name": "OryMini.exe", + "required_relpaths": [ + "config/mini.json", + ], + }, + "ory_box": { + "dist_name": "OryBox", + "exe_name": "OryBox.exe", + "required_relpaths": [ + "config/ddjx01.json", + ], + }, +} + + +def verify_gui_dist( + dist_dir: Path | None = None, + *, + product: str = "ory_finger", +) -> None: + """Smoke-test a standalone GUI folder for the given product.""" + if product not in GUI_PRODUCTS: + raise ValueError(f"Unknown GUI product {product!r}; choose from {sorted(GUI_PRODUCTS)}") + + profile = GUI_PRODUCTS[product] + if dist_dir is None: + dist_dir = gui_dist_dir(profile["dist_name"]) if product != "ory_finger" else GUI_DIST_DIR + dist_dir = dist_dir.resolve() + + exe = dist_dir / profile["exe_name"] + if not exe.is_file(): + raise FileNotFoundError(f"GUI executable not found: {exe}") + + required = [dist_dir / rel for rel in profile["required_relpaths"]] + missing = [str(path.relative_to(dist_dir)) for path in required if not path.is_file()] + if missing: + raise FileNotFoundError(f"Missing bundled files in {dist_dir}: {', '.join(missing)}") + + subprocess.check_call([str(exe), "--help"], cwd=dist_dir) + print(f"GUI dist smoke test OK ({product}): {exe}") diff --git a/build/verify.py b/build/common/verify/wheel.py similarity index 52% rename from build/verify.py rename to build/common/verify/wheel.py index 6dcd23b..1891bf5 100644 --- a/build/verify.py +++ b/build/common/verify/wheel.py @@ -1,16 +1,13 @@ -"""Post-build smoke tests for packaged wheels and standalone GUI builds.""" +"""Isolated venv smoke tests for packaged orisys wheels.""" from __future__ import annotations -import argparse import subprocess import sys import tempfile import venv 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 @@ -59,42 +56,3 @@ def verify_built_wheel(wheel_path: Path) -> None: [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: - """Smoke-test a standalone GUI folder produced by build_gui_win.py.""" - dist_dir = (dist_dir or GUI_DIST_DIR).resolve() - exe = dist_dir / "OrisysDashboard.exe" - if not exe.is_file(): - raise FileNotFoundError(f"GUI executable not found: {exe}") - - required = [ - dist_dir / "config" / "dd02-ov.json", - dist_dir / "assets" / "finger" / "visualization" / "world_to_cam.npy", - dist_dir / "assets" / "finger" / "30fps-0703" / "mesh.npz", - ] - missing = [str(path.relative_to(dist_dir)) for path in required if not path.is_file()] - if missing: - raise FileNotFoundError(f"Missing bundled files in {dist_dir}: {', '.join(missing)}") - - subprocess.check_call([str(exe), "--help"], cwd=dist_dir) - print(f"GUI dist smoke test OK: {exe}") - - -def main() -> None: - parser = argparse.ArgumentParser(description="Verify packaged Orisys artifacts.") - parser.add_argument("--gui-dist", action="store_true", help="Verify dist/OrisysDashboard/") - parser.add_argument("--dist-dir", type=Path, default=None, help="Override GUI dist folder") - parser.add_argument("wheel", nargs="?", type=Path, help="Wheel path for import smoke test") - args = parser.parse_args() - - if args.gui_dist: - verify_gui_dist(args.dist_dir) - return - if args.wheel is None: - parser.error("Provide a wheel path or pass --gui-dist") - verify_built_wheel(args.wheel) - - -if __name__ == "__main__": - main() diff --git a/build/gui/__init__.py b/build/gui/__init__.py new file mode 100644 index 0000000..a293b27 --- /dev/null +++ b/build/gui/__init__.py @@ -0,0 +1 @@ +"""Standalone GUI packaging targets (PyInstaller).""" diff --git a/build/gui/common/__init__.py b/build/gui/common/__init__.py new file mode 100644 index 0000000..4c9ff42 --- /dev/null +++ b/build/gui/common/__init__.py @@ -0,0 +1 @@ +"""Shared helpers for product GUI builds.""" diff --git a/build/gui/common/pyinstaller_driver.py b/build/gui/common/pyinstaller_driver.py new file mode 100644 index 0000000..ac8dddd --- /dev/null +++ b/build/gui/common/pyinstaller_driver.py @@ -0,0 +1,141 @@ +"""Shared PyInstaller driver for product GUI standalone folders.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Callable + +_BUILD_ROOT = Path(__file__).resolve().parents[2] +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + +from common.paths import PYINSTALLER_WORK_DIR, REPO_ROOT, gui_dist_dir + + +@dataclass +class GuiBuildConfig: + product: str + entry_script: Path + exe_name: str + dist_name: str + hidden_imports: list[str] = field(default_factory=list) + exclude_modules: list[str] = field(default_factory=list) + data_dirs: list[tuple[str, str]] = field(default_factory=list) + readme_lines: list[str] = field(default_factory=list) + windowed: bool = True + upx_dir: str | None = r"C:\tools\upx" + logo_name: str = "logo.png" + work_dir: Path = PYINSTALLER_WORK_DIR + spec_dir: Path = REPO_ROOT / "build" / "pyinstaller_work" + + +def clean_dir(path: Path) -> None: + if path.exists(): + shutil.rmtree(path) + path.mkdir(parents=True) + + +def build_with_pyinstaller( + config: GuiBuildConfig, + *, + verify_fn: Callable[[Path], None] | None = None, +) -> Path: + """Run PyInstaller for one GUI product and copy bundled data folders.""" + print("Using Python:", sys.executable) + print(f"Building GUI product: {config.product}") + + dist_dir = gui_dist_dir(config.dist_name) + clean_dir(config.work_dir) + clean_dir(dist_dir) + config.spec_dir.mkdir(parents=True, exist_ok=True) + + entry = config.entry_script + if not entry.is_file(): + raise FileNotFoundError(f"GUI entry script not found: {entry}") + + cmd = [ + sys.executable, + "-m", + "PyInstaller", + "--onedir", + "--name", + config.dist_name, + f"--distpath={dist_dir.parent}", + f"--workpath={config.work_dir}", + f"--specpath={config.spec_dir}", + "--noconfirm", + "--clean", + "--windowed" if config.windowed else "--console", + ] + + for src_rel, dest_rel in config.data_dirs: + src = (REPO_ROOT / src_rel).resolve() + if not src.is_dir(): + raise FileNotFoundError(f"Data directory missing: {src}") + cmd.append(f"--add-data={src};{dest_rel}") + + logo = REPO_ROOT / config.logo_name + if logo.is_file(): + cmd.append(f"--add-data={logo.resolve()};.") + + for mod in config.hidden_imports: + cmd.append(f"--hidden-import={mod}") + for mod in config.exclude_modules: + cmd.append(f"--exclude-module={mod}") + + if config.upx_dir and Path(config.upx_dir).is_dir(): + cmd.append(f"--upx-dir={config.upx_dir}") + + cmd.append(str(entry.resolve())) + + env = os.environ.copy() + env["PYTHONPATH"] = ( + str((REPO_ROOT / "src").resolve()) + + os.pathsep + + str((REPO_ROOT / "examples").resolve()) + + os.pathsep + + env.get("PYTHONPATH", "") + ) + + print("\n=== Running PyInstaller ===") + print("Command:", " ".join(cmd)) + subprocess.check_call(cmd, env=env, cwd=REPO_ROOT) + + final_exe = dist_dir / config.exe_name + if not final_exe.is_file(): + raise RuntimeError(f"Executable not found at {final_exe}") + + print("\n=== Copying data directories ===") + for src_rel, dest_rel in config.data_dirs: + src = REPO_ROOT / src_rel + dest = dist_dir / dest_rel + if not src.is_dir(): + raise FileNotFoundError(f"Data directory not found: {src}") + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(src, dest, dirs_exist_ok=True) + print(f" {src} -> {dest}") + + if logo.is_file(): + shutil.copy2(logo, dist_dir / config.logo_name) + print(" Logo copied") + + if config.readme_lines: + (dist_dir / "README.txt").write_text("\n".join(config.readme_lines) + "\n", encoding="utf-8") + + print(f"\n[SUCCESS] Standalone GUI folder: {dist_dir}") + print(f"Executable: {final_exe}") + + if verify_fn is not None: + try: + print("\n=== Smoke test ===") + verify_fn(dist_dir) + except Exception as exc: + print(f"\n[WARNING] Smoke test failed: {exc}") + + return dist_dir diff --git a/build/gui/ory_box/__init__.py b/build/gui/ory_box/__init__.py new file mode 100644 index 0000000..ed836af --- /dev/null +++ b/build/gui/ory_box/__init__.py @@ -0,0 +1 @@ +"""ORY-BOX internal calibration/testing GUI packaging (scaffold).""" diff --git a/build/gui/ory_box/build.py b/build/gui/ory_box/build.py new file mode 100644 index 0000000..3b1233f --- /dev/null +++ b/build/gui/ory_box/build.py @@ -0,0 +1,30 @@ +"""Scaffold for ORY-BOX internal GUI packaging. + +Not implemented yet. When ready, wire a dedicated calibration/testing entry +script and call ``gui.common.pyinstaller_driver.build_with_pyinstaller``. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parents[2] +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + +from gui.ory_box.config import DIST_NAME, ENTRY_SCRIPT, make_config + + +def main() -> None: + config = make_config() + raise NotImplementedError( + "ORY-BOX GUI packaging is a scaffold only. " + f"Planned dist={DIST_NAME!r}, entry={ENTRY_SCRIPT}. " + "Implement the internal calibration/testing UI entry, then call " + "gui.common.pyinstaller_driver.build_with_pyinstaller(config)." + ) + + +if __name__ == "__main__": + main() diff --git a/build/gui/ory_box/config.py b/build/gui/ory_box/config.py new file mode 100644 index 0000000..10e07fc --- /dev/null +++ b/build/gui/ory_box/config.py @@ -0,0 +1,63 @@ +"""Product configuration scaffold for the ORY-BOX internal GUI.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parents[2] +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + +from common.paths import REPO_ROOT +from gui.common.pyinstaller_driver import GuiBuildConfig + +ENTRY_SCRIPT = REPO_ROOT / "examples" / "products" / "ory_box" / "basic_pipeline.py" +EXE_NAME = "OryBox.exe" +DIST_NAME = "OryBox" + +HIDDEN_IMPORTS = [ + "PyQt5", + "pyqtgraph", + "cv2", + "orisys", + "gui", + "pipeline", + "viz", +] + +EXCLUDE_MODULES = [ + "cupy", + "cupyx", + "pyopencl", + "open3d", + "tkinter", + "pip", + "wheel", +] + +DATA_DIRS = [ + ("config", "config"), +] + +README_LINES = [ + "ORY-BOX internal calibration/testing GUI (scaffold)", + "", + "This product packaging target is reserved for a future internal tool.", + "Entry point placeholder:", + f" {ENTRY_SCRIPT.as_posix()}", +] + + +def make_config() -> GuiBuildConfig: + return GuiBuildConfig( + product="ory_box", + entry_script=ENTRY_SCRIPT, + exe_name=EXE_NAME, + dist_name=DIST_NAME, + hidden_imports=list(HIDDEN_IMPORTS), + exclude_modules=list(EXCLUDE_MODULES), + data_dirs=list(DATA_DIRS), + readme_lines=list(README_LINES), + windowed=False, + ) diff --git a/build/gui/ory_finger/__init__.py b/build/gui/ory_finger/__init__.py new file mode 100644 index 0000000..8061dac --- /dev/null +++ b/build/gui/ory_finger/__init__.py @@ -0,0 +1 @@ +"""ORY-FINGER standalone GUI packaging.""" diff --git a/build/gui/ory_finger/build.py b/build/gui/ory_finger/build.py new file mode 100644 index 0000000..46c32fc --- /dev/null +++ b/build/gui/ory_finger/build.py @@ -0,0 +1,31 @@ +"""Build the ORY-FINGER standalone Windows GUI folder with PyInstaller. + +Usage: + python build/run.py gui finger + python build/gui/ory_finger/build.py +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parents[2] +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + +from common.verify.gui import verify_gui_dist +from gui.common.pyinstaller_driver import build_with_pyinstaller +from gui.ory_finger.config import make_config + + +def main() -> None: + config = make_config() + build_with_pyinstaller( + config, + verify_fn=lambda dist_dir: verify_gui_dist(dist_dir, product="ory_finger"), + ) + + +if __name__ == "__main__": + main() diff --git a/build/gui/ory_finger/config.py b/build/gui/ory_finger/config.py new file mode 100644 index 0000000..6d3cf21 --- /dev/null +++ b/build/gui/ory_finger/config.py @@ -0,0 +1,75 @@ +"""Product configuration for the ORY-FINGER Windows dashboard.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parents[2] +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + +from common.paths import REPO_ROOT +from gui.common.pyinstaller_driver import GuiBuildConfig + +ENTRY_SCRIPT = REPO_ROOT / "examples" / "products" / "ory_finger" / "finger_pipeline_3dview.py" +EXE_NAME = "OrisysDashboard.exe" +DIST_NAME = "OrisysDashboard" + +HIDDEN_IMPORTS = [ + "PyQt5", + "pyqtgraph", + "open3d", + "cv2", + "orisys", + "gui", + "pipeline", + "viz", + "gui.app_paths", + "plotly", + "_plotly_utils", + "open3d.cpu.pybind", +] + +EXCLUDE_MODULES = [ + "cupy", + "cupyx", + "pyopencl", + "tkinter", + "pip", + "wheel", + "matplotlib", +] + +DATA_DIRS = [ + ("config", "config"), + ("assets", "assets"), +] + +README_LINES = [ + "Orisys Finger Dashboard (standalone)", + "", + "Run:", + f" {EXE_NAME}", + f" {EXE_NAME} --video path\\to\\video.mp4", + f" {EXE_NAME} --config config\\dd02-ov.json", + "", + "Bundled folders:", + " config/ sensor JSON configs", + " assets/ finger mesh and visualization data", + "", + "CPU-only build (no CUDA/CuPy).", +] + + +def make_config() -> GuiBuildConfig: + return GuiBuildConfig( + product="ory_finger", + entry_script=ENTRY_SCRIPT, + exe_name=EXE_NAME, + dist_name=DIST_NAME, + hidden_imports=list(HIDDEN_IMPORTS), + exclude_modules=list(EXCLUDE_MODULES), + data_dirs=list(DATA_DIRS), + readme_lines=list(README_LINES), + ) diff --git a/build/gui/ory_mini/__init__.py b/build/gui/ory_mini/__init__.py new file mode 100644 index 0000000..86780ca --- /dev/null +++ b/build/gui/ory_mini/__init__.py @@ -0,0 +1 @@ +"""ORY-MINI internal calibration/testing GUI packaging (scaffold).""" diff --git a/build/gui/ory_mini/build.py b/build/gui/ory_mini/build.py new file mode 100644 index 0000000..9f1a3c6 --- /dev/null +++ b/build/gui/ory_mini/build.py @@ -0,0 +1,30 @@ +"""Scaffold for ORY-MINI internal GUI packaging. + +Not implemented yet. When ready, wire a dedicated calibration/testing entry +script and call ``gui.common.pyinstaller_driver.build_with_pyinstaller``. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parents[2] +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + +from gui.ory_mini.config import DIST_NAME, ENTRY_SCRIPT, make_config + + +def main() -> None: + config = make_config() + raise NotImplementedError( + "ORY-MINI GUI packaging is a scaffold only. " + f"Planned dist={DIST_NAME!r}, entry={ENTRY_SCRIPT}. " + "Implement the internal calibration/testing UI entry, then call " + "gui.common.pyinstaller_driver.build_with_pyinstaller(config)." + ) + + +if __name__ == "__main__": + main() diff --git a/build/gui/ory_mini/config.py b/build/gui/ory_mini/config.py new file mode 100644 index 0000000..53ae8e4 --- /dev/null +++ b/build/gui/ory_mini/config.py @@ -0,0 +1,63 @@ +"""Product configuration scaffold for the ORY-MINI internal GUI.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parents[2] +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + +from common.paths import REPO_ROOT +from gui.common.pyinstaller_driver import GuiBuildConfig + +ENTRY_SCRIPT = REPO_ROOT / "examples" / "products" / "ory_mini" / "basic_pipeline.py" +EXE_NAME = "OryMini.exe" +DIST_NAME = "OryMini" + +HIDDEN_IMPORTS = [ + "PyQt5", + "pyqtgraph", + "cv2", + "orisys", + "gui", + "pipeline", + "viz", +] + +EXCLUDE_MODULES = [ + "cupy", + "cupyx", + "pyopencl", + "open3d", + "tkinter", + "pip", + "wheel", +] + +DATA_DIRS = [ + ("config", "config"), +] + +README_LINES = [ + "ORY-MINI internal calibration/testing GUI (scaffold)", + "", + "This product packaging target is reserved for a future internal tool.", + "Entry point placeholder:", + f" {ENTRY_SCRIPT.as_posix()}", +] + + +def make_config() -> GuiBuildConfig: + return GuiBuildConfig( + product="ory_mini", + entry_script=ENTRY_SCRIPT, + exe_name=EXE_NAME, + dist_name=DIST_NAME, + hidden_imports=list(HIDDEN_IMPORTS), + exclude_modules=list(EXCLUDE_MODULES), + data_dirs=list(DATA_DIRS), + readme_lines=list(README_LINES), + windowed=False, + ) diff --git a/build/run.py b/build/run.py new file mode 100644 index 0000000..092c06f --- /dev/null +++ b/build/run.py @@ -0,0 +1,104 @@ +"""Unified Orisys packaging entrypoint. + +Usage (from repo root): + + python build/run.py wheel [--target internal|pypi-linux] [--verify] + python build/run.py wheel-win + python build/run.py wheel-manylinux [PowerShell args...] + python build/run.py gui finger|mini|box + python build/run.py verify [wheel.whl] + python build/run.py verify --gui-dist [--product ory_finger] + +See build/README.md for details. +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +_BUILD_ROOT = Path(__file__).resolve().parent +_REPO_ROOT = _BUILD_ROOT.parent +if str(_BUILD_ROOT) not in sys.path: + sys.path.insert(0, str(_BUILD_ROOT)) + + +def _run_python(script: Path, argv: list[str]) -> int: + cmd = [sys.executable, str(script), *argv] + return subprocess.call(cmd, cwd=_REPO_ROOT) + + +def _cmd_wheel(argv: list[str]) -> int: + return _run_python(_BUILD_ROOT / "wheel" / "build.py", argv) + + +def _cmd_wheel_win(argv: list[str]) -> int: + bat = _BUILD_ROOT / "wheel" / "win.bat" + return subprocess.call(["cmd", "/c", str(bat), *argv], cwd=_REPO_ROOT) + + +def _cmd_manylinux(argv: list[str]) -> int: + ps1 = _BUILD_ROOT / "wheel" / "docker" / "run.ps1" + cmd = [ + "powershell", + "-ExecutionPolicy", + "Bypass", + "-File", + str(ps1), + *argv, + ] + return subprocess.call(cmd, cwd=_REPO_ROOT) + + +def _cmd_gui(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog="build/run.py gui") + parser.add_argument( + "product", + choices=("finger", "mini", "box", "ory_finger", "ory_mini", "ory_box"), + help="GUI product to package", + ) + args, rest = parser.parse_known_args(argv) + product = { + "finger": "ory_finger", + "mini": "ory_mini", + "box": "ory_box", + }.get(args.product, args.product) + return _run_python(_BUILD_ROOT / "gui" / product / "build.py", rest) + + +def _cmd_verify(argv: list[str]) -> int: + from common.verify.cli import main as verify_main + + return verify_main(argv) + + +def main(argv: list[str] | None = None) -> int: + argv = list(sys.argv[1:] if argv is None else argv) + if not argv or argv[0] in ("-h", "--help"): + print(__doc__) + print("Commands: wheel | wheel-win | manylinux | gui | verify") + print("Examples:") + print(" python build/run.py wheel --help") + print(" python build/run.py gui finger") + print(" python build/run.py manylinux -PythonVersions 3.10") + return 0 + + command, rest = argv[0], argv[1:] + dispatch = { + "wheel": _cmd_wheel, + "wheel-win": _cmd_wheel_win, + "wheel-manylinux": _cmd_manylinux, + "gui": _cmd_gui, + "verify": _cmd_verify, + } + if command not in dispatch: + print(f"Unknown command: {command!r}", file=sys.stderr) + print("Choose from: wheel, wheel-win, manylinux, gui, verify", file=sys.stderr) + return 2 + return dispatch[command](rest) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/build/wheel/__init__.py b/build/wheel/__init__.py new file mode 100644 index 0000000..a7580e0 --- /dev/null +++ b/build/wheel/__init__.py @@ -0,0 +1 @@ +"""Nuitka-compiled SDK wheel packaging.""" diff --git a/build/build_nuitka.py b/build/wheel/build.py similarity index 90% rename from build/build_nuitka.py rename to build/wheel/build.py index a6abdb3..09dca0a 100644 --- a/build/build_nuitka.py +++ b/build/wheel/build.py @@ -11,8 +11,9 @@ Targets: 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 + 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 @@ -24,18 +25,22 @@ 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, 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 ( +_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, ) -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. diff --git a/build/nuitka_compile.py b/build/wheel/compile.py similarity index 94% rename from build/nuitka_compile.py rename to build/wheel/compile.py index 9caa35c..0a5d626 100644 --- a/build/nuitka_compile.py +++ b/build/wheel/compile.py @@ -8,7 +8,11 @@ import sys from importlib.machinery import EXTENSION_SUFFIXES from pathlib import Path -from paths import repo_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", diff --git a/build/Dockerfile.manylinux b/build/wheel/docker/Dockerfile.manylinux similarity index 93% rename from build/Dockerfile.manylinux rename to build/wheel/docker/Dockerfile.manylinux index 9e3d684..fdc6ca0 100644 --- a/build/Dockerfile.manylinux +++ b/build/wheel/docker/Dockerfile.manylinux @@ -23,4 +23,4 @@ RUN for PYBIN in \ done WORKDIR /src -ENTRYPOINT ["/bin/bash", "/src/build/manylinux_entrypoint.sh"] +ENTRYPOINT ["/bin/bash", "/src/build/wheel/docker/entrypoint.sh"] diff --git a/build/manylinux_entrypoint.sh b/build/wheel/docker/entrypoint.sh similarity index 95% rename from build/manylinux_entrypoint.sh rename to build/wheel/docker/entrypoint.sh index c0d2514..470b754 100644 --- a/build/manylinux_entrypoint.sh +++ b/build/wheel/docker/entrypoint.sh @@ -42,7 +42,7 @@ for tag in "${TAG_ARRAY[@]}"; do "${PYBIN}/python" -m pip install -q --upgrade pip setuptools wheel auditwheel - if "${PYBIN}/python" build/build_nuitka.py --target "${TARGET}"; then + if "${PYBIN}/python" build/wheel/build.py --target "${TARGET}"; then BUILT+=("${tag}") else echo "[ERROR] Build failed for ${tag}" >&2 diff --git a/build/build_manylinux_docker.ps1 b/build/wheel/docker/run.ps1 similarity index 84% rename from build/build_manylinux_docker.ps1 rename to build/wheel/docker/run.ps1 index b6f1b6d..219674e 100644 --- a/build/build_manylinux_docker.ps1 +++ b/build/wheel/docker/run.ps1 @@ -1,17 +1,11 @@ # 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 +# .\build\wheel\docker\run.ps1 +# .\build\build_manylinux_docker.ps1 # compatibility wrapper # # 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")] @@ -39,7 +33,7 @@ function Convert-ToManylinuxTag { } } -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path Set-Location $RepoRoot $tags = @() @@ -67,15 +61,17 @@ if ($dockerInfo -ne "linux") { throw "Docker server OS is '$dockerInfo'. Switch Docker Desktop to Linux containers." } -$entrypoint = Join-Path $RepoRoot "build\manylinux_entrypoint.sh" +$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 build/Dockerfile.manylinux -t $ImageName . + docker build -f $dockerfile -t $ImageName . if ($LASTEXITCODE -ne 0) { throw "docker build failed" } diff --git a/build/wheel_layout.py b/build/wheel/layout.py similarity index 100% rename from build/wheel_layout.py rename to build/wheel/layout.py diff --git a/build/wheel_build.py b/build/wheel/package.py similarity index 96% rename from build/wheel_build.py rename to build/wheel/package.py index 19b9670..9bcf495 100644 --- a/build/wheel_build.py +++ b/build/wheel/package.py @@ -8,7 +8,11 @@ import subprocess import sys from pathlib import Path -from paths import sdk_dir +_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( diff --git a/build/build_nuitka_win.bat b/build/wheel/win.bat similarity index 92% rename from build/build_nuitka_win.bat rename to build/wheel/win.bat index fb9f9d1..6a45b5e 100644 --- a/build/build_nuitka_win.bat +++ b/build/wheel/win.bat @@ -3,7 +3,7 @@ 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\.." +cd /d "%~dp0\..\.." if errorlevel 1 ( echo ERROR: Could not change to repository root. exit /b 1 @@ -32,7 +32,7 @@ for %%E in (py310 py311 py312) do ( set "OK=0" if !HAS_CONDA! equ 1 ( - call conda run --no-capture-output -n %%E python build\build_nuitka.py + call conda run --no-capture-output -n %%E python build\wheel\build.py if not errorlevel 1 set "OK=1" ) @@ -44,7 +44,7 @@ for %%E in (py310 py311 py312) do ( if not exist "!PYEXE!" ( echo ERROR: Python not found: !PYEXE! ) else ( - "!PYEXE!" build\build_nuitka.py + "!PYEXE!" build\wheel\build.py if not errorlevel 1 set "OK=1" ) ) diff --git a/config/ddjx01.json b/config/ddjx01.json index af88413..6b66456 100644 --- a/config/ddjx01.json +++ b/config/ddjx01.json @@ -31,7 +31,7 @@ "dis_preset": "medium", "dis_mode": 3, "algorithm_list": ["raft", "dis"], - "hhd_downsample": 3 + "hhd_downsample": 1 }, "contact": { "min_contour_area": 3000, diff --git a/config/ddjx01_q6a.json b/config/ddjx01_q6a.json index d6a1335..08c7f59 100644 --- a/config/ddjx01_q6a.json +++ b/config/ddjx01_q6a.json @@ -31,7 +31,7 @@ "dis_preset": "medium", "dis_mode": 3, "algorithm_list": ["raft", "dis"], - "hhd_downsample": 3, + "hhd_downsample": 1, "backend": "cpu" }, "contact": { diff --git a/docs/publishing.md b/docs/publishing.md index c860375..17b07e8 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -40,35 +40,35 @@ docker run --rm hello-world ## Build manylinux wheels (3.10 / 3.11 / 3.12) -From the repository root. Prefer PowerShell, or from **cmd.exe**: +From the repository root: -```bat -powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1 +```bash +python build/run.py manylinux ``` -PowerShell: +Or call the Docker helper directly: ```powershell -.\build\build_manylinux_docker.ps1 +.\build\wheel\docker\run.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 +python build/run.py manylinux -PythonVersions 3.10,3.12 +python build/run.py manylinux -PythonVersions 3.11 ``` What this does: -1. Builds `orisys-manylinux` from [`build/Dockerfile.manylinux`](../build/Dockerfile.manylinux) +1. Builds `orisys-manylinux` from [`build/wheel/docker/Dockerfile.manylinux`](../build/wheel/docker/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: +2. Runs [`build/wheel/docker/entrypoint.sh`](../build/wheel/docker/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 + /opt/python/cp310-cp310/bin/python build/wheel/build.py --target pypi-linux + /opt/python/cp311-cp311/bin/python build/wheel/build.py --target pypi-linux + /opt/python/cp312-cp312/bin/python build/wheel/build.py --target pypi-linux ``` 3. For each ABI: Nuitka compile → wheel → `auditwheel repair` → isolated smoke test. @@ -82,13 +82,13 @@ Expect roughly **3×** the wall time of a single-ABI build. Rebuild without refreshing the image: ```powershell -.\build\build_manylinux_docker.ps1 -SkipImageBuild +python build/run.py manylinux -SkipImageBuild ``` Equivalent manual `docker` invocation: ```powershell -docker build -f build/Dockerfile.manylinux -t orisys-manylinux . +docker build -f build/wheel/docker/Dockerfile.manylinux -t orisys-manylinux . docker run --rm ` -e ORISYS_BUILD_TARGET=pypi-linux ` -e ORISYS_PYTHON_TAGS=cp310-cp310,cp311-cp311,cp312-cp312 ` @@ -106,7 +106,7 @@ docker run --rm ` Optional re-verify on a Linux machine: ```bash -python build/verify.py dist/repaired/orisys-*-cp310-cp310-manylinux_*.whl +python build/run.py verify dist/repaired/orisys-*-cp310-cp310-manylinux_*.whl ``` ## Promote: TestPyPI then PyPI @@ -157,11 +157,15 @@ are not stored on developer machines. | 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 build/run.py wheel` | Preferred wheel builder launcher | +| `python build/wheel/build.py` | Direct wheel builder (current host) | +| `python build/run.py wheel --target pypi-linux` | Linux-only; requires manylinux + CPython 3.10–3.12 | +| `python build/run.py wheel-win` | Windows wheels for py310/py311/py312 conda envs | +| `python build/run.py manylinux` | manylinux Docker runner | +| `python build/run.py gui finger` | ORY-FINGER standalone GUI | | `python -m build` | Pure-Python / sdist path from `pyproject.toml` (no Nuitka) | +See also [`build/README.md`](../build/README.md) for the target-oriented layout. + Windows binary wheels and Linux manylinux wheels must be built on their respective platforms. Never rename a platform tag to bypass PyPI validation. diff --git a/pyproject.toml b/pyproject.toml index 4dcad2e..e9ea3d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ version = { attr = "orisys.__version__" } # Single source of truth for bundled non-code files. # - dev_install = true -> pip install / editable install (via setup.py) -# - all items -> Nuitka wheel (build/wheel_layout.py) +# - all items -> Nuitka wheel (build/wheel/layout.py) [tool.orisys.bundle] # Use patterns (globs) and/or files (explicit names under source/). diff --git a/setup.py b/setup.py index 6008341..deacbae 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,6 @@ from setuptools import setup REPO_ROOT = Path(__file__).resolve().parent sys.path.insert(0, str(REPO_ROOT / "build")) -from pyproject_utils import bundle_data_files +from common.pyproject_utils import bundle_data_files setup(data_files=bundle_data_files(REPO_ROOT)) diff --git a/tests/test_pyproject.py b/tests/test_pyproject.py index 485009d..29a83a2 100644 --- a/tests/test_pyproject.py +++ b/tests/test_pyproject.py @@ -12,7 +12,7 @@ def packaging_utils(): build_dir = REPO_ROOT / "build" if str(build_dir) not in sys.path: sys.path.insert(0, str(build_dir)) - from pyproject_utils import extras_require, install_requires, project_metadata + from common.pyproject_utils import extras_require, install_requires, project_metadata return install_requires, extras_require, project_metadata @@ -33,7 +33,7 @@ def test_pyproject_dynamic_version(packaging_utils): build_dir = REPO_ROOT / "build" if str(build_dir) not in sys.path: sys.path.insert(0, str(build_dir)) - from pyproject_utils import get_version + from common.pyproject_utils import get_version assert get_version(REPO_ROOT) == "0.4.2" @@ -44,7 +44,7 @@ def test_pyproject_bundle_manifest(packaging_utils): build_dir = REPO_ROOT / "build" if str(build_dir) not in sys.path: sys.path.insert(0, str(build_dir)) - from pyproject_utils import bundle_data_files, bundle_items + from common.pyproject_utils import bundle_data_files, bundle_items items = bundle_items(REPO_ROOT) names = {item["name"] for item in items} @@ -69,7 +69,7 @@ def test_bundle_explicit_files(): build_dir = REPO_ROOT / "build" if str(build_dir) not in sys.path: sys.path.insert(0, str(build_dir)) - from wheel_layout import data_file_entries_for_item, resolve_bundle_source_files + from wheel.layout import data_file_entries_for_item, resolve_bundle_source_files item = { "name": "configs", diff --git a/tests/test_wheel_build.py b/tests/test_wheel_build.py index 471a9fd..6e405ec 100644 --- a/tests/test_wheel_build.py +++ b/tests/test_wheel_build.py @@ -6,37 +6,37 @@ REPO_ROOT = Path(__file__).resolve().parents[1] @pytest.fixture -def wheel_build(): +def wheel_package(): 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 + from wheel import package as wheel_package - return wheel_build + return wheel_package -def test_compliant_manylinux_tags(wheel_build): - assert wheel_build.is_compliant_manylinux_wheel( +def test_compliant_manylinux_tags(wheel_package): + assert wheel_package.is_compliant_manylinux_wheel( Path("orisys-0.4.2-cp310-cp310-manylinux_2_17_x86_64.whl") ) - assert wheel_build.is_compliant_manylinux_wheel( + assert wheel_package.is_compliant_manylinux_wheel( Path("orisys-0.4.2-cp310-cp310-manylinux2014_x86_64.whl") ) - assert wheel_build.is_compliant_manylinux_wheel( + assert wheel_package.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( +def test_rejects_illegal_manylinux_tags(wheel_package): + assert not wheel_package.is_compliant_manylinux_wheel( Path("orisys-0.4.2-cp310-cp310-manylinux_x86_64.whl") ) - assert not wheel_build.is_compliant_manylinux_wheel( + assert not wheel_package.is_compliant_manylinux_wheel( Path("orisys-0.4.2-cp310-cp310-linux_x86_64.whl") ) - assert not wheel_build.is_compliant_manylinux_wheel( + assert not wheel_package.is_compliant_manylinux_wheel( Path("orisys-0.4.2-cp310-cp310-win_amd64.whl") ) @@ -47,7 +47,7 @@ def test_pyproject_no_private_classifier(): 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 + from common.pyproject_utils import project_metadata classifiers = project_metadata(REPO_ROOT).get("classifiers", []) assert "Private :: Do Not Upload" not in classifiers