add back GUI 2D display with optional raw / pure color background

This commit is contained in:
ykcheungab
2026-06-21 18:01:13 +08:00
parent 7feae9550e
commit 04a4df3303
25 changed files with 931 additions and 9 deletions
+44
View File
@@ -0,0 +1,44 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['D:\\repo\\orisyssdk_git\\examples\\finger_pipeline_3dview.py'],
pathex=[],
binaries=[],
datas=[('D:\\repo\\orisyssdk_git\\config', 'config'), ('D:\\repo\\orisyssdk_git\\assets', 'assets')],
hiddenimports=['PyQt5', 'pyqtgraph', 'open3d', 'cv2', 'orisys', 'gui', 'pipeline', 'viz', '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',
)
+194
View File
@@ -0,0 +1,194 @@
"""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/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",
"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()
+89
View File
@@ -0,0 +1,89 @@
"""Build a Nuitka-compiled orisys wheel and publish it to SDK/{version}/.
Pipeline:
1. Compile src/orisys → .pyd
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
Run from repo root: python build/build_nuitka.py
"""
from __future__ import annotations
import os
import shutil
import subprocess
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 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_layout import copy_bundle_items, package_data_from_bundle
def _reset_dir(path) -> None:
"""Remove and recreate a build output directory."""
if path.exists():
shutil.rmtree(path)
path.mkdir(parents=True)
def build() -> None:
# --- 1. Compile ---
install_nuitka()
version = get_version(REPO_ROOT)
_reset_dir(NUITKA_COMPILE_DIR)
_reset_dir(NUITKA_WHEEL_DIR)
print("\n=== Compiling with Nuitka ===")
env = os.environ.copy()
src_dir = repo_path("src")
env["PYTHONPATH"] = src_dir.as_posix() + os.pathsep + env.get("PYTHONPATH", "")
ext_file = compile_orisys(NUITKA_COMPILE_DIR, env)
# --- 2. Stage wheel contents ---
print("\n=== Preparing Wheel Structure ===")
pkg_dir = NUITKA_WHEEL_DIR / "wheel_root" / "orisys"
pkg_dir.mkdir(parents=True)
ext_suffix = copy_compiled_orisys(pkg_dir, ext_file)
# Bundle rules live in pyproject.toml → [[tool.orisys.bundle.items]]
bundle = bundle_items(REPO_ROOT)
copied_counts = copy_bundle_items(REPO_ROOT, pkg_dir, bundle)
print(f"Copied {copied_counts['configs']} JSON config files")
print(f"Copied {copied_counts['examples']} example files to orisys/examples/")
print(f"Copied {copied_counts['docs']} doc files to orisys/docs/")
print(f"Copied {copied_counts['assets']} finger asset files to orisys/assets/")
write_wheel_setup(
NUITKA_WHEEL_DIR,
version=version,
package_data=package_data_from_bundle(ext_suffix, bundle),
install_requires=install_requires(REPO_ROOT),
extras_require=extras_require(REPO_ROOT),
)
# --- 3. Build wheel and publish to SDK/ ---
wheel_path = run_bdist_wheel(NUITKA_WHEEL_DIR)
if wheel_path is None:
print("\n[ERROR] Wheel not found.")
return
final_wheel = publish_wheel_to_sdk(wheel_path, version)
print(f"\n[SUCCESS] Wheel created and moved to: {final_wheel}")
# # --- 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 __name__ == "__main__":
build()
+70
View File
@@ -0,0 +1,70 @@
@echo off
REM Build Nuitka wheels for Python 3.10, 3.11, and 3.12.
REM Expects conda envs named py310, py311, py312 (or set CONDA_ENVS_ROOT).
setlocal enabledelayedexpansion
cd /d "%~dp0\.."
if errorlevel 1 (
echo ERROR: Could not change to repository root.
exit /b 1
)
if not defined CONDA_ENVS_ROOT (
if exist "%USERPROFILE%\anaconda3\envs" (
set "CONDA_ENVS_ROOT=%USERPROFILE%\anaconda3\envs"
) else if exist "%USERPROFILE%\miniconda3\envs" (
set "CONDA_ENVS_ROOT=%USERPROFILE%\miniconda3\envs"
) else if exist "%LOCALAPPDATA%\anaconda3\envs" (
set "CONDA_ENVS_ROOT=%LOCALAPPDATA%\anaconda3\envs"
)
)
where conda >nul 2>&1
set "HAS_CONDA=0"
if not errorlevel 1 set "HAS_CONDA=1"
set FAILED=0
for %%E in (py310 py311 py312) do (
echo.
echo ========================================
echo Building orisys wheel: %%E
echo ========================================
set "OK=0"
if !HAS_CONDA! equ 1 (
call conda run --no-capture-output -n %%E python build\build_nuitka.py
if not errorlevel 1 set "OK=1"
)
if !OK! equ 0 (
if not defined CONDA_ENVS_ROOT (
echo ERROR: Could not find conda env %%E. Set CONDA_ENVS_ROOT or add conda to PATH.
) else (
set "PYEXE=!CONDA_ENVS_ROOT!\%%E\python.exe"
if not exist "!PYEXE!" (
echo ERROR: Python not found: !PYEXE!
) else (
"!PYEXE!" build\build_nuitka.py
if not errorlevel 1 set "OK=1"
)
)
)
if !OK! equ 0 set FAILED=1
)
echo.
if !FAILED! equ 1 (
echo One or more wheel builds failed.
exit /b 1
)
echo SUCCESS: all wheels built under SDK\
for /f "delims=" %%V in ('dir /b /ad SDK 2^>nul') do (
for %%W in ("SDK\%%V\orisys-*-cp310-*.whl" "SDK\%%V\orisys-*-cp311-*.whl" "SDK\%%V\orisys-*-cp312-*.whl") do (
if exist %%W echo %%~nxW
)
)
endlocal
exit /b 0
+87
View File
@@ -0,0 +1,87 @@
"""Nuitka compilation of the orisys package."""
from __future__ import annotations
import shutil
import subprocess
import sys
from importlib.machinery import EXTENSION_SUFFIXES
from pathlib import Path
from paths import repo_path
NUITKA_INCLUDE_PACKAGES = (
"orisys",
"orisys.finger",
"orisys.unroll",
"orisys.hhd",
"orisys.hhd.solvers",
)
NUITKA_NOFOLLOW_IMPORTS = (
"cupy",
"cupyx",
"pyopencl",
"open3d",
)
def install_nuitka() -> None:
print("Checking Nuitka installation...")
try:
import nuitka # noqa: F401
print("Nuitka is installed.")
except ImportError:
print("Installing Nuitka...")
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "nuitka", "ordered-set", "zstandard"]
)
def find_compiled_extension(output_dir: Path, module_name: str) -> Path:
candidates = []
for suffix in EXTENSION_SUFFIXES:
candidates.extend(output_dir.glob(f"{module_name}*{suffix}"))
if not candidates:
all_files = ", ".join(p.name for p in output_dir.iterdir()) if output_dir.exists() else "(none)"
raise RuntimeError(
f"Nuitka did not produce a compiled extension for {module_name}. Found: {all_files}"
)
return sorted(candidates, key=lambda p: len(p.name))[0]
def compile_orisys(build_root: Path, env: dict[str, str]) -> Path:
module_output_dir = build_root / "orisys"
if module_output_dir.exists():
shutil.rmtree(module_output_dir)
module_output_dir.mkdir(parents=True)
cmd = [
sys.executable,
"-m",
"nuitka",
"--module",
"--output-dir=" + str(module_output_dir),
"--remove-output",
"--no-pyi-file",
repo_path("src/orisys").as_posix(),
]
for package_name in NUITKA_INCLUDE_PACKAGES:
cmd.append(f"--include-package={package_name}")
for import_name in NUITKA_NOFOLLOW_IMPORTS:
cmd.append(f"--nofollow-import-to={import_name}")
print(f"Running: {' '.join(cmd)}")
subprocess.check_call(cmd, env=env)
ext_file = find_compiled_extension(module_output_dir, "orisys")
print(f"Generated compiled extension for orisys: {ext_file}")
return ext_file
def copy_compiled_orisys(pkg_dir: Path, ext_file: Path) -> str:
ext_suffix = "".join(ext_file.suffixes)
dest_ext = pkg_dir / f"__init__{ext_suffix}"
shutil.copy(ext_file, dest_ext)
print(f"Copied compiled module orisys to {dest_ext}")
return ext_suffix
+20
View File
@@ -0,0 +1,20 @@
"""Repository and Nuitka build output paths."""
from __future__ import annotations
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
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"
def repo_path(*parts: str) -> Path:
return REPO_ROOT.joinpath(*parts)
def sdk_dir(version: str) -> Path:
return REPO_ROOT / "SDK" / version
+100
View File
@@ -0,0 +1,100 @@
"""Read project metadata from pyproject.toml for packaging scripts."""
from __future__ import annotations
import importlib
import subprocess
import sys
from pathlib import Path
def _load_toml(text: str) -> dict:
if sys.version_info >= (3, 11):
import tomllib
return tomllib.loads(text)
try:
import tomli
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "tomli", "-q"])
import tomli
return tomli.loads(text)
def load_pyproject(repo_root: Path) -> dict:
path = repo_root / "pyproject.toml"
return _load_toml(path.read_text(encoding="utf-8"))
def project_metadata(repo_root: Path) -> dict:
return load_pyproject(repo_root)["project"]
def install_requires(repo_root: Path) -> list[str]:
return list(project_metadata(repo_root)["dependencies"])
def extras_require(repo_root: Path) -> dict[str, list[str]]:
optional = project_metadata(repo_root).get("optional-dependencies", {})
return {name: list(requirements) for name, requirements in optional.items()}
def _resolve_version_attr(repo_root: Path, attr: str) -> str:
"""Resolve a setuptools dynamic version attr (e.g. orisys.__version__)."""
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)
def get_version(repo_root: Path) -> str:
"""Return project version from pyproject.toml (static or dynamic)."""
project = project_metadata(repo_root)
if "version" in project:
return str(project["version"])
dynamic = load_pyproject(repo_root).get("tool", {}).get("setuptools", {}).get("dynamic", {})
version_cfg = dynamic.get("version")
if isinstance(version_cfg, dict) and "attr" in version_cfg:
return _resolve_version_attr(repo_root, version_cfg["attr"])
raise RuntimeError("Project version is not defined in pyproject.toml")
def bundle_items(repo_root: Path) -> list[dict]:
"""Return bundle rules from [[tool.orisys.bundle.items]] in pyproject.toml."""
items = load_pyproject(repo_root).get("tool", {}).get("orisys", {}).get("bundle", {}).get("items")
if not items:
raise RuntimeError("Missing [[tool.orisys.bundle.items]] in pyproject.toml")
return list(items)
def bundle_data_files(repo_root: Path) -> list[tuple[str, list[str]]]:
"""Map bundle items marked dev_install=true to setuptools data_files entries."""
from wheel_layout import resolve_bundle_source_files
grouped: dict[str, list[str]] = {}
for item in bundle_items(repo_root):
if not item.get("dev_install", False):
continue
src_dir = repo_root / item["source"]
base_dest = f"orisys/{item['target']}".replace("\\", "/")
for src_file in resolve_bundle_source_files(repo_root, item):
rel_src = src_file.relative_to(repo_root).as_posix()
if item.get("preserve_tree", False):
rel_parent = src_file.relative_to(src_dir).parent.as_posix()
dest = base_dest if rel_parent == "." else f"{base_dest}/{rel_parent}"
else:
dest = base_dest
grouped.setdefault(dest, []).append(rel_src)
return [(dest, sorted(files)) for dest, files in sorted(grouped.items())]
+82
View File
@@ -0,0 +1,82 @@
"""Post-build smoke tests for packaged wheels and standalone GUI builds."""
from __future__ import annotations
import argparse
import subprocess
import sys
import tempfile
import venv
from pathlib import Path
from paths import GUI_DIST_DIR
def _venv_python(venv_dir: Path) -> Path:
if sys.platform == "win32":
return venv_dir / "Scripts" / "python.exe"
return venv_dir / "bin" / "python"
def verify_built_wheel(wheel_path: Path) -> None:
"""Install the wheel in a temporary venv and smoke-test finger imports."""
wheel_path = wheel_path.resolve()
with tempfile.TemporaryDirectory(prefix="orisys-wheel-verify-") as tmp:
venv_dir = Path(tmp) / "venv"
venv.create(venv_dir, with_pip=True)
python = _venv_python(venv_dir)
subprocess.check_call(
[python, "-m", "pip", "install", "-q", str(wheel_path)],
)
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')"
),
],
)
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" / "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()
+62
View File
@@ -0,0 +1,62 @@
"""Assemble and build the Nuitka wheel with setuptools."""
from __future__ import annotations
import shutil
import subprocess
import sys
from pathlib import Path
from paths import sdk_dir
def write_wheel_setup(
wheel_dir: Path,
*,
version: str,
package_data: dict[str, list[str]],
install_requires: list[str],
extras_require: dict[str, list[str]],
) -> Path:
setup_py_path = wheel_dir / "setup.py"
setup_content = f"""
from setuptools import setup, Distribution
class BinaryDistribution(Distribution):
def has_ext_modules(self):
return True
setup(
name="orisys",
version="{version}",
description="Orisys - Optical Tactile Sensor SDK (Nuitka Compiled)",
author="Orisys",
packages=['orisys', 'orisys.examples'],
package_dir={{'': 'wheel_root'}},
package_data={package_data},
include_package_data=True,
install_requires={install_requires!r},
extras_require={extras_require!r},
distclass=BinaryDistribution,
zip_safe=False,
)
"""
setup_py_path.write_text(setup_content, encoding="utf-8")
return setup_py_path
def run_bdist_wheel(wheel_dir: Path) -> Path | None:
print("\n=== Building Wheel ===")
subprocess.check_call([sys.executable, "setup.py", "bdist_wheel"], cwd=wheel_dir)
wheels = sorted((wheel_dir / "dist").glob("*.whl"))
return wheels[0] if wheels else None
def 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))
return dest_wheel
+91
View File
@@ -0,0 +1,91 @@
"""Copy bundled non-code files into the Nuitka wheel tree."""
from __future__ import annotations
import shutil
from pathlib import Path
def resolve_bundle_source_files(repo_root: Path, item: dict) -> list[Path]:
"""Resolve explicit files and/or glob patterns for one bundle item."""
src_dir = repo_root / item["source"]
patterns = item.get("patterns", [])
files = item.get("files", [])
if not patterns and not files:
raise ValueError(f"Bundle item {item['name']!r} requires patterns and/or files")
seen: set[Path] = set()
resolved: list[Path] = []
def add(path: Path) -> None:
key = path.resolve()
if key not in seen:
seen.add(key)
resolved.append(path)
for pattern in patterns:
for path in sorted(src_dir.glob(pattern)):
if path.is_file():
add(path)
for name in files:
path = src_dir / name
if not path.is_file():
raise FileNotFoundError(f"Bundle file not found for {item['name']}: {path}")
add(path)
return resolved
def package_data_paths_for_item(item: dict) -> list[str]:
"""Return setuptools package_data paths relative to the orisys package."""
target = str(item["target"]).replace("\\", "/")
entries = [f"{target}/{pattern}" for pattern in item.get("patterns", [])]
entries.extend(f"{target}/{name}" for name in item.get("files", []))
return entries
def data_file_entries_for_item(item: dict) -> list[str]:
"""Return setuptools data_files paths relative to the repo root."""
source = str(item["source"]).replace("\\", "/")
entries = [f"{source}/{pattern}" for pattern in item.get("patterns", [])]
entries.extend(f"{source}/{name}" for name in item.get("files", []))
return entries
def copy_bundle_items(repo_root: Path, pkg_dir: Path, items: list[dict]) -> dict[str, int]:
copied_counts: dict[str, int] = {}
for item in items:
name = item["name"]
src_dir = repo_root / item["source"]
dst_dir = pkg_dir / item["target"]
dst_dir.mkdir(parents=True, exist_ok=True)
copied = 0
for src_file in resolve_bundle_source_files(repo_root, item):
if item.get("preserve_tree", False):
dst_file = dst_dir / src_file.relative_to(src_dir)
dst_file.parent.mkdir(parents=True, exist_ok=True)
else:
dst_file = dst_dir / src_file.name
shutil.copy(src_file, dst_file)
copied += 1
print(f"Copied {name}: {src_file}")
if name == "examples":
(dst_dir / "__init__.py").write_text("# Examples for orisys package\n", encoding="utf-8")
copied_counts[name] = copied
return copied_counts
def package_data_from_bundle(ext_suffix: str, items: list[dict]) -> dict[str, list[str]]:
package_data: dict[str, list[str]] = {
"orisys": [f"*{ext_suffix}"],
"orisys.examples": ["*.py", "README.md", "app_paths.py", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"],
}
for item in items:
if item["name"] == "examples":
continue
package_data["orisys"].extend(package_data_paths_for_item(item))
return package_data