From 764c73aa36a79e2062ed63db7b24952d5a4e6e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Mon, 12 Jan 2026 11:40:59 -0500 Subject: [PATCH 01/18] test package import python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- cmake/dependencies/python3.cmake | 27 ++++++++++++++++--- ..._python.py => test_python_distribution.py} | 0 2 files changed, 23 insertions(+), 4 deletions(-) rename src/build/{test_python.py => test_python_distribution.py} (100%) diff --git a/cmake/dependencies/python3.cmake b/cmake/dependencies/python3.cmake index 8f8337b78..d76b77ab2 100644 --- a/cmake/dependencies/python3.cmake +++ b/cmake/dependencies/python3.cmake @@ -414,21 +414,40 @@ ADD_CUSTOM_COMMAND( DEPENDS ${_python3_target} ${${_python3_target}-build-deps-flag} ${_requirements_output_file} ${_requirements_input_file} ) -# Test the Python distribution after requirements are installed +# Test Python package imports after requirements are installed. This validates that all pip-installed packages (numpy, opentimelineio, OpenGL, cryptography, +# etc.) can be imported successfully and tests for ABI compatibility issues. Runs in-place using the built Python executable. +SET(${_python3_target}-imports-test-flag + ${_install_dir}/${_python3_target}-imports-test-flag +) + +SET(_test_python_imports_script + "${PROJECT_SOURCE_DIR}/src/build/test_python_imports.py" +) + +ADD_CUSTOM_COMMAND( + COMMENT "Testing Python package imports (build-time validation)" + OUTPUT ${${_python3_target}-imports-test-flag} + COMMAND "${_python3_executable}" "${_test_python_imports_script}" + COMMAND cmake -E touch ${${_python3_target}-imports-test-flag} + DEPENDS ${${_python3_target}-requirements-flag} ${_test_python_imports_script} +) + +# Test the Python distribution's relocatability and environment setup. This moves Python to a temporary location to ensure it works when relocated and validates +# SSL_CERT_FILE setup via sitecustomize.py. Uses system Python to orchestrate the test (since the built Python gets moved during testing). SET(${_python3_target}-test-flag ${_install_dir}/${_python3_target}-test-flag ) SET(_test_python_script - "${PROJECT_SOURCE_DIR}/src/build/test_python.py" + "${PROJECT_SOURCE_DIR}/src/build/test_python_distribution.py" ) ADD_CUSTOM_COMMAND( - COMMENT "Testing Python distribution" + COMMENT "Testing Python distribution relocatability and environment" OUTPUT ${${_python3_target}-test-flag} COMMAND python3 "${_test_python_script}" --python-home "${_install_dir}" --variant "${CMAKE_BUILD_TYPE}" COMMAND cmake -E touch ${${_python3_target}-test-flag} - DEPENDS ${${_python3_target}-requirements-flag} ${_test_python_script} + DEPENDS ${${_python3_target}-imports-test-flag} ${_test_python_script} ) IF(RV_TARGET_WINDOWS diff --git a/src/build/test_python.py b/src/build/test_python_distribution.py similarity index 100% rename from src/build/test_python.py rename to src/build/test_python_distribution.py From 84e49618b81ff290cddfe1347d394d5cc82f470f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Mon, 12 Jan 2026 12:28:38 -0500 Subject: [PATCH 02/18] forgot to add test_python_imports.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/test_python_imports.py | 100 +++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/build/test_python_imports.py diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py new file mode 100644 index 000000000..9772204be --- /dev/null +++ b/src/build/test_python_imports.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ***************************************************************************** +# Copyright 2025 Autodesk, Inc. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +# ***************************************************************************** + +""" +Test script to validate all Python package imports at build time. +This catches issues like missing dependencies, ABI incompatibilities, and +configuration problems (like OpenSSL legacy provider) before runtime. +""" + +import sys +import traceback + +# List of all packages that should be importable +REQUIRED_IMPORTS = [ + # Core packages + ("pip", "Package installer"), + ("setuptools", "Build system"), + ("wheel", "Wheel format support"), + # Scientific/Data packages + ("numpy", "Numerical computing"), + ("opentimelineio", "Editorial timeline"), + # Graphics/OpenGL + ("OpenGL", "PyOpenGL - OpenGL bindings"), + ("OpenGL_accelerate", "PyOpenGL acceleration"), + # SSL/Security + ("certifi", "SSL certificate bundle"), + ("cryptography", "Cryptographic primitives"), + ("cryptography.fernet", "Fernet encryption (requires OpenSSL legacy provider)"), + ("cryptography.hazmat.bindings._rust", "Rust bindings (tests OpenSSL provider loading)"), + # HTTP/Network + ("requests", "HTTP library"), + # Utilities + ("six", "Python 2/3 compatibility"), + ("packaging", "Version parsing"), + ("pydantic", "Data validation"), +] + + +def test_imports(): + """Test that all required packages can be imported.""" + failed_imports = [] + successful_imports = [] + + print("=" * 80) + print("Testing Python package imports at build time") + print("=" * 80) + print() + + for module_name, description in REQUIRED_IMPORTS: + try: + print(f"Testing {module_name:40} ({description})...", end=" ") + __import__(module_name) + print(" OK") + successful_imports.append(module_name) + except Exception as e: + print(" FAILED") + print(f" Error: {type(e).__name__}: {e}") + failed_imports.append((module_name, e)) + # Print full traceback for debugging + if "--verbose" in sys.argv: + print(" Traceback:") + traceback.print_exc(file=sys.stdout) + + print() + print("=" * 80) + print(f"Results: {len(successful_imports)} passed, {len(failed_imports)} failed") + print("=" * 80) + + if failed_imports: + print() + print("FAILED IMPORTS:") + for module_name, error in failed_imports: + print(f" - {module_name}: {type(error).__name__}: {error}") + print() + print("Build-time import test FAILED!") + print("One or more required Python packages could not be imported.") + print() + if any("OpenSSL" in str(e) or "legacy" in str(e).lower() for _, e in failed_imports): + print("NOTE: If you see OpenSSL legacy provider errors:") + print( + " - Rebuild OpenSSL to generate openssl.cnf: ninja -t clean RV_DEPS_OPENSSL && ninja RV_DEPS_OPENSSL" + ) + print(" - Check that OPENSSL_CONF is set in sitecustomize.py") + print() + return 1 + else: + print() + print("All Python package imports successful! ✓") + return 0 + + +if __name__ == "__main__": + sys.exit(test_imports()) From a249f3d3dc222ed61e92bb992fb153c78290b94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Mon, 12 Jan 2026 13:18:33 -0500 Subject: [PATCH 03/18] remove build dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/test_python_imports.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 9772204be..642bc016d 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -21,8 +21,6 @@ REQUIRED_IMPORTS = [ # Core packages ("pip", "Package installer"), - ("setuptools", "Build system"), - ("wheel", "Wheel format support"), # Scientific/Data packages ("numpy", "Numerical computing"), ("opentimelineio", "Editorial timeline"), From fbb62e437506d965a13da46fd8318222e3785a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Mon, 12 Jan 2026 14:30:09 -0500 Subject: [PATCH 04/18] autogenerate the module list to test from requirement.txt.in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/test_python_imports.py | 119 ++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 642bc016d..afa60a5b1 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -12,53 +12,118 @@ Test script to validate all Python package imports at build time. This catches issues like missing dependencies, ABI incompatibilities, and configuration problems (like OpenSSL legacy provider) before runtime. + +Package list is automatically generated from requirements.txt.in to prevent +manual synchronization errors. """ +import os +import re import sys import traceback -# List of all packages that should be importable -REQUIRED_IMPORTS = [ - # Core packages - ("pip", "Package installer"), - # Scientific/Data packages - ("numpy", "Numerical computing"), - ("opentimelineio", "Editorial timeline"), - # Graphics/OpenGL - ("OpenGL", "PyOpenGL - OpenGL bindings"), - ("OpenGL_accelerate", "PyOpenGL acceleration"), - # SSL/Security - ("certifi", "SSL certificate bundle"), - ("cryptography", "Cryptographic primitives"), - ("cryptography.fernet", "Fernet encryption (requires OpenSSL legacy provider)"), - ("cryptography.hazmat.bindings._rust", "Rust bindings (tests OpenSSL provider loading)"), - # HTTP/Network - ("requests", "HTTP library"), - # Utilities - ("six", "Python 2/3 compatibility"), - ("packaging", "Version parsing"), - ("pydantic", "Data validation"), +# Packages to skip from requirements.txt.in (e.g., build dependencies that don't need import testing). +SKIP_IMPORTS = [ + "pip", + "setuptools", + "wheel", +] + +# Additional imports to test beyond what's in requirements.txt.in. +# These test deeper functionality (e.g., sub-modules that verify OpenSSL legacy provider support). +ADDITIONAL_IMPORTS = [ + "cryptography.fernet", + "cryptography.hazmat.bindings._rust", ] +def parse_requirements(file_path): + """Parse requirements.txt.in and return list of package names. + + Extracts package names from lines like: + - numpy==@_numpy_version@ + - pip==24.0 + + Skips comments and empty lines. + """ + packages = [] + with open(file_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + # Skip comments and empty lines + if not line or line.startswith("#"): + continue + # Extract package name (before ==, <, >, etc.) + match = re.match(r"^([A-Za-z0-9_-]+)", line) + if match: + packages.append(match.group(1)) + return packages + + +def try_import(package_name): + """Try importing package, with fallback to stripped 'Py' prefix. + + Handles cases where pip package name differs from import name: + - PyOpenGL -> OpenGL + - PyOpenGL_accelerate -> OpenGL_accelerate + + Raises ImportError if both attempts fail. + """ + try: + return __import__(package_name) + except ImportError: + if package_name.startswith("Py"): + # Try without "Py" prefix + return __import__(package_name[2:]) + raise + + def test_imports(): """Test that all required packages can be imported.""" + # Get requirements.txt.in from same directory as this script + script_dir = os.path.dirname(os.path.abspath(__file__)) + requirements_file = os.path.join(script_dir, "requirements.txt.in") + + if not os.path.exists(requirements_file): + print(f"ERROR: Could not find {requirements_file}") + return 1 + + # Parse package list from requirements.txt.in + packages = parse_requirements(requirements_file) + + # Build list of imports to test (packages from requirements.txt + additional imports) + imports_to_test = [] + skipped_packages = [] + for package in packages: + # Skip build dependencies and other packages that don't need import testing + if package not in SKIP_IMPORTS: + imports_to_test.append((package, "from requirements.txt")) + else: + skipped_packages.append(package) + + # Add any additional imports (e.g., sub-modules for deeper testing) + for additional_import in ADDITIONAL_IMPORTS: + imports_to_test.append((additional_import, "additional import")) + failed_imports = [] successful_imports = [] print("=" * 80) print("Testing Python package imports at build time") + print(f"Source: {requirements_file}") + if skipped_packages: + print(f"Skipping: {', '.join(skipped_packages)}") print("=" * 80) print() - for module_name, description in REQUIRED_IMPORTS: + for module_name, description in imports_to_test: try: - print(f"Testing {module_name:40} ({description})...", end=" ") - __import__(module_name) - print(" OK") + print(f"Testing {module_name:45} ({description})...", end=" ") + try_import(module_name) + print("OK") successful_imports.append(module_name) except Exception as e: - print(" FAILED") + print("FAILED") print(f" Error: {type(e).__name__}: {e}") failed_imports.append((module_name, e)) # Print full traceback for debugging @@ -90,7 +155,7 @@ def test_imports(): return 1 else: print() - print("All Python package imports successful! ✓") + print("All Python package imports successful!") return 0 From 79a851e925a54170550dd01beb62e5f821f24ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Mon, 12 Jan 2026 15:21:42 -0500 Subject: [PATCH 05/18] fix windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/test_python_imports.py | 72 ++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index afa60a5b1..206456ec3 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -18,6 +18,7 @@ """ import os +import platform import re import sys import traceback @@ -80,6 +81,28 @@ def try_import(package_name): def test_imports(): """Test that all required packages can be imported.""" + # On Windows, ensure Python bin directory is on PATH for DLL loading. + # This is required for .pyd extensions (like _opentime.pyd) to find Python DLL and dependencies. + if platform.system() == "Windows": + python_bin = os.path.dirname(sys.executable) + path_env = os.environ.get("PATH", "") + paths_to_add = [] + + if python_bin not in path_env: + paths_to_add.append(python_bin) + + # For Python 3.11+, also check for DLLs directory. + python_root = os.path.dirname(python_bin) + dlls_dir = os.path.join(python_root, "DLLs") + if os.path.exists(dlls_dir) and dlls_dir not in path_env: + paths_to_add.append(dlls_dir) + + if paths_to_add: + # Add paths to the front of PATH. + new_path = os.pathsep.join(paths_to_add) + os.pathsep + path_env + os.environ["PATH"] = new_path + print(f"Added to PATH for DLL loading: {', '.join(paths_to_add)}") + # Get requirements.txt.in from same directory as this script script_dir = os.path.dirname(os.path.abspath(__file__)) requirements_file = os.path.join(script_dir, "requirements.txt.in") @@ -110,9 +133,23 @@ def test_imports(): print("=" * 80) print("Testing Python package imports at build time") + print(f"Python: {sys.version}") + print(f"Platform: {sys.platform}") + print(f"Executable: {sys.executable}") print(f"Source: {requirements_file}") if skipped_packages: print(f"Skipping: {', '.join(skipped_packages)}") + + # On Windows, show PATH info for DLL debugging + if platform.system() == "Windows": + python_bin = os.path.dirname(sys.executable) + print(f"Python bin directory: {python_bin}") + path_env = os.environ.get("PATH", "") + if python_bin in path_env: + print("Python bin is on PATH: Yes") + else: + print("Python bin is on PATH: No (this should not happen - PATH was modified at startup)") + print("=" * 80) print() @@ -126,6 +163,33 @@ def test_imports(): print("FAILED") print(f" Error: {type(e).__name__}: {e}") failed_imports.append((module_name, e)) + + # For opentimelineio failures, provide diagnostics + if "opentimelineio" in module_name.lower(): + print(" Diagnostic info for OpenTimelineIO:") + try: + import site + + site_packages = site.getsitepackages() + print(f" Site-packages: {site_packages}") + + # Check if opentimelineio is installed + for sp in site_packages: + otio_path = os.path.join(sp, "opentimelineio") + if os.path.exists(otio_path): + print(f" Found opentimelineio at: {otio_path}") + # List contents + contents = os.listdir(otio_path) + print(f" Contents: {', '.join(contents[:10])}") + # Check for _opentime + opentime_files = [f for f in contents if "_opentime" in f] + if opentime_files: + print(f" _opentime files: {opentime_files}") + else: + print(" WARNING: No _opentime extension found!") + except Exception as diag_e: + print(f" Could not get diagnostic info: {diag_e}") + # Print full traceback for debugging if "--verbose" in sys.argv: print(" Traceback:") @@ -152,6 +216,14 @@ def test_imports(): ) print(" - Check that OPENSSL_CONF is set in sitecustomize.py") print() + + if any("opentimelineio" in str(m).lower() for m, _ in failed_imports): + print("NOTE: If you see OpenTimelineIO import errors:") + print(" - Check that opentimelineio was built from source (not from wheel)") + print(" - Verify CMAKE_ARGS were passed correctly to pip install") + print(" - On Windows, check that the C++ extension (_opentime.pyd) was built") + print(" - Try rebuilding: ninja -t clean RV_DEPS_PYTHON3 && ninja RV_DEPS_PYTHON3") + print() return 1 else: print() From ed522ea71b74b35c1081928bd8dab8ac3341dce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Tue, 13 Jan 2026 14:02:01 -0500 Subject: [PATCH 06/18] POC - Test OTIO fix in windows debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/test_python_imports.py | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 206456ec3..62d68f8ee 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -17,9 +17,11 @@ manual synchronization errors. """ +import glob import os import platform import re +import shutil import sys import traceback @@ -61,6 +63,43 @@ def parse_requirements(file_path): return packages +def fix_opentimelineio_debug_windows(): + """Fix OpenTimelineIO Debug extension naming on Windows. + + In Debug builds, OTIO creates _opentimed.pyd but Python expects _opentime_d.pyd. + This copies the file to the correct name if needed. + """ + if platform.system() != "Windows" or "_d.exe" not in sys.executable.lower(): + return # Only needed for Windows Debug builds + + try: + import site + + site_packages = site.getsitepackages() + + for sp in site_packages: + otio_path = os.path.join(sp, "opentimelineio") + if not os.path.exists(otio_path): + continue + + # Is there more modules to rename? + + # Look for misnamed Debug extension: _opentimed.*.pyd + misnamed_files = glob.glob(os.path.join(otio_path, "_opentimed*.pyd")) + + for misnamed_file in misnamed_files: + # Create correct name: _opentime_d.*.pyd + correct_name = os.path.basename(misnamed_file).replace("_opentimed", "_opentime_d") + correct_path = os.path.join(otio_path, correct_name) + + if not os.path.exists(correct_path): + shutil.copy2(misnamed_file, correct_path) + print(f"Fixed OTIO Debug extension: {os.path.basename(misnamed_file)} -> {correct_name}") + except Exception as e: + # Don't fail if fix doesn't work, let import fail naturally + print(f"Warning: Could not fix OTIO Debug naming: {e}") + + def try_import(package_name): """Try importing package, with fallback to stripped 'Py' prefix. @@ -70,6 +109,10 @@ def try_import(package_name): Raises ImportError if both attempts fail. """ + # Fix OpenTimelineIO Debug naming issue before importing + if "opentimelineio" in package_name.lower(): + fix_opentimelineio_debug_windows() + try: return __import__(package_name) except ImportError: From c2c183f11869c3530b44b3a1a719c979dac46d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Tue, 13 Jan 2026 15:25:13 -0500 Subject: [PATCH 07/18] POC - Test OTIO fix in windows debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/test_python_imports.py | 37 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 62d68f8ee..d00fa5976 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -82,19 +82,30 @@ def fix_opentimelineio_debug_windows(): if not os.path.exists(otio_path): continue - # Is there more modules to rename? - - # Look for misnamed Debug extension: _opentimed.*.pyd - misnamed_files = glob.glob(os.path.join(otio_path, "_opentimed*.pyd")) - - for misnamed_file in misnamed_files: - # Create correct name: _opentime_d.*.pyd - correct_name = os.path.basename(misnamed_file).replace("_opentimed", "_opentime_d") - correct_path = os.path.join(otio_path, correct_name) - - if not os.path.exists(correct_path): - shutil.copy2(misnamed_file, correct_path) - print(f"Fixed OTIO Debug extension: {os.path.basename(misnamed_file)} -> {correct_name}") + # Look for ALL misnamed Debug extensions that end with 'd' before the Python tag + # Examples: _opentimed.*.pyd, _otiod.*.pyd + # These should be: _opentime_d.*.pyd, _otio_d.*.pyd + all_pyd_files = glob.glob(os.path.join(otio_path, "*.pyd")) + + for pyd_file in all_pyd_files: + basename = os.path.basename(pyd_file) + + # Check if it matches pattern: _d.cp-win_amd64.pyd + # where ends with 'd' but should be _d + if basename.startswith("_") and "d.cp" in basename and "_d.cp" not in basename: + # Extract the module name (between '_' and 'd.cp') + # Example: _opentimed.cp311 -> _opentime + parts = basename.split("d.cp") + if len(parts) == 2: + module_base = parts[0] # e.g., "_opentime" + + # Create correct name with _d suffix + correct_name = f"{module_base}_d.cp{parts[1]}" + correct_path = os.path.join(otio_path, correct_name) + + if not os.path.exists(correct_path): + shutil.copy2(pyd_file, correct_path) + print(f"Fixed OTIO Debug extension: {basename} -> {correct_name}") except Exception as e: # Don't fail if fix doesn't work, let import fail naturally print(f"Warning: Could not fix OTIO Debug naming: {e}") From 38871aeabdc6caecfbc9e3e72c22666bfdabd994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Thu, 15 Jan 2026 10:55:58 -0500 Subject: [PATCH 08/18] try to fix debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/check_pyd_dependencies.py | 260 ++++++++++++++++++++++++++++ src/build/test_python_imports.py | 183 ++++++++++++++++++-- 2 files changed, 430 insertions(+), 13 deletions(-) create mode 100644 src/build/check_pyd_dependencies.py diff --git a/src/build/check_pyd_dependencies.py b/src/build/check_pyd_dependencies.py new file mode 100644 index 000000000..fb74b24f6 --- /dev/null +++ b/src/build/check_pyd_dependencies.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ***************************************************************************** +# Copyright 2025 Autodesk, Inc. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +# ***************************************************************************** + +""" +Check dependencies of Python extension (.pyd) files on Windows. +Uses PowerShell to call dumpbin if available, otherwise provides manual instructions. +""" + +import glob +import os +import subprocess +import sys + + +def find_dumpbin(): + """Find dumpbin.exe in Visual Studio installation.""" + vs_paths = [ + r"C:\Program Files\Microsoft Visual Studio", + r"C:\Program Files (x86)\Microsoft Visual Studio", + ] + + for vs_base in vs_paths: + if not os.path.exists(vs_base): + continue + + try: + for vs_year in os.listdir(vs_base): + vs_year_path = os.path.join(vs_base, vs_year) + if not os.path.isdir(vs_year_path): + continue + + for edition in ["Enterprise", "Professional", "Community", "BuildTools"]: + edition_path = os.path.join(vs_year_path, edition) + if not os.path.exists(edition_path): + continue + + # Look for dumpbin in VC\Tools + vc_tools = os.path.join(edition_path, "VC", "Tools", "MSVC") + if os.path.exists(vc_tools): + try: + msvc_versions = sorted(os.listdir(vc_tools), reverse=True) + for msvc_ver in msvc_versions[:3]: + dumpbin_path = os.path.join(vc_tools, msvc_ver, "bin", "Hostx64", "x64", "dumpbin.exe") + if os.path.exists(dumpbin_path): + return dumpbin_path + except (OSError, PermissionError): + pass + except (OSError, PermissionError): + pass + + return None + + +def check_pyd_dependencies(pyd_path, dumpbin_path=None): + """Check dependencies of a .pyd file. + + Args: + pyd_path: Path to the .pyd file + dumpbin_path: Optional path to dumpbin.exe + + Returns: + List of dependency DLL names, or None if check failed + """ + if not os.path.exists(pyd_path): + print(f"ERROR: File not found: {pyd_path}") + return None + + if dumpbin_path is None: + dumpbin_path = find_dumpbin() + + if not dumpbin_path or not os.path.exists(dumpbin_path): + print("dumpbin.exe not found. Manual check required:") + print(" 1. Open Developer Command Prompt for VS") + print(f' 2. Run: dumpbin /dependents "{pyd_path}"') + return None + + try: + result = subprocess.run([dumpbin_path, "/dependents", pyd_path], capture_output=True, text=True, timeout=30) + + if result.returncode != 0: + print(f"dumpbin failed with return code {result.returncode}") + print(result.stderr) + return None + + # Parse output to extract dependencies + dependencies = [] + in_dependencies_section = False + + for line in result.stdout.split("\n"): + line = line.strip() + + if "Image has the following dependencies:" in line: + in_dependencies_section = True + continue + + if in_dependencies_section: + if line == "Summary" or line == "": + break + + if line.endswith(".dll"): + dependencies.append(line) + + return dependencies + + except subprocess.TimeoutExpired: + print("dumpbin timed out") + return None + except Exception as e: + print(f"Error running dumpbin: {e}") + return None + + +def analyze_otio_dependencies(site_packages_dir): + """Analyze OpenTimelineIO extension dependencies. + + Args: + site_packages_dir: Path to site-packages directory + """ + otio_path = os.path.join(site_packages_dir, "opentimelineio") + + if not os.path.exists(otio_path): + print(f"OpenTimelineIO not found in: {site_packages_dir}") + return + + print("=" * 80) + print("Analyzing OpenTimelineIO Extension Dependencies") + print("=" * 80) + print(f"Location: {otio_path}") + print() + + # Find all .pyd files + pyd_files = glob.glob(os.path.join(otio_path, "*.pyd")) + + if not pyd_files: + print("No .pyd files found!") + return + + print(f"Found {len(pyd_files)} extension(s):") + for pyd in pyd_files: + print(f" - {os.path.basename(pyd)}") + print() + + # Find dumpbin + dumpbin_path = find_dumpbin() + if dumpbin_path: + print(f"Using dumpbin: {dumpbin_path}") + else: + print("WARNING: dumpbin.exe not found - cannot check dependencies automatically") + print() + + # Check each .pyd file + for pyd_file in pyd_files: + basename = os.path.basename(pyd_file) + print("-" * 80) + print(f"Extension: {basename}") + print("-" * 80) + + deps = check_pyd_dependencies(pyd_file, dumpbin_path) + + if deps: + print(f"Dependencies ({len(deps)}):") + + # Categorize dependencies + debug_runtime = [] + release_runtime = [] + python_dlls = [] + system_dlls = [] + other_dlls = [] + + for dep in deps: + dep_lower = dep.lower() + if "d.dll" in dep_lower and ("msvcp" in dep_lower or "vcruntime" in dep_lower): + debug_runtime.append(dep) + elif "msvcp" in dep_lower or "vcruntime" in dep_lower: + release_runtime.append(dep) + elif "python" in dep_lower: + python_dlls.append(dep) + elif "api-ms-win-crt" in dep_lower or "kernel32" in dep_lower: + system_dlls.append(dep) + else: + other_dlls.append(dep) + + if python_dlls: + print(" Python Runtime:") + for dll in python_dlls: + print(f" - {dll}") + + if debug_runtime: + print(" MSVC Debug Runtime:") + for dll in debug_runtime: + print(f" - {dll}") + + if release_runtime: + print(" MSVC Release Runtime:") + for dll in release_runtime: + print(f" - {dll}") + + if other_dlls: + print(" Other:") + for dll in other_dlls: + print(f" - {dll}") + + if system_dlls: + print(" System (Universal CRT + Windows):") + for dll in system_dlls[:5]: + print(f" - {dll}") + if len(system_dlls) > 5: + print(f" ... and {len(system_dlls) - 5} more") + + # Detect Debug/Release mismatch + print() + if "_d.cp" in basename or basename.endswith("_d.pyd"): + print(" This is a Debug extension (name ends with _d)") + if release_runtime and not debug_runtime: + print(" ⚠️ WARNING: Debug extension linking against RELEASE runtime!") + print(" This will cause 'DLL initialization routine failed' errors") + print(" OTIO should link against Debug runtime (msvcp140d.dll, vcruntime140d.dll)") + elif debug_runtime: + print(" ✓ Correctly links against Debug runtime") + else: + print(" This is a Release extension") + if debug_runtime and not release_runtime: + print(" ⚠️ WARNING: Release extension linking against DEBUG runtime!") + elif release_runtime: + print(" ✓ Correctly links against Release runtime") + else: + print(" Could not determine dependencies") + + print() + + +def main(): + """Main function.""" + if len(sys.argv) > 1: + # Check specific directory + site_packages = sys.argv[1] + else: + # Use current Python's site-packages + import site + + site_packages_list = site.getsitepackages() + if site_packages_list: + site_packages = site_packages_list[0] + else: + print("ERROR: Could not find site-packages directory") + return 1 + + analyze_otio_dependencies(site_packages) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 6a9ed5853..82f1d8164 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -22,6 +22,7 @@ import platform import re import shutil +import subprocess import sys import traceback @@ -178,10 +179,134 @@ def try_import(package_name): raise +def find_dumpbin(): + """Find dumpbin.exe in Visual Studio installation.""" + vs_paths = [ + r"C:\Program Files\Microsoft Visual Studio", + r"C:\Program Files (x86)\Microsoft Visual Studio", + ] + + for vs_base in vs_paths: + if not os.path.exists(vs_base): + continue + + try: + for vs_year in os.listdir(vs_base): + vs_year_path = os.path.join(vs_base, vs_year) + if not os.path.isdir(vs_year_path): + continue + + for edition in ["Enterprise", "Professional", "Community", "BuildTools"]: + edition_path = os.path.join(vs_year_path, edition) + if not os.path.exists(edition_path): + continue + + vc_tools = os.path.join(edition_path, "VC", "Tools", "MSVC") + if os.path.exists(vc_tools): + try: + msvc_versions = sorted(os.listdir(vc_tools), reverse=True) + for msvc_ver in msvc_versions[:3]: + dumpbin_path = os.path.join(vc_tools, msvc_ver, "bin", "Hostx64", "x64", "dumpbin.exe") + if os.path.exists(dumpbin_path): + return dumpbin_path + except (OSError, PermissionError): + pass + except (OSError, PermissionError): + pass + + return None + + +def check_pyd_dependencies(pyd_path): + """Check dependencies of a .pyd file using dumpbin. + + Returns list of dependency DLL names, or None if check failed. + """ + if not os.path.exists(pyd_path): + return None + + dumpbin_path = find_dumpbin() + if not dumpbin_path: + return None + + try: + result = subprocess.run([dumpbin_path, "/dependents", pyd_path], capture_output=True, text=True, timeout=10) + + if result.returncode != 0: + return None + + # Parse output to extract dependencies + dependencies = [] + in_dependencies_section = False + + for line in result.stdout.split("\n"): + line = line.strip() + + if "Image has the following dependencies:" in line: + in_dependencies_section = True + continue + + if in_dependencies_section: + if line == "Summary" or line == "": + break + + if line.endswith(".dll"): + dependencies.append(line) + + return dependencies + + except Exception: + return None + + +def check_otio_pyd_dependencies(otio_path): + """Check and report OTIO .pyd dependencies.""" + pyd_files = glob.glob(os.path.join(otio_path, "*_d.cp*.pyd")) + + if not pyd_files: + print(" No Debug .pyd files found") + return + + for pyd_file in pyd_files[:2]: # Check first 2 files to avoid too much output + basename = os.path.basename(pyd_file) + print(f" Checking: {basename}") + + deps = check_pyd_dependencies(pyd_file) + + if deps: + # Categorize dependencies + debug_runtime = [] + release_runtime = [] + python_dlls = [] + + for dep in deps: + dep_lower = dep.lower() + if "d.dll" in dep_lower and ("msvcp" in dep_lower or "vcruntime" in dep_lower): + debug_runtime.append(dep) + elif "msvcp" in dep_lower or "vcruntime" in dep_lower: + release_runtime.append(dep) + elif "python" in dep_lower: + python_dlls.append(dep) + + if python_dlls: + print(f" Python: {', '.join(python_dlls)}") + + if debug_runtime: + print(f" MSVC Debug Runtime: {', '.join(debug_runtime)}") + + if release_runtime: + print(f" ⚠️ MSVC Release Runtime: {', '.join(release_runtime)}") + print(" ERROR: Debug extension should NOT link against Release runtime!") + print(" This causes 'DLL initialization routine failed' errors") + else: + print(" Could not check dependencies (dumpbin not available or failed)") + + def find_msvc_runtime_dlls(): """Find MSVC runtime DLL directories from Visual Studio installation. Returns list of directories containing MSVC runtime DLLs (both Release and Debug). + Searches in both VC\Tools (build tools) and VC\Redist (redistributable packages). """ msvc_dll_dirs = [] @@ -208,7 +333,39 @@ def find_msvc_runtime_dlls(): if not os.path.exists(edition_path): continue - # Look for MSVC runtime in VC\Tools + # 1. Check VC\Redist for Debug runtime DLLs (for Debug builds) + vc_redist = os.path.join(edition_path, "VC", "Redist", "MSVC") + if os.path.exists(vc_redist): + try: + msvc_versions = sorted(os.listdir(vc_redist), reverse=True) + for msvc_ver in msvc_versions[:3]: # Check up to 3 latest versions + # Check Debug redistributables + debug_redist_paths = [ + os.path.join( + vc_redist, msvc_ver, "debug_nonredist", "x64", "Microsoft.VC143.DebugCRT" + ), + os.path.join( + vc_redist, + msvc_ver, + "onecore", + "debug_nonredist", + "x64", + "Microsoft.VC143.DebugCRT", + ), + ] + for debug_path in debug_redist_paths: + if os.path.exists(debug_path): + # Check for Debug runtime DLLs + if glob.glob(os.path.join(debug_path, "*140d.dll")): + if debug_path not in msvc_dll_dirs: + msvc_dll_dirs.append(debug_path) + break + if msvc_dll_dirs: + break + except (OSError, PermissionError): + pass + + # 2. Check VC\Tools for runtime DLLs (fallback for Release or if Redist not found) vc_tools = os.path.join(edition_path, "VC", "Tools", "MSVC") if os.path.exists(vc_tools): try: @@ -229,7 +386,8 @@ def find_msvc_runtime_dlls(): if bin_path not in msvc_dll_dirs: msvc_dll_dirs.append(bin_path) break - if msvc_dll_dirs: + if msvc_dll_dirs and len(msvc_dll_dirs) >= 2: + # Found both Redist and Tools, that's enough break except (OSError, PermissionError): pass @@ -398,16 +556,11 @@ def test_imports(): else: print(" No DLL files in OTIO directory") - # On Windows Debug, check if this is a DLL dependency issue - if platform.system() == "Windows" and "_d.exe" in sys.executable.lower(): - if "DLL load failed" in str(e): - print(" ") - print(" This is a DLL dependency issue in Debug build.") - print(" The .pyd extension exists but can't load its dependencies.") - print(" Possible causes:") - print(" - Missing Debug C++ runtime DLLs (msvcp140d.dll, vcruntime140d.dll)") - print(" - OTIO built with different compiler/runtime than Python") - print(" - Missing OTIO library DLLs") + # On Windows, check DLL dependencies + if platform.system() == "Windows" and "DLL load failed" in str(e): + print() + print(" Checking .pyd dependencies...") + check_otio_pyd_dependencies(otio_path) except Exception as diag_e: print(f" Could not get diagnostic info: {diag_e}") @@ -443,9 +596,13 @@ def test_imports(): print("NOTE: If you see OpenTimelineIO import errors:") print(" - Check that opentimelineio was built from source (not from wheel)") print(" - Verify CMAKE_ARGS were passed correctly to pip install") - print(" - On Windows, check that the C++ extension (_opentime.pyd) was built") + print(" - On Windows Debug builds: OTIO may link against Release runtime (msvcp140.dll)") + print(" instead of Debug runtime (msvcp140d.dll), causing initialization failures") print(" - Try rebuilding: ninja -t clean RV_DEPS_PYTHON3 && ninja RV_DEPS_PYTHON3") print() + print("To diagnose DLL dependencies, run:") + print(f" python {os.path.join(script_dir, 'check_pyd_dependencies.py')}") + print() return 1 else: print() From 0fcf46b7befce7b4451bac3025e53b084b00159b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Thu, 15 Jan 2026 20:44:55 -0500 Subject: [PATCH 09/18] fix otio with patch for python debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- cmake/dependencies/python3.cmake | 29 ++++- src/build/check_pyd_dependencies.py | 34 +++++- src/build/patch_opentimelineio_debug.py | 143 ++++++++++++++++++++++++ src/build/test_python_imports.py | 26 ++++- 4 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 src/build/patch_opentimelineio_debug.py diff --git a/cmake/dependencies/python3.cmake b/cmake/dependencies/python3.cmake index d76b77ab2..5f503b3f7 100644 --- a/cmake/dependencies/python3.cmake +++ b/cmake/dependencies/python3.cmake @@ -323,7 +323,12 @@ ENDIF() LIST( APPEND _requirements_install_command - "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable}" + # Force downstream CMake projects (notably OpenTimelineIO + pybind11) to use the *same* custom-built Python. + # OTIO depends on pybind11 and can use different FindPython variants depending on version/environment: + # - legacy: FindPythonInterp/FindPythonLibsNew via PYTHON_* variables + # - modern: FindPython / FindPython3 via Python*_EXECUTABLE / Python*_INCLUDE_DIR / Python*_LIBRARY + # Passing all of them avoids Debug/Release mismatches on Windows. + "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable} -DPython3_ROOT_DIR=${_install_dir} -DPython3_EXECUTABLE=${_python3_executable} -DPython3_INCLUDE_DIR=${_include_dir} -DPython3_LIBRARY=${_python3_cmake_library} -DPython_ROOT_DIR=${_install_dir} -DPython_EXECUTABLE=${_python3_executable} -DPython_INCLUDE_DIR=${_include_dir} -DPython_LIBRARY=${_python3_cmake_library}" "${_python3_executable}" -s -E @@ -342,6 +347,26 @@ LIST( "${_requirements_output_file}" ) +# Patch OpenTimelineIO for Windows Debug pybind11 GIL assertion and ensure debug naming. +SET(_otio_patch_script + "${PROJECT_SOURCE_DIR}/src/build/patch_opentimelineio_debug.py" +) +SET(${_python3_target}-otio-patch-flag + ${_install_dir}/${_python3_target}-otio-patch-flag +) + +ADD_CUSTOM_COMMAND( + COMMENT "Patching OpenTimelineIO for Debug Python (lazy GIL init + debug pyd naming)" + OUTPUT ${${_python3_target}-otio-patch-flag} + COMMAND "${_python3_executable}" "${_otio_patch_script}" + --python-exe "${_python3_executable}" + --otio-version "${_opentimelineio_version}" + --site-packages "${_install_dir}/Lib/site-packages" + --cmake-args "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable} -DPython3_ROOT_DIR=${_install_dir} -DPython3_EXECUTABLE=${_python3_executable} -DPython3_INCLUDE_DIR=${_include_dir} -DPython3_LIBRARY=${_python3_cmake_library} -DPython_ROOT_DIR=${_install_dir} -DPython_EXECUTABLE=${_python3_executable} -DPython_INCLUDE_DIR=${_include_dir} -DPython_LIBRARY=${_python3_cmake_library}" + COMMAND cmake -E touch ${${_python3_target}-otio-patch-flag} + DEPENDS ${${_python3_target}-requirements-flag} ${_otio_patch_script} +) + IF(RV_TARGET_WINDOWS) SET(_patch_python_command "patch -p1 < ${CMAKE_CURRENT_SOURCE_DIR}/patch/python-${RV_DEPS_PYTHON_VERSION}/python.${RV_DEPS_PYTHON_VERSION}.openssl.props.patch &&\ @@ -429,7 +454,7 @@ ADD_CUSTOM_COMMAND( OUTPUT ${${_python3_target}-imports-test-flag} COMMAND "${_python3_executable}" "${_test_python_imports_script}" COMMAND cmake -E touch ${${_python3_target}-imports-test-flag} - DEPENDS ${${_python3_target}-requirements-flag} ${_test_python_imports_script} + DEPENDS ${${_python3_target}-requirements-flag} ${${_python3_target}-otio-patch-flag} ${_test_python_imports_script} ) # Test the Python distribution's relocatability and environment setup. This moves Python to a temporary location to ensure it works when relocated and validates diff --git a/src/build/check_pyd_dependencies.py b/src/build/check_pyd_dependencies.py index fb74b24f6..9ff21dfe9 100644 --- a/src/build/check_pyd_dependencies.py +++ b/src/build/check_pyd_dependencies.py @@ -15,12 +15,21 @@ import glob import os +import shutil import subprocess import sys def find_dumpbin(): """Find dumpbin.exe in Visual Studio installation.""" + # Prefer PATH first (CI/build environments may pre-configure this). + try: + found = shutil.which("dumpbin.exe") + if found and os.path.exists(found): + return found + except Exception: + pass + vs_paths = [ r"C:\Program Files\Microsoft Visual Studio", r"C:\Program Files (x86)\Microsoft Visual Studio", @@ -82,11 +91,21 @@ def check_pyd_dependencies(pyd_path, dumpbin_path=None): return None try: - result = subprocess.run([dumpbin_path, "/dependents", pyd_path], capture_output=True, text=True, timeout=30) + result = subprocess.run( + [dumpbin_path, "/dependents", pyd_path], + capture_output=True, + text=True, + timeout=120, + ) if result.returncode != 0: print(f"dumpbin failed with return code {result.returncode}") - print(result.stderr) + if result.stdout: + print("--- dumpbin stdout ---") + print(result.stdout) + if result.stderr: + print("--- dumpbin stderr ---") + print(result.stderr) return None # Parse output to extract dependencies @@ -101,10 +120,17 @@ def check_pyd_dependencies(pyd_path, dumpbin_path=None): continue if in_dependencies_section: - if line == "Summary" or line == "": + # dumpbin prints "Summary" with indentation; treat it whitespace-insensitively. + if line.strip() == "Summary": break - if line.endswith(".dll"): + # Blank lines appear inside the dependency block; ignore them. + if line == "": + continue + + # dumpbin prints DLL names in uppercase (e.g. KERNEL32.dll). + # Be case-insensitive here. + if line.lower().endswith(".dll"): dependencies.append(line) return dependencies diff --git a/src/build/patch_opentimelineio_debug.py b/src/build/patch_opentimelineio_debug.py new file mode 100644 index 000000000..aa9fe652f --- /dev/null +++ b/src/build/patch_opentimelineio_debug.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +""" +Patch OpenTimelineIO debug build to avoid pybind11 GIL assertions and ensure debug +module naming on Windows. + +Actions: +- Download OTIO source for the requested version (sdist only). +- Patch otio_utils.cpp to lazily initialize _value_to_any (fixes GIL assert). +- Build & install OTIO from the patched source using the provided Python. +- On Windows Debug, copy _otio/_opentime pyds to *_d names if missing. +""" + +from __future__ import annotations + +import argparse +import glob +import os +import shutil +import subprocess +import tempfile +from pathlib import Path + + +def _run(cmd: list[str], env: dict[str, str]) -> None: + subprocess.check_call(cmd, env=env) + + +def _patch_otio_utils(root: Path) -> None: + target = next( + root.glob("**/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp") + ) + text = target.read_text(encoding="utf-8") + # Make _value_to_any lazy-initialized to avoid pybind11 GIL asserts at static init. + if "static py::object _value_to_any = py::none();" not in text: + raise RuntimeError("Expected marker not found in otio_utils.cpp") + text = text.replace( + "static py::object _value_to_any = py::none();", + "// Initialized lazily after the interpreter is ready; constructing py::none()\n" + "// at static init time triggers pybind11 GIL assertions in Debug builds.\n" + "static py::object _value_to_any;", + 1, + ) + # Relax the guard to handle default-constructed (null) py::object + text = text.replace( + "if (_value_to_any.is_none()) {", + "if (!_value_to_any || _value_to_any.is_none()) {", + 1, + ) + target.write_text(text, encoding="utf-8") + + +def _maybe_copy_debug_names(site_packages: Path) -> None: + otio_path = site_packages / "opentimelineio" + if not otio_path.exists(): + return + + for base in ("_otio", "_opentime"): + for pyd in otio_path.glob(f"{base}*.pyd"): + name = pyd.name + if "_d.cp" in name: + continue + if "d.cp" in name: + # already has a trailing d before cp tag + continue + if ".cp" in name: + corrected = name.replace(".cp", "_d.cp", 1) + else: + corrected = name.replace(".pyd", "_d.pyd") + dest = pyd.with_name(corrected) + if not dest.exists(): + shutil.copy2(pyd, dest) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--python-exe", required=True, help="Path to python_d.exe") + ap.add_argument("--otio-version", required=True, help="OTIO version to patch") + ap.add_argument("--site-packages", required=True, help="Target site-packages path") + ap.add_argument( + "--cmake-args", + required=True, + help="CMAKE_ARGS string to ensure Python debug libs are used", + ) + args = ap.parse_args() + + py = args.python_exe + version = args.otio_version + sp = Path(args.site_packages).resolve() + + env = os.environ.copy() + env["CMAKE_ARGS"] = args.cmake_args + # Work around rare pip/pyproject_hooks KeyError on _PYPROJECT_HOOKS_BUILD_BACKEND + # by providing a default backend name when unset. + env.setdefault("_PYPROJECT_HOOKS_BUILD_BACKEND", "setuptools.build_meta") + + with tempfile.TemporaryDirectory() as td: + td_path = Path(td) + _run( + [ + py, + "-m", + "pip", + "download", + f"opentimelineio=={version}", + "--no-binary", + ":all:", + "-d", + str(td_path), + ], + env, + ) + sdist = next(td_path.glob("opentimelineio-*.tar.gz")) + extract_dir = td_path / "src" + extract_dir.mkdir() + _run(["python", "-m", "tarfile", "-e", str(sdist), str(extract_dir)], env) + + # tarfile via python -m tarfile doesn't support -e; fall back to shutil + if not any(extract_dir.iterdir()): + shutil.unpack_archive(str(sdist), extract_dir) + + root = next(extract_dir.glob("opentimelineio-*")) + _patch_otio_utils(root) + + _run( + [ + py, + "-m", + "pip", + "install", + "--no-cache-dir", + "--force-reinstall", + "--no-build-isolation", + str(root), + ], + env, + ) + + _maybe_copy_debug_names(sp) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 82f1d8164..52f0e0239 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -181,6 +181,14 @@ def try_import(package_name): def find_dumpbin(): """Find dumpbin.exe in Visual Studio installation.""" + # First, prefer PATH (build systems often add dumpbin's directory to PATH). + try: + found = shutil.which("dumpbin.exe") + if found and os.path.exists(found): + return found + except Exception: + pass + vs_paths = [ r"C:\Program Files\Microsoft Visual Studio", r"C:\Program Files (x86)\Microsoft Visual Studio", @@ -230,9 +238,22 @@ def check_pyd_dependencies(pyd_path): return None try: - result = subprocess.run([dumpbin_path, "/dependents", pyd_path], capture_output=True, text=True, timeout=10) + result = subprocess.run( + [dumpbin_path, "/dependents", pyd_path], + capture_output=True, + text=True, + timeout=60, + ) if result.returncode != 0: + # Keep the import test resilient, but include enough info for diagnosis. + sys.stderr.write(f"dumpbin failed (code={result.returncode}) for: {pyd_path}\n") + if result.stdout: + sys.stderr.write("--- dumpbin stdout ---\n") + sys.stderr.write(result.stdout + "\n") + if result.stderr: + sys.stderr.write("--- dumpbin stderr ---\n") + sys.stderr.write(result.stderr + "\n") return None # Parse output to extract dependencies @@ -255,7 +276,8 @@ def check_pyd_dependencies(pyd_path): return dependencies - except Exception: + except Exception as e: + sys.stderr.write(f"dumpbin invocation error for {pyd_path}: {e}\n") return None From 395d5dcf07d00d85a625f7aacddb696646afcb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Fri, 16 Jan 2026 08:23:10 -0500 Subject: [PATCH 10/18] patch OTIO for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/check_pyd_dependencies.py | 286 ---------------------------- src/build/test_python_imports.py | 193 +------------------ 2 files changed, 1 insertion(+), 478 deletions(-) delete mode 100644 src/build/check_pyd_dependencies.py diff --git a/src/build/check_pyd_dependencies.py b/src/build/check_pyd_dependencies.py deleted file mode 100644 index 9ff21dfe9..000000000 --- a/src/build/check_pyd_dependencies.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# ***************************************************************************** -# Copyright 2025 Autodesk, Inc. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# -# ***************************************************************************** - -""" -Check dependencies of Python extension (.pyd) files on Windows. -Uses PowerShell to call dumpbin if available, otherwise provides manual instructions. -""" - -import glob -import os -import shutil -import subprocess -import sys - - -def find_dumpbin(): - """Find dumpbin.exe in Visual Studio installation.""" - # Prefer PATH first (CI/build environments may pre-configure this). - try: - found = shutil.which("dumpbin.exe") - if found and os.path.exists(found): - return found - except Exception: - pass - - vs_paths = [ - r"C:\Program Files\Microsoft Visual Studio", - r"C:\Program Files (x86)\Microsoft Visual Studio", - ] - - for vs_base in vs_paths: - if not os.path.exists(vs_base): - continue - - try: - for vs_year in os.listdir(vs_base): - vs_year_path = os.path.join(vs_base, vs_year) - if not os.path.isdir(vs_year_path): - continue - - for edition in ["Enterprise", "Professional", "Community", "BuildTools"]: - edition_path = os.path.join(vs_year_path, edition) - if not os.path.exists(edition_path): - continue - - # Look for dumpbin in VC\Tools - vc_tools = os.path.join(edition_path, "VC", "Tools", "MSVC") - if os.path.exists(vc_tools): - try: - msvc_versions = sorted(os.listdir(vc_tools), reverse=True) - for msvc_ver in msvc_versions[:3]: - dumpbin_path = os.path.join(vc_tools, msvc_ver, "bin", "Hostx64", "x64", "dumpbin.exe") - if os.path.exists(dumpbin_path): - return dumpbin_path - except (OSError, PermissionError): - pass - except (OSError, PermissionError): - pass - - return None - - -def check_pyd_dependencies(pyd_path, dumpbin_path=None): - """Check dependencies of a .pyd file. - - Args: - pyd_path: Path to the .pyd file - dumpbin_path: Optional path to dumpbin.exe - - Returns: - List of dependency DLL names, or None if check failed - """ - if not os.path.exists(pyd_path): - print(f"ERROR: File not found: {pyd_path}") - return None - - if dumpbin_path is None: - dumpbin_path = find_dumpbin() - - if not dumpbin_path or not os.path.exists(dumpbin_path): - print("dumpbin.exe not found. Manual check required:") - print(" 1. Open Developer Command Prompt for VS") - print(f' 2. Run: dumpbin /dependents "{pyd_path}"') - return None - - try: - result = subprocess.run( - [dumpbin_path, "/dependents", pyd_path], - capture_output=True, - text=True, - timeout=120, - ) - - if result.returncode != 0: - print(f"dumpbin failed with return code {result.returncode}") - if result.stdout: - print("--- dumpbin stdout ---") - print(result.stdout) - if result.stderr: - print("--- dumpbin stderr ---") - print(result.stderr) - return None - - # Parse output to extract dependencies - dependencies = [] - in_dependencies_section = False - - for line in result.stdout.split("\n"): - line = line.strip() - - if "Image has the following dependencies:" in line: - in_dependencies_section = True - continue - - if in_dependencies_section: - # dumpbin prints "Summary" with indentation; treat it whitespace-insensitively. - if line.strip() == "Summary": - break - - # Blank lines appear inside the dependency block; ignore them. - if line == "": - continue - - # dumpbin prints DLL names in uppercase (e.g. KERNEL32.dll). - # Be case-insensitive here. - if line.lower().endswith(".dll"): - dependencies.append(line) - - return dependencies - - except subprocess.TimeoutExpired: - print("dumpbin timed out") - return None - except Exception as e: - print(f"Error running dumpbin: {e}") - return None - - -def analyze_otio_dependencies(site_packages_dir): - """Analyze OpenTimelineIO extension dependencies. - - Args: - site_packages_dir: Path to site-packages directory - """ - otio_path = os.path.join(site_packages_dir, "opentimelineio") - - if not os.path.exists(otio_path): - print(f"OpenTimelineIO not found in: {site_packages_dir}") - return - - print("=" * 80) - print("Analyzing OpenTimelineIO Extension Dependencies") - print("=" * 80) - print(f"Location: {otio_path}") - print() - - # Find all .pyd files - pyd_files = glob.glob(os.path.join(otio_path, "*.pyd")) - - if not pyd_files: - print("No .pyd files found!") - return - - print(f"Found {len(pyd_files)} extension(s):") - for pyd in pyd_files: - print(f" - {os.path.basename(pyd)}") - print() - - # Find dumpbin - dumpbin_path = find_dumpbin() - if dumpbin_path: - print(f"Using dumpbin: {dumpbin_path}") - else: - print("WARNING: dumpbin.exe not found - cannot check dependencies automatically") - print() - - # Check each .pyd file - for pyd_file in pyd_files: - basename = os.path.basename(pyd_file) - print("-" * 80) - print(f"Extension: {basename}") - print("-" * 80) - - deps = check_pyd_dependencies(pyd_file, dumpbin_path) - - if deps: - print(f"Dependencies ({len(deps)}):") - - # Categorize dependencies - debug_runtime = [] - release_runtime = [] - python_dlls = [] - system_dlls = [] - other_dlls = [] - - for dep in deps: - dep_lower = dep.lower() - if "d.dll" in dep_lower and ("msvcp" in dep_lower or "vcruntime" in dep_lower): - debug_runtime.append(dep) - elif "msvcp" in dep_lower or "vcruntime" in dep_lower: - release_runtime.append(dep) - elif "python" in dep_lower: - python_dlls.append(dep) - elif "api-ms-win-crt" in dep_lower or "kernel32" in dep_lower: - system_dlls.append(dep) - else: - other_dlls.append(dep) - - if python_dlls: - print(" Python Runtime:") - for dll in python_dlls: - print(f" - {dll}") - - if debug_runtime: - print(" MSVC Debug Runtime:") - for dll in debug_runtime: - print(f" - {dll}") - - if release_runtime: - print(" MSVC Release Runtime:") - for dll in release_runtime: - print(f" - {dll}") - - if other_dlls: - print(" Other:") - for dll in other_dlls: - print(f" - {dll}") - - if system_dlls: - print(" System (Universal CRT + Windows):") - for dll in system_dlls[:5]: - print(f" - {dll}") - if len(system_dlls) > 5: - print(f" ... and {len(system_dlls) - 5} more") - - # Detect Debug/Release mismatch - print() - if "_d.cp" in basename or basename.endswith("_d.pyd"): - print(" This is a Debug extension (name ends with _d)") - if release_runtime and not debug_runtime: - print(" ⚠️ WARNING: Debug extension linking against RELEASE runtime!") - print(" This will cause 'DLL initialization routine failed' errors") - print(" OTIO should link against Debug runtime (msvcp140d.dll, vcruntime140d.dll)") - elif debug_runtime: - print(" ✓ Correctly links against Debug runtime") - else: - print(" This is a Release extension") - if debug_runtime and not release_runtime: - print(" ⚠️ WARNING: Release extension linking against DEBUG runtime!") - elif release_runtime: - print(" ✓ Correctly links against Release runtime") - else: - print(" Could not determine dependencies") - - print() - - -def main(): - """Main function.""" - if len(sys.argv) > 1: - # Check specific directory - site_packages = sys.argv[1] - else: - # Use current Python's site-packages - import site - - site_packages_list = site.getsitepackages() - if site_packages_list: - site_packages = site_packages_list[0] - else: - print("ERROR: Could not find site-packages directory") - return 1 - - analyze_otio_dependencies(site_packages) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 52f0e0239..d87170c12 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -22,9 +22,7 @@ import platform import re import shutil -import subprocess import sys -import traceback # Packages to skip from requirements.txt.in (e.g., build dependencies that don't need import testing). SKIP_IMPORTS = [ @@ -179,151 +177,6 @@ def try_import(package_name): raise -def find_dumpbin(): - """Find dumpbin.exe in Visual Studio installation.""" - # First, prefer PATH (build systems often add dumpbin's directory to PATH). - try: - found = shutil.which("dumpbin.exe") - if found and os.path.exists(found): - return found - except Exception: - pass - - vs_paths = [ - r"C:\Program Files\Microsoft Visual Studio", - r"C:\Program Files (x86)\Microsoft Visual Studio", - ] - - for vs_base in vs_paths: - if not os.path.exists(vs_base): - continue - - try: - for vs_year in os.listdir(vs_base): - vs_year_path = os.path.join(vs_base, vs_year) - if not os.path.isdir(vs_year_path): - continue - - for edition in ["Enterprise", "Professional", "Community", "BuildTools"]: - edition_path = os.path.join(vs_year_path, edition) - if not os.path.exists(edition_path): - continue - - vc_tools = os.path.join(edition_path, "VC", "Tools", "MSVC") - if os.path.exists(vc_tools): - try: - msvc_versions = sorted(os.listdir(vc_tools), reverse=True) - for msvc_ver in msvc_versions[:3]: - dumpbin_path = os.path.join(vc_tools, msvc_ver, "bin", "Hostx64", "x64", "dumpbin.exe") - if os.path.exists(dumpbin_path): - return dumpbin_path - except (OSError, PermissionError): - pass - except (OSError, PermissionError): - pass - - return None - - -def check_pyd_dependencies(pyd_path): - """Check dependencies of a .pyd file using dumpbin. - - Returns list of dependency DLL names, or None if check failed. - """ - if not os.path.exists(pyd_path): - return None - - dumpbin_path = find_dumpbin() - if not dumpbin_path: - return None - - try: - result = subprocess.run( - [dumpbin_path, "/dependents", pyd_path], - capture_output=True, - text=True, - timeout=60, - ) - - if result.returncode != 0: - # Keep the import test resilient, but include enough info for diagnosis. - sys.stderr.write(f"dumpbin failed (code={result.returncode}) for: {pyd_path}\n") - if result.stdout: - sys.stderr.write("--- dumpbin stdout ---\n") - sys.stderr.write(result.stdout + "\n") - if result.stderr: - sys.stderr.write("--- dumpbin stderr ---\n") - sys.stderr.write(result.stderr + "\n") - return None - - # Parse output to extract dependencies - dependencies = [] - in_dependencies_section = False - - for line in result.stdout.split("\n"): - line = line.strip() - - if "Image has the following dependencies:" in line: - in_dependencies_section = True - continue - - if in_dependencies_section: - if line == "Summary" or line == "": - break - - if line.endswith(".dll"): - dependencies.append(line) - - return dependencies - - except Exception as e: - sys.stderr.write(f"dumpbin invocation error for {pyd_path}: {e}\n") - return None - - -def check_otio_pyd_dependencies(otio_path): - """Check and report OTIO .pyd dependencies.""" - pyd_files = glob.glob(os.path.join(otio_path, "*_d.cp*.pyd")) - - if not pyd_files: - print(" No Debug .pyd files found") - return - - for pyd_file in pyd_files[:2]: # Check first 2 files to avoid too much output - basename = os.path.basename(pyd_file) - print(f" Checking: {basename}") - - deps = check_pyd_dependencies(pyd_file) - - if deps: - # Categorize dependencies - debug_runtime = [] - release_runtime = [] - python_dlls = [] - - for dep in deps: - dep_lower = dep.lower() - if "d.dll" in dep_lower and ("msvcp" in dep_lower or "vcruntime" in dep_lower): - debug_runtime.append(dep) - elif "msvcp" in dep_lower or "vcruntime" in dep_lower: - release_runtime.append(dep) - elif "python" in dep_lower: - python_dlls.append(dep) - - if python_dlls: - print(f" Python: {', '.join(python_dlls)}") - - if debug_runtime: - print(f" MSVC Debug Runtime: {', '.join(debug_runtime)}") - - if release_runtime: - print(f" ⚠️ MSVC Release Runtime: {', '.join(release_runtime)}") - print(" ERROR: Debug extension should NOT link against Release runtime!") - print(" This causes 'DLL initialization routine failed' errors") - else: - print(" Could not check dependencies (dumpbin not available or failed)") - - def find_msvc_runtime_dlls(): """Find MSVC runtime DLL directories from Visual Studio installation. @@ -546,51 +399,7 @@ def test_imports(): print(f" Error: {type(e).__name__}: {e}") failed_imports.append((module_name, e)) - # For opentimelineio failures, provide diagnostics - if "opentimelineio" in module_name.lower(): - print(" Diagnostic info for OpenTimelineIO:") - try: - import site - - site_packages = site.getsitepackages() - print(f" Site-packages: {site_packages}") - - # Check if opentimelineio is installed - for sp in site_packages: - otio_path = os.path.join(sp, "opentimelineio") - if os.path.exists(otio_path): - print(f" Found opentimelineio at: {otio_path}") - # List contents - contents = os.listdir(otio_path) - print(f" Contents: {', '.join(contents[:10])}") - - # Check for extensions - opentime_files = [f for f in contents if "_opentime" in f or "_otio" in f] - if opentime_files: - print(f" Extensions found: {opentime_files}") - else: - print(" WARNING: No OTIO extensions found!") - - # Check for DLL files - dll_files = glob.glob(os.path.join(otio_path, "*.dll")) - if dll_files: - print(f" DLL files: {[os.path.basename(d) for d in dll_files]}") - else: - print(" No DLL files in OTIO directory") - - # On Windows, check DLL dependencies - if platform.system() == "Windows" and "DLL load failed" in str(e): - print() - print(" Checking .pyd dependencies...") - check_otio_pyd_dependencies(otio_path) - - except Exception as diag_e: - print(f" Could not get diagnostic info: {diag_e}") - - # Print full traceback for debugging - if "--verbose" in sys.argv: - print(" Traceback:") - traceback.print_exc(file=sys.stdout) + # Keep failure output minimal; detailed diagnostics removed. print() print("=" * 80) From 1826cf102388c6169f5c7cbaee8be13f66d725ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Fri, 16 Jan 2026 08:43:42 -0500 Subject: [PATCH 11/18] tweak, clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- cmake/dependencies/python3.cmake | 45 ++++++++++++------------- src/build/patch_opentimelineio_debug.py | 18 ++++++---- src/build/test_python_imports.py | 2 +- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/cmake/dependencies/python3.cmake b/cmake/dependencies/python3.cmake index 5f503b3f7..a24b29187 100644 --- a/cmake/dependencies/python3.cmake +++ b/cmake/dependencies/python3.cmake @@ -323,11 +323,9 @@ ENDIF() LIST( APPEND _requirements_install_command - # Force downstream CMake projects (notably OpenTimelineIO + pybind11) to use the *same* custom-built Python. - # OTIO depends on pybind11 and can use different FindPython variants depending on version/environment: - # - legacy: FindPythonInterp/FindPythonLibsNew via PYTHON_* variables - # - modern: FindPython / FindPython3 via Python*_EXECUTABLE / Python*_INCLUDE_DIR / Python*_LIBRARY - # Passing all of them avoids Debug/Release mismatches on Windows. + # Force downstream CMake projects (notably OpenTimelineIO + pybind11) to use the *same* custom-built Python. OTIO depends on pybind11 and can use different + # FindPython variants depending on version/environment: - legacy: FindPythonInterp/FindPythonLibsNew via PYTHON_* variables - modern: FindPython / FindPython3 + # via Python*_EXECUTABLE / Python*_INCLUDE_DIR / Python*_LIBRARY Passing all of them avoids Debug/Release mismatches on Windows. "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable} -DPython3_ROOT_DIR=${_install_dir} -DPython3_EXECUTABLE=${_python3_executable} -DPython3_INCLUDE_DIR=${_include_dir} -DPython3_LIBRARY=${_python3_cmake_library} -DPython_ROOT_DIR=${_install_dir} -DPython_EXECUTABLE=${_python3_executable} -DPython_INCLUDE_DIR=${_include_dir} -DPython_LIBRARY=${_python3_cmake_library}" "${_python3_executable}" -s @@ -347,25 +345,26 @@ LIST( "${_requirements_output_file}" ) -# Patch OpenTimelineIO for Windows Debug pybind11 GIL assertion and ensure debug naming. -SET(_otio_patch_script - "${PROJECT_SOURCE_DIR}/src/build/patch_opentimelineio_debug.py" -) -SET(${_python3_target}-otio-patch-flag - ${_install_dir}/${_python3_target}-otio-patch-flag -) +IF(RV_TARGET_WINDOWS) + # Patch OpenTimelineIO for Windows Debug pybind11 GIL assertion and ensure debug naming. + SET(_otio_patch_script + "${PROJECT_SOURCE_DIR}/src/build/patch_opentimelineio_debug.py" + ) + SET(${_python3_target}-otio-patch-flag + ${_install_dir}/${_python3_target}-otio-patch-flag + ) -ADD_CUSTOM_COMMAND( - COMMENT "Patching OpenTimelineIO for Debug Python (lazy GIL init + debug pyd naming)" - OUTPUT ${${_python3_target}-otio-patch-flag} - COMMAND "${_python3_executable}" "${_otio_patch_script}" - --python-exe "${_python3_executable}" - --otio-version "${_opentimelineio_version}" - --site-packages "${_install_dir}/Lib/site-packages" - --cmake-args "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable} -DPython3_ROOT_DIR=${_install_dir} -DPython3_EXECUTABLE=${_python3_executable} -DPython3_INCLUDE_DIR=${_include_dir} -DPython3_LIBRARY=${_python3_cmake_library} -DPython_ROOT_DIR=${_install_dir} -DPython_EXECUTABLE=${_python3_executable} -DPython_INCLUDE_DIR=${_include_dir} -DPython_LIBRARY=${_python3_cmake_library}" - COMMAND cmake -E touch ${${_python3_target}-otio-patch-flag} - DEPENDS ${${_python3_target}-requirements-flag} ${_otio_patch_script} -) + ADD_CUSTOM_COMMAND( + COMMENT "Patching OpenTimelineIO for Debug Python (lazy GIL init + debug pyd naming)" + OUTPUT ${${_python3_target}-otio-patch-flag} + COMMAND + "${_python3_executable}" "${_otio_patch_script}" --python-exe "${_python3_executable}" --otio-version "${_opentimelineio_version}" --site-packages + "${_install_dir}/Lib/site-packages" --cmake-args + "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable} -DPython3_ROOT_DIR=${_install_dir} -DPython3_EXECUTABLE=${_python3_executable} -DPython3_INCLUDE_DIR=${_include_dir} -DPython3_LIBRARY=${_python3_cmake_library} -DPython_ROOT_DIR=${_install_dir} -DPython_EXECUTABLE=${_python3_executable} -DPython_INCLUDE_DIR=${_include_dir} -DPython_LIBRARY=${_python3_cmake_library}" + COMMAND cmake -E touch ${${_python3_target}-otio-patch-flag} + DEPENDS ${${_python3_target}-requirements-flag} ${_otio_patch_script} + ) +ENDIF() IF(RV_TARGET_WINDOWS) SET(_patch_python_command diff --git a/src/build/patch_opentimelineio_debug.py b/src/build/patch_opentimelineio_debug.py index aa9fe652f..403b44718 100644 --- a/src/build/patch_opentimelineio_debug.py +++ b/src/build/patch_opentimelineio_debug.py @@ -1,4 +1,13 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ***************************************************************************** +# Copyright 2025 Autodesk, Inc. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +# ***************************************************************************** + """ Patch OpenTimelineIO debug build to avoid pybind11 GIL assertions and ensure debug module naming on Windows. @@ -13,7 +22,6 @@ from __future__ import annotations import argparse -import glob import os import shutil import subprocess @@ -26,9 +34,7 @@ def _run(cmd: list[str], env: dict[str, str]) -> None: def _patch_otio_utils(root: Path) -> None: - target = next( - root.glob("**/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp") - ) + target = next(root.glob("**/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp")) text = target.read_text(encoding="utf-8") # Make _value_to_any lazy-initialized to avoid pybind11 GIL asserts at static init. if "static py::object _value_to_any = py::none();" not in text: @@ -49,7 +55,7 @@ def _patch_otio_utils(root: Path) -> None: target.write_text(text, encoding="utf-8") -def _maybe_copy_debug_names(site_packages: Path) -> None: +def copy_debug_names(site_packages: Path) -> None: otio_path = site_packages / "opentimelineio" if not otio_path.exists(): return @@ -135,7 +141,7 @@ def main() -> int: env, ) - _maybe_copy_debug_names(sp) + copy_debug_names(sp) return 0 diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index d87170c12..42f48def0 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -11,7 +11,7 @@ """ Test script to validate all Python package imports at build time. This catches issues like missing dependencies, ABI incompatibilities, and -configuration problems (like OpenSSL legacy provider) before runtime. +configuration problems (like OpenSSL legacy provider) before running the build. Package list is automatically generated from requirements.txt.in to prevent manual synchronization errors. From 5f49c77445b5326ef8516cd45585045db675787b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Fri, 16 Jan 2026 09:32:33 -0500 Subject: [PATCH 12/18] try to fix issue seen in CI only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/patch_opentimelineio_debug.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/build/patch_opentimelineio_debug.py b/src/build/patch_opentimelineio_debug.py index 403b44718..ccec7a0bb 100644 --- a/src/build/patch_opentimelineio_debug.py +++ b/src/build/patch_opentimelineio_debug.py @@ -98,6 +98,11 @@ def main() -> int: # Work around rare pip/pyproject_hooks KeyError on _PYPROJECT_HOOKS_BUILD_BACKEND # by providing a default backend name when unset. env.setdefault("_PYPROJECT_HOOKS_BUILD_BACKEND", "setuptools.build_meta") + # Avoid pip self-upgrade/uninstall churn in CI build envs + env.setdefault("PIP_NO_BUILD_ISOLATION", "1") + env.setdefault("PIP_NO_DEPS", "1") + env.setdefault("PIP_DISABLE_PIP_VERSION_CHECK", "1") + env.setdefault("PIP_NO_PYTHON_VERSION_WARNING", "1") with tempfile.TemporaryDirectory() as td: td_path = Path(td) @@ -108,8 +113,12 @@ def main() -> int: "pip", "download", f"opentimelineio=={version}", + "--no-deps", "--no-binary", ":all:", + "--no-build-isolation", + "--progress-bar", + "off", "-d", str(td_path), ], @@ -136,6 +145,9 @@ def main() -> int: "--no-cache-dir", "--force-reinstall", "--no-build-isolation", + "--no-deps", + "--progress-bar", + "off", str(root), ], env, From 799f732d700469b4e37d17b4f83c942dece33374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Fri, 16 Jan 2026 10:56:10 -0500 Subject: [PATCH 13/18] use fork for now, until fix in OTIO repo? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- cmake/dependencies/python3.cmake | 31 +---- src/build/patch_opentimelineio_debug.py | 161 ------------------------ src/build/requirements.txt.in | 2 +- 3 files changed, 2 insertions(+), 192 deletions(-) delete mode 100644 src/build/patch_opentimelineio_debug.py diff --git a/cmake/dependencies/python3.cmake b/cmake/dependencies/python3.cmake index a24b29187..a12ccb552 100644 --- a/cmake/dependencies/python3.cmake +++ b/cmake/dependencies/python3.cmake @@ -50,14 +50,6 @@ SET(_python3_download_hash "${RV_DEPS_PYTHON_DOWNLOAD_HASH}" ) -SET(_opentimelineio_download_url - "https://github.com/AcademySoftwareFoundation/OpenTimelineIO" -) - -SET(_opentimelineio_git_tag - "v${_opentimelineio_version}" -) - SET(_pyside_archive_url "${RV_DEPS_PYSIDE_ARCHIVE_URL}" ) @@ -345,27 +337,6 @@ LIST( "${_requirements_output_file}" ) -IF(RV_TARGET_WINDOWS) - # Patch OpenTimelineIO for Windows Debug pybind11 GIL assertion and ensure debug naming. - SET(_otio_patch_script - "${PROJECT_SOURCE_DIR}/src/build/patch_opentimelineio_debug.py" - ) - SET(${_python3_target}-otio-patch-flag - ${_install_dir}/${_python3_target}-otio-patch-flag - ) - - ADD_CUSTOM_COMMAND( - COMMENT "Patching OpenTimelineIO for Debug Python (lazy GIL init + debug pyd naming)" - OUTPUT ${${_python3_target}-otio-patch-flag} - COMMAND - "${_python3_executable}" "${_otio_patch_script}" --python-exe "${_python3_executable}" --otio-version "${_opentimelineio_version}" --site-packages - "${_install_dir}/Lib/site-packages" --cmake-args - "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable} -DPython3_ROOT_DIR=${_install_dir} -DPython3_EXECUTABLE=${_python3_executable} -DPython3_INCLUDE_DIR=${_include_dir} -DPython3_LIBRARY=${_python3_cmake_library} -DPython_ROOT_DIR=${_install_dir} -DPython_EXECUTABLE=${_python3_executable} -DPython_INCLUDE_DIR=${_include_dir} -DPython_LIBRARY=${_python3_cmake_library}" - COMMAND cmake -E touch ${${_python3_target}-otio-patch-flag} - DEPENDS ${${_python3_target}-requirements-flag} ${_otio_patch_script} - ) -ENDIF() - IF(RV_TARGET_WINDOWS) SET(_patch_python_command "patch -p1 < ${CMAKE_CURRENT_SOURCE_DIR}/patch/python-${RV_DEPS_PYTHON_VERSION}/python.${RV_DEPS_PYTHON_VERSION}.openssl.props.patch &&\ @@ -453,7 +424,7 @@ ADD_CUSTOM_COMMAND( OUTPUT ${${_python3_target}-imports-test-flag} COMMAND "${_python3_executable}" "${_test_python_imports_script}" COMMAND cmake -E touch ${${_python3_target}-imports-test-flag} - DEPENDS ${${_python3_target}-requirements-flag} ${${_python3_target}-otio-patch-flag} ${_test_python_imports_script} + DEPENDS ${${_python3_target}-requirements-flag} ${_test_python_imports_script} ) # Test the Python distribution's relocatability and environment setup. This moves Python to a temporary location to ensure it works when relocated and validates diff --git a/src/build/patch_opentimelineio_debug.py b/src/build/patch_opentimelineio_debug.py deleted file mode 100644 index ccec7a0bb..000000000 --- a/src/build/patch_opentimelineio_debug.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# ***************************************************************************** -# Copyright 2025 Autodesk, Inc. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# -# ***************************************************************************** - -""" -Patch OpenTimelineIO debug build to avoid pybind11 GIL assertions and ensure debug -module naming on Windows. - -Actions: -- Download OTIO source for the requested version (sdist only). -- Patch otio_utils.cpp to lazily initialize _value_to_any (fixes GIL assert). -- Build & install OTIO from the patched source using the provided Python. -- On Windows Debug, copy _otio/_opentime pyds to *_d names if missing. -""" - -from __future__ import annotations - -import argparse -import os -import shutil -import subprocess -import tempfile -from pathlib import Path - - -def _run(cmd: list[str], env: dict[str, str]) -> None: - subprocess.check_call(cmd, env=env) - - -def _patch_otio_utils(root: Path) -> None: - target = next(root.glob("**/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp")) - text = target.read_text(encoding="utf-8") - # Make _value_to_any lazy-initialized to avoid pybind11 GIL asserts at static init. - if "static py::object _value_to_any = py::none();" not in text: - raise RuntimeError("Expected marker not found in otio_utils.cpp") - text = text.replace( - "static py::object _value_to_any = py::none();", - "// Initialized lazily after the interpreter is ready; constructing py::none()\n" - "// at static init time triggers pybind11 GIL assertions in Debug builds.\n" - "static py::object _value_to_any;", - 1, - ) - # Relax the guard to handle default-constructed (null) py::object - text = text.replace( - "if (_value_to_any.is_none()) {", - "if (!_value_to_any || _value_to_any.is_none()) {", - 1, - ) - target.write_text(text, encoding="utf-8") - - -def copy_debug_names(site_packages: Path) -> None: - otio_path = site_packages / "opentimelineio" - if not otio_path.exists(): - return - - for base in ("_otio", "_opentime"): - for pyd in otio_path.glob(f"{base}*.pyd"): - name = pyd.name - if "_d.cp" in name: - continue - if "d.cp" in name: - # already has a trailing d before cp tag - continue - if ".cp" in name: - corrected = name.replace(".cp", "_d.cp", 1) - else: - corrected = name.replace(".pyd", "_d.pyd") - dest = pyd.with_name(corrected) - if not dest.exists(): - shutil.copy2(pyd, dest) - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--python-exe", required=True, help="Path to python_d.exe") - ap.add_argument("--otio-version", required=True, help="OTIO version to patch") - ap.add_argument("--site-packages", required=True, help="Target site-packages path") - ap.add_argument( - "--cmake-args", - required=True, - help="CMAKE_ARGS string to ensure Python debug libs are used", - ) - args = ap.parse_args() - - py = args.python_exe - version = args.otio_version - sp = Path(args.site_packages).resolve() - - env = os.environ.copy() - env["CMAKE_ARGS"] = args.cmake_args - # Work around rare pip/pyproject_hooks KeyError on _PYPROJECT_HOOKS_BUILD_BACKEND - # by providing a default backend name when unset. - env.setdefault("_PYPROJECT_HOOKS_BUILD_BACKEND", "setuptools.build_meta") - # Avoid pip self-upgrade/uninstall churn in CI build envs - env.setdefault("PIP_NO_BUILD_ISOLATION", "1") - env.setdefault("PIP_NO_DEPS", "1") - env.setdefault("PIP_DISABLE_PIP_VERSION_CHECK", "1") - env.setdefault("PIP_NO_PYTHON_VERSION_WARNING", "1") - - with tempfile.TemporaryDirectory() as td: - td_path = Path(td) - _run( - [ - py, - "-m", - "pip", - "download", - f"opentimelineio=={version}", - "--no-deps", - "--no-binary", - ":all:", - "--no-build-isolation", - "--progress-bar", - "off", - "-d", - str(td_path), - ], - env, - ) - sdist = next(td_path.glob("opentimelineio-*.tar.gz")) - extract_dir = td_path / "src" - extract_dir.mkdir() - _run(["python", "-m", "tarfile", "-e", str(sdist), str(extract_dir)], env) - - # tarfile via python -m tarfile doesn't support -e; fall back to shutil - if not any(extract_dir.iterdir()): - shutil.unpack_archive(str(sdist), extract_dir) - - root = next(extract_dir.glob("opentimelineio-*")) - _patch_otio_utils(root) - - _run( - [ - py, - "-m", - "pip", - "install", - "--no-cache-dir", - "--force-reinstall", - "--no-build-isolation", - "--no-deps", - "--progress-bar", - "off", - str(root), - ], - env, - ) - - copy_debug_names(sp) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/build/requirements.txt.in b/src/build/requirements.txt.in index f5e26a869..a5cdb36cb 100644 --- a/src/build/requirements.txt.in +++ b/src/build/requirements.txt.in @@ -10,7 +10,7 @@ pip==24.0 # License: MIT License (MIT) setuptools==69.5.1 # License: MIT License wheel==0.43.0 # License: MIT License (MIT) numpy==@_numpy_version@ # License: BSD License (BSD-3-Clause) - Required by PySide6 -opentimelineio==@_opentimelineio_version@ # License: Other/Proprietary License (Modified Apache 2.0 License) +opentimelineio @ git+https://github.com/cedrik-fuoco-adsk/OpenTimelineIO.git@968ecb96c6c8194db6b69cc04d9a2d3a8c475cfe # License: Other/Proprietary License (Modified Apache 2.0 License) PyOpenGL==3.1.7 # License: BSD License (BSD) PyOpenGL_accelerate==3.1.10 # License: BSD License (BSD) - v3.1.10 includes Python 3 fix for Cython compatibility From 4612a92405c133a14e31179e33e061e9a56944c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Fri, 16 Jan 2026 10:58:05 -0500 Subject: [PATCH 14/18] fix import script for url format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/test_python_imports.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 42f48def0..74f84d405 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -51,10 +51,15 @@ def parse_requirements(file_path): packages = [] with open(file_path, encoding="utf-8") as f: for line in f: - line = line.strip() + # Remove inline comments and whitespace + line = line.split("#", 1)[0].strip() # Skip comments and empty lines if not line or line.startswith("#"): continue + # Handle VCS/URL form: name @ git+... + if " @ " in line: + packages.append(line.split(" @ ", 1)[0].strip()) + continue # Extract package name (before ==, <, >, etc.) match = re.match(r"^([A-Za-z0-9_-]+)", line) if match: From fc625d90f4ba72ee7f22a6f1d670388a09985b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Fri, 16 Jan 2026 11:00:57 -0500 Subject: [PATCH 15/18] undo some changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- cmake/dependencies/python3.cmake | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmake/dependencies/python3.cmake b/cmake/dependencies/python3.cmake index a12ccb552..85647c1ee 100644 --- a/cmake/dependencies/python3.cmake +++ b/cmake/dependencies/python3.cmake @@ -315,10 +315,7 @@ ENDIF() LIST( APPEND _requirements_install_command - # Force downstream CMake projects (notably OpenTimelineIO + pybind11) to use the *same* custom-built Python. OTIO depends on pybind11 and can use different - # FindPython variants depending on version/environment: - legacy: FindPythonInterp/FindPythonLibsNew via PYTHON_* variables - modern: FindPython / FindPython3 - # via Python*_EXECUTABLE / Python*_INCLUDE_DIR / Python*_LIBRARY Passing all of them avoids Debug/Release mismatches on Windows. - "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable} -DPython3_ROOT_DIR=${_install_dir} -DPython3_EXECUTABLE=${_python3_executable} -DPython3_INCLUDE_DIR=${_include_dir} -DPython3_LIBRARY=${_python3_cmake_library} -DPython_ROOT_DIR=${_install_dir} -DPython_EXECUTABLE=${_python3_executable} -DPython_INCLUDE_DIR=${_include_dir} -DPython_LIBRARY=${_python3_cmake_library}" + "CMAKE_ARGS=-DPYTHON_LIBRARY=${_python3_cmake_library} -DPYTHON_INCLUDE_DIR=${_include_dir} -DPYTHON_EXECUTABLE=${_python3_executable}" "${_python3_executable}" -s -E From 5e698c95fcc65280d509111854f791bec226b0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Fri, 16 Jan 2026 12:45:52 -0500 Subject: [PATCH 16/18] clean up, no need to add PATHs to PATH env.var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/test_python_imports.py | 288 +------------------------------ 1 file changed, 1 insertion(+), 287 deletions(-) diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py index 74f84d405..7eb6927bc 100644 --- a/src/build/test_python_imports.py +++ b/src/build/test_python_imports.py @@ -17,11 +17,8 @@ manual synchronization errors. """ -import glob import os -import platform import re -import shutil import sys # Packages to skip from requirements.txt.in (e.g., build dependencies that don't need import testing). @@ -67,80 +64,6 @@ def parse_requirements(file_path): return packages -def fix_opentimelineio_debug_windows(): - """Fix OpenTimelineIO Debug extension naming and DLL dependencies on Windows. - - In Debug builds, OTIO creates _opentimed.pyd but Python expects _opentime_d.pyd. - This copies the file to the correct name if needed. - - Also ensures OTIO's DLL dependencies are accessible by adding lib directories to PATH. - """ - if platform.system() != "Windows" or "_d.exe" not in sys.executable.lower(): - return # Only needed for Windows Debug builds - - try: - import site - - site_packages = site.getsitepackages() - paths_to_add = [] - - for sp in site_packages: - otio_path = os.path.join(sp, "opentimelineio") - if not os.path.exists(otio_path): - continue - - # Add any OTIO lib directories to PATH for DLL loading - # Check common locations for OTIO DLLs - potential_lib_dirs = [ - otio_path, # The package itself - os.path.join(otio_path, "lib"), - os.path.join(otio_path, ".libs"), - ] - - for lib_dir in potential_lib_dirs: - if os.path.exists(lib_dir) and lib_dir not in os.environ.get("PATH", ""): - # Check if there are DLL files in this directory - dll_files = glob.glob(os.path.join(lib_dir, "*.dll")) - if dll_files: - paths_to_add.append(lib_dir) - - # Look for ALL misnamed Debug extensions that end with 'd' before the Python tag - # Examples: _opentimed.*.pyd, _otiod.*.pyd - # These should be: _opentime_d.*.pyd, _otio_d.*.pyd - all_pyd_files = glob.glob(os.path.join(otio_path, "*.pyd")) - - for pyd_file in all_pyd_files: - basename = os.path.basename(pyd_file) - - # Check if it matches pattern: _d.cp-win_amd64.pyd - # where ends with 'd' but should be _d - if basename.startswith("_") and "d.cp" in basename and "_d.cp" not in basename: - # Extract the module name (between '_' and 'd.cp') - # Example: _opentimed.cp311 -> _opentime - parts = basename.split("d.cp") - if len(parts) == 2: - module_base = parts[0] # e.g., "_opentime" - - # Create correct name with _d suffix - correct_name = f"{module_base}_d.cp{parts[1]}" - correct_path = os.path.join(otio_path, correct_name) - - if not os.path.exists(correct_path): - shutil.copy2(pyd_file, correct_path) - print(f"Fixed OTIO Debug extension: {basename} -> {correct_name}") - - # Add any discovered lib paths to PATH - if paths_to_add: - current_path = os.environ.get("PATH", "") - new_path = os.pathsep.join(paths_to_add) + os.pathsep + current_path - os.environ["PATH"] = new_path - print(f"Added OTIO lib directories to PATH: {', '.join(paths_to_add)}") - - except Exception as e: - # Don't fail if fix doesn't work, let import fail naturally - print(f"Warning: Could not fix OTIO Debug naming: {e}") - - def try_import(package_name): """Try importing package, with fallback to stripped 'Py' prefix. @@ -150,29 +73,6 @@ def try_import(package_name): Raises ImportError if both attempts fail. """ - # Fix OpenTimelineIO Debug naming issue before importing - if "opentimelineio" in package_name.lower(): - fix_opentimelineio_debug_windows() - - # On Windows Debug, add current directory to DLL search path - # This helps Windows find any DLLs that might be in the package directory - if platform.system() == "Windows" and "_d.exe" in sys.executable.lower(): - try: - import site - - site_packages = site.getsitepackages() - for sp in site_packages: - otio_path = os.path.join(sp, "opentimelineio") - if os.path.exists(otio_path): - # Add DLL search directory (Python 3.8+) - if hasattr(os, "add_dll_directory"): - try: - os.add_dll_directory(otio_path) - except (OSError, FileNotFoundError): - pass - except Exception: - pass # Silently continue if this fails - try: return __import__(package_name) except ImportError: @@ -182,166 +82,9 @@ def try_import(package_name): raise -def find_msvc_runtime_dlls(): - """Find MSVC runtime DLL directories from Visual Studio installation. - - Returns list of directories containing MSVC runtime DLLs (both Release and Debug). - Searches in both VC\Tools (build tools) and VC\Redist (redistributable packages). - """ - msvc_dll_dirs = [] - - # Common Visual Studio installation paths - vs_paths = [ - r"C:\Program Files\Microsoft Visual Studio", - r"C:\Program Files (x86)\Microsoft Visual Studio", - ] - - for vs_base in vs_paths: - if not os.path.exists(vs_base): - continue - - try: - # Look for VS versions (2022, 2019, etc.) - for vs_year in os.listdir(vs_base): - vs_year_path = os.path.join(vs_base, vs_year) - if not os.path.isdir(vs_year_path): - continue - - # Look for editions (Enterprise, Professional, Community, BuildTools) - for edition in ["Enterprise", "Professional", "Community", "BuildTools"]: - edition_path = os.path.join(vs_year_path, edition) - if not os.path.exists(edition_path): - continue - - # 1. Check VC\Redist for Debug runtime DLLs (for Debug builds) - vc_redist = os.path.join(edition_path, "VC", "Redist", "MSVC") - if os.path.exists(vc_redist): - try: - msvc_versions = sorted(os.listdir(vc_redist), reverse=True) - for msvc_ver in msvc_versions[:3]: # Check up to 3 latest versions - # Check Debug redistributables - debug_redist_paths = [ - os.path.join( - vc_redist, msvc_ver, "debug_nonredist", "x64", "Microsoft.VC143.DebugCRT" - ), - os.path.join( - vc_redist, - msvc_ver, - "onecore", - "debug_nonredist", - "x64", - "Microsoft.VC143.DebugCRT", - ), - ] - for debug_path in debug_redist_paths: - if os.path.exists(debug_path): - # Check for Debug runtime DLLs - if glob.glob(os.path.join(debug_path, "*140d.dll")): - if debug_path not in msvc_dll_dirs: - msvc_dll_dirs.append(debug_path) - break - if msvc_dll_dirs: - break - except (OSError, PermissionError): - pass - - # 2. Check VC\Tools for runtime DLLs (fallback for Release or if Redist not found) - vc_tools = os.path.join(edition_path, "VC", "Tools", "MSVC") - if os.path.exists(vc_tools): - try: - # Get the latest MSVC version - msvc_versions = sorted(os.listdir(vc_tools), reverse=True) - for msvc_ver in msvc_versions[:3]: # Check up to 3 latest versions - # Check bin directories for runtime DLLs - bin_paths = [ - os.path.join(vc_tools, msvc_ver, "bin", "Hostx64", "x64"), - os.path.join(vc_tools, msvc_ver, "bin", "Hostx86", "x64"), - ] - for bin_path in bin_paths: - if os.path.exists(bin_path): - # Check for MSVC runtime DLLs - if glob.glob(os.path.join(bin_path, "msvcp140*.dll")) or glob.glob( - os.path.join(bin_path, "vcruntime140*.dll") - ): - if bin_path not in msvc_dll_dirs: - msvc_dll_dirs.append(bin_path) - break - if msvc_dll_dirs and len(msvc_dll_dirs) >= 2: - # Found both Redist and Tools, that's enough - break - except (OSError, PermissionError): - pass - - if msvc_dll_dirs: - break - if msvc_dll_dirs: - break - except (OSError, PermissionError): - pass - - return msvc_dll_dirs - - def test_imports(): """Test that all required packages can be imported.""" - # On Windows, ensure Python bin directory is on PATH for DLL loading. - # This is required for .pyd extensions (like _opentime.pyd) to find Python DLL and dependencies. - if platform.system() == "Windows": - python_bin = os.path.dirname(sys.executable) - path_env = os.environ.get("PATH", "") - paths_to_add = [] - - if python_bin not in path_env: - paths_to_add.append(python_bin) - - # For Python 3.11+, also check for DLLs directory. - python_root = os.path.dirname(python_bin) - dlls_dir = os.path.join(python_root, "DLLs") - if os.path.exists(dlls_dir) and dlls_dir not in path_env: - paths_to_add.append(dlls_dir) - - # For Debug builds, add additional directories for runtime DLLs - if "_d.exe" in sys.executable.lower(): - # Add Python lib/libs directories - for dir_name in ["lib", "libs"]: - potential_dir = os.path.join(python_root, dir_name) - if os.path.exists(potential_dir) and potential_dir not in path_env: - paths_to_add.append(potential_dir) - - # Look for MSVC runtime DLLs from Visual Studio installation - # This is needed because OTIO Debug extensions link against MSVC runtime - msvc_dirs = find_msvc_runtime_dlls() - for msvc_dir in msvc_dirs: - if msvc_dir not in path_env: - paths_to_add.append(msvc_dir) - - # Look for RV_DEPS directories that might contain MSVC DLLs - # Check in RV_DEPS directory structure (for CI builds) - build_root = python_root - for _ in range(5): # Go up max 5 levels to find _build directory - build_root = os.path.dirname(build_root) - if not build_root or build_root == os.path.dirname(build_root): - break - - # Check for RV_DEPS directories that might contain MSVC DLLs - if os.path.exists(build_root): - # Look for any RV_DEPS_* directories that might have bin folders with DLLs - try: - for item in os.listdir(build_root): - if item.startswith("RV_DEPS_") and os.path.isdir(os.path.join(build_root, item)): - bin_dir = os.path.join(build_root, item, "install", "bin") - if os.path.exists(bin_dir) and bin_dir not in path_env: - # Check if it has DLL files - if glob.glob(os.path.join(bin_dir, "*.dll")): - paths_to_add.append(bin_dir) - except (OSError, PermissionError): - pass - - if paths_to_add: - # Add paths to the front of PATH. - new_path = os.pathsep.join(paths_to_add) + os.pathsep + path_env - os.environ["PATH"] = new_path - print(f"Added to PATH for DLL loading: {', '.join(paths_to_add)}") + # No PATH manipulation needed. # Get requirements.txt.in from same directory as this script script_dir = os.path.dirname(os.path.abspath(__file__)) @@ -380,16 +123,6 @@ def test_imports(): if skipped_packages: print(f"Skipping: {', '.join(skipped_packages)}") - # On Windows, show PATH info for DLL debugging - if platform.system() == "Windows": - python_bin = os.path.dirname(sys.executable) - print(f"Python bin directory: {python_bin}") - path_env = os.environ.get("PATH", "") - if python_bin in path_env: - print("Python bin is on PATH: Yes") - else: - print("Python bin is on PATH: No (this should not happen - PATH was modified at startup)") - print("=" * 80) print() @@ -420,25 +153,6 @@ def test_imports(): print("Build-time import test FAILED!") print("One or more required Python packages could not be imported.") print() - if any("OpenSSL" in str(e) or "legacy" in str(e).lower() for _, e in failed_imports): - print("NOTE: If you see OpenSSL legacy provider errors:") - print( - " - Rebuild OpenSSL to generate openssl.cnf: ninja -t clean RV_DEPS_OPENSSL && ninja RV_DEPS_OPENSSL" - ) - print(" - Check that OPENSSL_CONF is set in sitecustomize.py") - print() - - if any("opentimelineio" in str(m).lower() for m, _ in failed_imports): - print("NOTE: If you see OpenTimelineIO import errors:") - print(" - Check that opentimelineio was built from source (not from wheel)") - print(" - Verify CMAKE_ARGS were passed correctly to pip install") - print(" - On Windows Debug builds: OTIO may link against Release runtime (msvcp140.dll)") - print(" instead of Debug runtime (msvcp140d.dll), causing initialization failures") - print(" - Try rebuilding: ninja -t clean RV_DEPS_PYTHON3 && ninja RV_DEPS_PYTHON3") - print() - print("To diagnose DLL dependencies, run:") - print(f" python {os.path.join(script_dir, 'check_pyd_dependencies.py')}") - print() return 1 else: print() From f89d02bfbdaa6bf039dc9272ba6f28d9094acc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Thu, 26 Mar 2026 14:21:28 -0400 Subject: [PATCH 17/18] Back to official repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/requirements.txt.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/requirements.txt.in b/src/build/requirements.txt.in index a5cdb36cb..f5e26a869 100644 --- a/src/build/requirements.txt.in +++ b/src/build/requirements.txt.in @@ -10,7 +10,7 @@ pip==24.0 # License: MIT License (MIT) setuptools==69.5.1 # License: MIT License wheel==0.43.0 # License: MIT License (MIT) numpy==@_numpy_version@ # License: BSD License (BSD-3-Clause) - Required by PySide6 -opentimelineio @ git+https://github.com/cedrik-fuoco-adsk/OpenTimelineIO.git@968ecb96c6c8194db6b69cc04d9a2d3a8c475cfe # License: Other/Proprietary License (Modified Apache 2.0 License) +opentimelineio==@_opentimelineio_version@ # License: Other/Proprietary License (Modified Apache 2.0 License) PyOpenGL==3.1.7 # License: BSD License (BSD) PyOpenGL_accelerate==3.1.10 # License: BSD License (BSD) - v3.1.10 includes Python 3 fix for Cython compatibility From 0ac10b5958a1a0ca7cc64f8f13d5fbc60546b28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drik=20Fuoco?= Date: Mon, 30 Mar 2026 16:10:18 -0400 Subject: [PATCH 18/18] Build from a known commit that fixes issues with windows debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédrik Fuoco --- src/build/requirements.txt.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/build/requirements.txt.in b/src/build/requirements.txt.in index f5e26a869..4b96b4256 100644 --- a/src/build/requirements.txt.in +++ b/src/build/requirements.txt.in @@ -10,7 +10,8 @@ pip==24.0 # License: MIT License (MIT) setuptools==69.5.1 # License: MIT License wheel==0.43.0 # License: MIT License (MIT) numpy==@_numpy_version@ # License: BSD License (BSD-3-Clause) - Required by PySide6 -opentimelineio==@_opentimelineio_version@ # License: Other/Proprietary License (Modified Apache 2.0 License) +# This commit fixes issue with OTIO and the Windows debug build. +opentimelineio @ git+https://github.com/AcademySoftwareFoundation/OpenTimelineIO.git@d793d6f99729ffc5d3ed4943ea17e8fd6065f5ae # License: Other/Proprietary License (Modified Apache 2.0 License) PyOpenGL==3.1.7 # License: BSD License (BSD) PyOpenGL_accelerate==3.1.10 # License: BSD License (BSD) - v3.1.10 includes Python 3 fix for Cython compatibility