diff --git a/cmake/dependencies/python3.cmake b/cmake/dependencies/python3.cmake index 696e1ceb5..53489c0ad 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}" ) @@ -433,21 +425,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/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 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 diff --git a/src/build/test_python_imports.py b/src/build/test_python_imports.py new file mode 100644 index 000000000..7eb6927bc --- /dev/null +++ b/src/build/test_python_imports.py @@ -0,0 +1,164 @@ +#!/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 running the build. + +Package list is automatically generated from requirements.txt.in to prevent +manual synchronization errors. +""" + +import os +import re +import sys + +# 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: + # 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: + 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.""" + # No PATH manipulation needed. + + # 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"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)}") + + print("=" * 80) + print() + + for module_name, description in imports_to_test: + try: + 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(f" Error: {type(e).__name__}: {e}") + failed_imports.append((module_name, e)) + + # Keep failure output minimal; detailed diagnostics removed. + + 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() + return 1 + else: + print() + print("All Python package imports successful!") + return 0 + + +if __name__ == "__main__": + sys.exit(test_imports())