Skip to content

Commit 1958a71

Browse files
committed
pep 517 wheels
1 parent af41fb0 commit 1958a71

24 files changed

Lines changed: 2079 additions & 26 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
.p4*
22
.DS_Store
33
.AppleDouble
4-
4+
.wheel-build
5+
.venv_openusd_packaging
6+
dist
7+
packaging/**/*.pyc

build_scripts/build_usd.py

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -223,21 +223,28 @@ def GetPythonInfo(context):
223223
pythonVersion = sysconfig.get_config_var("py_version_short") # "3.7"
224224

225225
# Lib path is unfortunately special for each platform and there is no
226-
# config_var for it. But we can deduce it for each platform, and this
227-
# logic works for any Python version.
228-
def _GetPythonLibraryFilename(context):
226+
# single config_var that works everywhere. Build an ordered list of
227+
# candidate filenames and then pick the first one that actually exists.
228+
def _GetPythonLibraryFilenames(context):
229229
if Windows():
230-
return "python{version}{suffix}.lib".format(
230+
return ["python{version}{suffix}.lib".format(
231231
version=sysconfig.get_config_var("py_version_nodot"),
232232
suffix=('_d' if context.buildDebug and context.debugPython
233233
else ''))
234+
]
234235
elif Linux():
235-
return sysconfig.get_config_var("LDLIBRARY")
236+
candidates = [
237+
sysconfig.get_config_var("LDLIBRARY"),
238+
sysconfig.get_config_var("INSTSONAME"),
239+
sysconfig.get_config_var("LIBRARY"),
240+
]
241+
return [c for i, c in enumerate(candidates) if c and c not in candidates[:i]]
236242
elif MacOS():
237-
return "libpython{version}.dylib".format(
243+
return ["libpython{version}.dylib".format(
238244
version=(sysconfig.get_config_var('LDVERSION') or
239245
sysconfig.get_config_var('VERSION') or
240246
pythonVersion))
247+
]
241248
else:
242249
raise RuntimeError("Platform not supported")
243250

@@ -250,27 +257,47 @@ def _GetPythonLibraryFilename(context):
250257
# if in a venv, installed_base will be the "original" python,
251258
# which is where the libs are ("base" will be the venv dir)
252259
pythonBaseDir = sysconfig.get_config_var("installed_base")
260+
pythonLibPath = None
253261
if Windows():
254262
pythonLibPath = os.path.join(pythonBaseDir, "libs",
255-
_GetPythonLibraryFilename(context))
263+
_GetPythonLibraryFilenames(context)[0])
256264
elif Linux():
257265
pythonMultiarchSubdir = sysconfig.get_config_var("multiarchsubdir")
258-
# Try multiple ways to get the python lib dir
259-
for pythonLibDir in (sysconfig.get_config_var("LIBDIR"),
260-
os.path.join(pythonBaseDir, "lib")):
261-
if pythonMultiarchSubdir:
262-
pythonLibPath = \
263-
os.path.join(pythonLibDir + pythonMultiarchSubdir,
264-
_GetPythonLibraryFilename(context))
265-
if os.path.isfile(pythonLibPath):
266+
pythonLibDirs = []
267+
268+
def _AppendPythonLibDir(path):
269+
if path and path not in pythonLibDirs:
270+
pythonLibDirs.append(path)
271+
272+
def _AppendMultiarchPythonLibDir(path):
273+
if not path or not pythonMultiarchSubdir:
274+
return
275+
276+
multiarchSubdir = pythonMultiarchSubdir.lstrip(os.sep)
277+
normalizedPath = os.path.normpath(path)
278+
if os.path.basename(normalizedPath) == multiarchSubdir:
279+
return
280+
_AppendPythonLibDir(os.path.join(path, multiarchSubdir))
281+
282+
libDir = sysconfig.get_config_var("LIBDIR")
283+
baseLibDir = os.path.join(pythonBaseDir, "lib")
284+
285+
_AppendPythonLibDir(libDir)
286+
_AppendMultiarchPythonLibDir(libDir)
287+
_AppendPythonLibDir(baseLibDir)
288+
_AppendMultiarchPythonLibDir(baseLibDir)
289+
290+
for pythonLibDir in pythonLibDirs:
291+
for pythonLibFilename in _GetPythonLibraryFilenames(context):
292+
candidateLibPath = os.path.join(pythonLibDir, pythonLibFilename)
293+
if os.path.isfile(candidateLibPath):
294+
pythonLibPath = candidateLibPath
266295
break
267-
pythonLibPath = os.path.join(pythonLibDir,
268-
_GetPythonLibraryFilename(context))
269-
if os.path.isfile(pythonLibPath):
296+
if pythonLibPath:
270297
break
271298
elif MacOS():
272299
pythonLibPath = os.path.join(pythonBaseDir, "lib",
273-
_GetPythonLibraryFilename(context))
300+
_GetPythonLibraryFilenames(context)[0])
274301
else:
275302
raise RuntimeError("Platform not supported")
276303

@@ -1766,12 +1793,15 @@ def InstallUSD(context, force, buildArgs):
17661793
# itself rather than rely on CMake's heuristics.
17671794
pythonInfo = GetPythonInfo(context)
17681795
if pythonInfo:
1769-
extraArgs.append('-DPython3_EXECUTABLE="{pyExecPath}"'
1770-
.format(pyExecPath=pythonInfo[0]))
1771-
extraArgs.append('-DPython3_LIBRARY="{pyLibPath}"'
1772-
.format(pyLibPath=pythonInfo[1]))
1773-
extraArgs.append('-DPython3_INCLUDE_DIR="{pyIncPath}"'
1774-
.format(pyIncPath=pythonInfo[2]))
1796+
if pythonInfo[0]:
1797+
extraArgs.append('-DPython3_EXECUTABLE="{pyExecPath}"'
1798+
.format(pyExecPath=pythonInfo[0]))
1799+
if pythonInfo[1]:
1800+
extraArgs.append('-DPython3_LIBRARY="{pyLibPath}"'
1801+
.format(pyLibPath=pythonInfo[1]))
1802+
if pythonInfo[2]:
1803+
extraArgs.append('-DPython3_INCLUDE_DIR="{pyIncPath}"'
1804+
.format(pyIncPath=pythonInfo[2]))
17751805
else:
17761806
extraArgs.append('-DPXR_ENABLE_PYTHON_SUPPORT=OFF')
17771807

packaging/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# OpenUSD Pyproject Packaging
2+
3+
This directory contains a `pyproject.toml` based wheel build flow.
4+
5+
The OpenUSD build is created with `build_scripts/build_usd.py`, and a PEP 517 backend stages files from that install tree into a wheel.
6+
In order to provide robust wheels with proper isolation, ownership of libraries, and dependency links, the wheels are modified.
7+
This approach was chosen over installing all wheels to a common namespace because of fragility with uninstallation, upgrades, versions, and layout conflicts.
8+
The cmake build was not extended to support a wheel native build, so the wheels needs to be fixed up by the backend.
9+
The RPATHs are corrected for the installed layout of the wheels, and platform repair steps still use `auditwheel`, `delocate`, and
10+
`build_scripts/pypi/updatePluginfos.py`.
11+
12+
The backend is designed to be extensible and support more package definitions.
13+
14+
15+
## Packages
16+
17+
The number and content of the packages is easily configurable, the following separation is the current implementation:
18+
19+
- `packaging/usd-core`
20+
- `packaging/usd-imaging`
21+
- `packaging/usd-tools`
22+
- `packaging/usdview`
23+
24+
Each package owns its corresponding shared libraries, bindings, python modules, resources, and metadata.
25+
26+
These packages are staged from an installed OpenUSD tree using explicit manifest globs.
27+
The file ownership boundaries are intentionally easy to revise during review.
28+
29+
On Linux, native shared-library ownership now follows the package dependency graph during repair:
30+
31+
- `usd-core` repairs as a self-contained base wheel
32+
- higher-level wheels exclude libraries already exported by their OpenUSD
33+
dependencies
34+
- the backend patches ELF `NEEDED` entries and RPATHs so later wheels resolve
35+
back into sibling dependency `*.libs` directories after install
36+
37+
That keeps `usd-imaging`, `usd-tools`, and `usdview` from vendoring duplicate
38+
copies of the same OpenUSD shared libraries.
39+
40+
For command-line tools, the wheel keeps the real executable under
41+
`site-packages` and exposes the usual command name through a generated Python
42+
launcher. That avoids relying on `pip` to preserve executable-to-library
43+
relative paths when it installs scripts into the environment's `bin/`
44+
directory.
45+
46+
## Local Build Entry Point
47+
48+
The backend supports two build modes:
49+
50+
- Build OpenUSD directly by invoking `build_scripts/build_usd.py`.
51+
- Reuse a prebuilt install tree by setting `OPENUSD_PREBUILT_INSTALL_ROOT`.
52+
53+
The second mode is intended for CI integration where the install tree is
54+
already produced elsewhere in the workflow.
55+
56+
## Helper Script
57+
58+
`packaging/build_wheels.sh` is a convenience entry point for local development.
59+
It will:
60+
61+
- create a virtual environment,
62+
- install the packaging prerequisites,
63+
- build one shared OpenUSD install tree unless `--reuse-install` is set,
64+
- build one or more wheels into a chosen output directory.
65+
66+
Example usage:
67+
68+
```bash
69+
packaging/build_wheels.sh
70+
packaging/build_wheels.sh --packages usd-core,usd-imaging
71+
packaging/build_wheels.sh --reuse-install --install-root /tmp/openusd-inst
72+
packaging/build_wheels.sh --build-usd-arg --no-tests
73+
```
74+
75+
The helper installs the wheel-building tools it needs, and any necessary pip
76+
packages for OpenUSD. That currently includes `Jinja2` when `usd-tools` is
77+
requested so the schema-generation tools such as `usdGenSchema` and
78+
`usdInitSchema` are built, and the Qt-for-Python dependencies needed for
79+
`usdview`.
80+
81+
On Linux, the repair step uses `auditwheel` and requires `patchelf`. The helper
82+
installs both into the packaging virtual environment when repair is enabled.
83+
84+
## Alternatives
85+
86+
### Common namespace packages
87+
88+
There are a subset of the OpenUSD libraries on pypi as individual packages (pxr-ar, pxr-sdf, etc.).
89+
These all install into a common site-packages/pxr/.libs tree, eliminating the need for wheel repair.
90+
There are downsides, though. Ownership of the files is not managed, uninstallation/upgrade of the packages is brittle,
91+
and there are collision risks.
92+
93+
Because additional wheel build configuration is needed anyways to support the CLI tools because RPATHs will be broken due to default install layouts,
94+
this approach was not used here.
95+
96+
### Single Runtime Package
97+
98+
Similar pypi projects sometimes use a single runtime package with all native code in it, and packages built on top of it are pure python/resources/metadata.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Local PEP 517 backend for OpenUSD packaging."""
2+
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from __future__ import annotations
2+
3+
from string import Template
4+
5+
6+
BINARY_LAUNCHER_MODULE = """from __future__ import annotations
7+
8+
import os
9+
import sys
10+
import sysconfig
11+
from pathlib import Path
12+
13+
14+
def _library_env_key() -> str:
15+
if sys.platform == "win32":
16+
return "PATH"
17+
if sys.platform == "darwin":
18+
return "DYLD_FALLBACK_LIBRARY_PATH"
19+
return "LD_LIBRARY_PATH"
20+
21+
22+
def _packaged_binary(tool_name: str) -> Path:
23+
package_root = Path(__file__).resolve().parent
24+
binary_dir = package_root / "bin"
25+
if sys.platform == "win32":
26+
executable = binary_dir / f"{tool_name}.exe"
27+
if executable.exists():
28+
return executable
29+
return binary_dir / tool_name
30+
31+
32+
def _library_search_dirs(platlib: Path, launcher_package: Path) -> list[str]:
33+
search_dirs = []
34+
if launcher_package.exists():
35+
search_dirs.append(str(launcher_package))
36+
for libs_dir in sorted(platlib.glob("*.libs")):
37+
search_dirs.append(str(libs_dir))
38+
return search_dirs
39+
40+
41+
def main() -> None:
42+
tool_name = Path(sys.argv[0]).name
43+
binary = _packaged_binary(tool_name)
44+
if not binary.exists():
45+
raise SystemExit(f"Could not find packaged OpenUSD tool: {binary}")
46+
47+
env = os.environ.copy()
48+
platlib = Path(sysconfig.get_path("platlib"))
49+
search_dirs = _library_search_dirs(platlib, binary.parent)
50+
env_key = _library_env_key()
51+
if search_dirs:
52+
existing = env.get(env_key)
53+
if existing:
54+
search_dirs.append(existing)
55+
env[env_key] = os.pathsep.join(search_dirs)
56+
57+
os.execvpe(str(binary), [str(binary), *sys.argv[1:]], env)
58+
"""
59+
60+
61+
BUILD_SYSTEM_PYPROJECT = """[build-system]
62+
requires = ["setuptools>=69", "wheel"]
63+
build-backend = "setuptools.build_meta"
64+
"""
65+
66+
67+
SETUP_PY_TEMPLATE = Template(
68+
"""import re
69+
import sysconfig
70+
from pathlib import Path
71+
from setuptools import find_namespace_packages, setup
72+
from setuptools.command.bdist_wheel import bdist_wheel as _bdist_wheel
73+
from setuptools.command.install import install as _install
74+
75+
76+
class openusd_bdist_wheel(_bdist_wheel):
77+
def finalize_options(self):
78+
super().finalize_options()
79+
self.root_is_pure = False
80+
81+
def get_tag(self):
82+
python_tag, abi_tag, platform_tag = super().get_tag()
83+
soabi = sysconfig.get_config_var("SOABI") or ""
84+
match = re.match(r"cpython-(\\d+[a-z]*)", soabi)
85+
if match:
86+
cpython_tag = f"cp{match.group(1)}"
87+
return cpython_tag, cpython_tag, platform_tag
88+
return python_tag, abi_tag, platform_tag
89+
90+
91+
class openusd_install(_install):
92+
def finalize_options(self):
93+
super().finalize_options()
94+
self.install_lib = self.install_platlib
95+
96+
97+
root = Path(__file__).resolve().parent
98+
kwargs = $setup_kwargs_literal
99+
packages = find_namespace_packages(where="staged/lib/python", include=["pxr*"])
100+
for helper_package in $helper_packages_literal:
101+
if helper_package not in packages:
102+
packages.append(helper_package)
103+
kwargs["packages"] = sorted(packages)
104+
kwargs["long_description"] = (root / "README.md").read_text(encoding="utf-8")
105+
kwargs["cmdclass"] = {
106+
"bdist_wheel": openusd_bdist_wheel,
107+
"install": openusd_install,
108+
}
109+
110+
data_files = {}
111+
for path in (root / "staged").rglob("*"):
112+
if not path.is_file():
113+
continue
114+
rel = path.relative_to(root / "staged")
115+
if rel.parts[:2] == ("lib", "python"):
116+
continue
117+
destination = "." if rel.parent == Path(".") else str(rel.parent)
118+
data_files.setdefault(destination, []).append(str(path))
119+
kwargs["data_files"] = sorted((dst, files) for dst, files in data_files.items())
120+
setup(**kwargs)
121+
"""
122+
)

0 commit comments

Comments
 (0)