Skip to content

Commit 0563de1

Browse files
authored
PR realsenseai#15064 from remibettan: pyrealsense2: add build_wheel.py helper for local wheel builds
2 parents 6301abe + 4e838f2 commit 0563de1

2 files changed

Lines changed: 199 additions & 1 deletion

File tree

wrappers/python/build_wheel.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
## License: Apache 2.0. See LICENSE file in root directory.
2+
## Copyright(c) 2026 RealSense, Inc. All Rights Reserved.
3+
4+
"""
5+
Build a pyrealsense2 wheel locally from a CMake build directory.
6+
7+
Usage:
8+
python build_wheel.py --build-dir <path-to-cmake-build>
9+
10+
Examples (run from wrappers/python/):
11+
# Windows multi-config build
12+
python build_wheel.py --build-dir ../../build/Release
13+
14+
# Linux/macOS single-config build
15+
python build_wheel.py --build-dir ../../build
16+
17+
# Windows build with DDS — bundle fastdds, fastcdr, foonathan_memory
18+
python build_wheel.py --build-dir ../../build/Release \
19+
--extra-lib "fastdds*.dll" --extra-lib "fastcdr*.dll" --extra-lib "foonathan_memory*.dll"
20+
21+
The script:
22+
1. Generates pyrealsense2/_version.py (via find_librs_version.py)
23+
2. Copies the compiled extension (.pyd/.so) and the librealsense2 shared
24+
library out of the build dir into pyrealsense2/
25+
3. Runs `python -m build --wheel`, producing dist/pyrealsense2-*.whl
26+
27+
Use --extra-lib to bundle additional runtime dependencies (e.g. DDS libs).
28+
"""
29+
import argparse
30+
import os
31+
import platform
32+
import shutil
33+
import subprocess
34+
import sys
35+
from pathlib import Path
36+
37+
SCRIPT_DIR = Path(__file__).resolve().parent # wrappers/python/
38+
PACKAGE_DIR = SCRIPT_DIR / "pyrealsense2"
39+
LIBREALSENSE_ROOT = (SCRIPT_DIR / ".." / "..").resolve()
40+
41+
# Files we manage in the package dir — safe to remove between runs.
42+
# __init__.py is committed and must NOT be touched.
43+
STAGED_SUFFIXES = (".pyd", ".dll", ".so", ".dylib")
44+
STAGED_NAMES = ("_version.py",)
45+
46+
47+
def platform_patterns():
48+
system = platform.system()
49+
if system == "Windows":
50+
return "pyrealsense2*.pyd", ["realsense2.dll"]
51+
if system == "Darwin":
52+
return "pyrealsense2*.so", ["librealsense2*.dylib"]
53+
return "pyrealsense2*.so", ["librealsense2.so*"]
54+
55+
56+
def find_artifacts(build_dir, extra_patterns):
57+
ext_pattern, runtime_patterns = platform_patterns()
58+
runtime_patterns = list(runtime_patterns) + list(extra_patterns)
59+
60+
ext_matches = list(build_dir.rglob(ext_pattern))
61+
if not ext_matches:
62+
raise FileNotFoundError(
63+
"No '{}' found under {}. Did the build complete?".format(ext_pattern, build_dir)
64+
)
65+
if len(ext_matches) > 1:
66+
print("Warning: multiple {} found, using first:".format(ext_pattern))
67+
for m in ext_matches:
68+
print(" {}".format(m))
69+
extension = ext_matches[0]
70+
71+
runtime_libs = []
72+
seen = set()
73+
for pattern in runtime_patterns:
74+
for lib in build_dir.rglob(pattern):
75+
if lib.name in seen:
76+
continue
77+
seen.add(lib.name)
78+
runtime_libs.append(lib)
79+
return extension, runtime_libs
80+
81+
82+
def clean_staged():
83+
for f in PACKAGE_DIR.iterdir():
84+
if f.name == "__init__.py":
85+
continue
86+
if f.name in STAGED_NAMES or f.suffix in STAGED_SUFFIXES or ".so." in f.name:
87+
print(" removing stale: {}".format(f.name))
88+
if f.is_symlink() or f.exists():
89+
f.unlink()
90+
91+
92+
def stage(src):
93+
# Resolve symlinks so the wheel ends up with real files. Wheels are zips
94+
# and don't preserve symlinks reliably; a versioned `librealsense2.so.2`
95+
# symlink would otherwise land in the wheel as a broken pointer back into
96+
# the build tree.
97+
dest = PACKAGE_DIR / src.name
98+
if dest.exists() or dest.is_symlink():
99+
dest.unlink()
100+
shutil.copy2(src.resolve(), dest, follow_symlinks=True)
101+
print(" staged: {}".format(src.name))
102+
103+
104+
def generate_version_file():
105+
subprocess.check_call([
106+
sys.executable,
107+
str(SCRIPT_DIR / "find_librs_version.py"),
108+
str(LIBREALSENSE_ROOT),
109+
str(PACKAGE_DIR),
110+
])
111+
112+
113+
def ensure_build_tool():
114+
try:
115+
subprocess.check_call(
116+
[sys.executable, "-m", "build", "--version"],
117+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
118+
)
119+
except (subprocess.CalledProcessError, FileNotFoundError):
120+
print("Installing 'build' and 'hatchling'...")
121+
subprocess.check_call([sys.executable, "-m", "pip", "install", "build", "hatchling"])
122+
123+
124+
def main():
125+
parser = argparse.ArgumentParser(
126+
description=__doc__,
127+
formatter_class=argparse.RawDescriptionHelpFormatter,
128+
)
129+
parser.add_argument(
130+
"--build-dir",
131+
required=True,
132+
type=lambda p: Path(p).expanduser().resolve(),
133+
help="CMake build directory containing the compiled pyrealsense2 extension",
134+
)
135+
parser.add_argument(
136+
"--extra-lib",
137+
action="append",
138+
default=[],
139+
metavar="GLOB",
140+
help="Additional runtime library glob to bundle (repeatable), e.g. fastdds*.dll",
141+
)
142+
args = parser.parse_args()
143+
144+
if not args.build_dir.exists():
145+
sys.exit("Error: build dir not found: {}".format(args.build_dir))
146+
147+
print("Source root: {}".format(LIBREALSENSE_ROOT))
148+
print("Build dir: {}".format(args.build_dir))
149+
print("Package dir: {}".format(PACKAGE_DIR))
150+
print()
151+
152+
print("Locating build artifacts...")
153+
extension, runtime_libs = find_artifacts(args.build_dir, args.extra_lib)
154+
print(" extension: {}".format(extension))
155+
for lib in runtime_libs:
156+
print(" runtime: {}".format(lib))
157+
if not runtime_libs:
158+
print(" (no runtime libs matched — extension may fail to load on the target machine)")
159+
print()
160+
161+
print("Cleaning previously-staged files...")
162+
clean_staged()
163+
print()
164+
165+
print("Generating _version.py...")
166+
generate_version_file()
167+
print()
168+
169+
print("Staging into package...")
170+
stage(extension)
171+
for lib in runtime_libs:
172+
stage(lib)
173+
print()
174+
175+
print("Ensuring 'build' is available...")
176+
ensure_build_tool()
177+
print()
178+
179+
print("Running `python -m build --wheel`...")
180+
subprocess.check_call([sys.executable, "-m", "build", "--wheel"], cwd=str(SCRIPT_DIR))
181+
print()
182+
183+
dist = SCRIPT_DIR / "dist"
184+
wheels = sorted(dist.glob("pyrealsense2-*.whl"), key=lambda p: p.stat().st_mtime, reverse=True)
185+
if not wheels:
186+
sys.exit("Build completed but no wheel found in {}".format(dist))
187+
188+
print("Success! Wheel: {}".format(wheels[0]))
189+
print()
190+
print("Install on the target machine with:")
191+
print(" pip install {}".format(wheels[0].name))
192+
193+
194+
if __name__ == "__main__":
195+
main()

wrappers/python/pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ homepage = "https://github.com/realsenseai/librealsense"
4646
[tool.hatch.build]
4747
artifacts = [
4848
"pyrealsense2/*.so",
49+
"pyrealsense2/*.so.*",
4950
"pyrealsense2/*.pyd",
50-
"pyrealsense2/*.dll"
51+
"pyrealsense2/*.dll",
52+
"pyrealsense2/*.dylib",
53+
"pyrealsense2/*.dylib.*"
5154
]
5255
packages = ["pyrealsense2"]
5356

0 commit comments

Comments
 (0)