diff --git a/.gitignore b/.gitignore index b3e3cc5..7ee6ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /release/ __pycache__/ orisys.egg-info/ -/.cursor/ \ No newline at end of file +/.cursor/ +/dist_pyd/ \ No newline at end of file diff --git a/build_pyd.py b/build_pyd.py new file mode 100644 index 0000000..4a015cd --- /dev/null +++ b/build_pyd.py @@ -0,0 +1,902 @@ +""" +Build script to create wheel distributions with .pyd/.so files instead of .py files. + +This script uses Cython to compile Python source files into extension modules (.pyd on Windows, .so on Linux), +making the source code completely hidden in the final distribution. + +Requirements: + - Cython + - A C compiler (Visual Studio Build Tools on Windows, GCC on Linux) +""" +import os +import shutil +import subprocess +import sys +import platform +from pathlib import Path +import tempfile + + +# Package version +VERSION = "0.2.0" + +# Python versions to build for +PYTHON_VERSIONS = ['3.8', '3.9', '3.10'] + +# Detect platform +IS_WINDOWS = platform.system() == 'Windows' +IS_LINUX = platform.system() == 'Linux' +PLATFORM_NAME = 'win' if IS_WINDOWS else 'linux' if IS_LINUX else 'unknown' + +# Extension for compiled modules +EXT_EXTENSION = '.pyd' if IS_WINDOWS else '.so' + + +def clean_build_dirs(): + """Remove previous build artifacts.""" + dirs_to_clean = ['build', 'dist', 'orisys.egg-info', 'dist_pyd', 'build_pyd'] + for dir_name in dirs_to_clean: + if os.path.exists(dir_name): + print(f"Cleaning {dir_name}...") + try: + # Try to remove with error handling + shutil.rmtree(dir_name, ignore_errors=False, onerror=_handle_remove_error) + except (PermissionError, OSError) as e: + print(f" ⚠️ Warning: Could not fully remove {dir_name}: {e}") + print(f" Some files may be locked. Continuing anyway...") + # Try again with ignore_errors=True + try: + shutil.rmtree(dir_name, ignore_errors=True) + except Exception: + pass + + # Ensure dist directory exists with platform subdirectories + os.makedirs(f'dist/{PLATFORM_NAME}', exist_ok=True) + + +def _handle_remove_error(func, path, exc_info): + """Handle errors during directory removal.""" + # If it's a permission error, try to change permissions and retry + if func is os.rmdir: + try: + # Try to change permissions and remove + os.chmod(path, 0o777) + os.rmdir(path) + except Exception: + # If that fails, just skip it + pass + elif func is os.remove: + try: + os.chmod(path, 0o777) + os.remove(path) + except Exception: + pass + + +def check_cython(): + """Check if Cython is installed.""" + try: + import Cython + print(f"✓ Cython {Cython.__version__} is available") + return True + except ImportError: + print("❌ Cython is not installed!") + print(" Install it with: pip install cython") + return False + + +def check_c_compiler(): + """Check if a C compiler is available.""" + if IS_WINDOWS: + # On Windows, check for cl.exe (Microsoft Visual C++) + # Note: cl.exe might not be in PATH even if installed + # We'll do a basic check but the actual compilation will provide better errors + cl_path = shutil.which('cl.exe') + if cl_path: + print("✓ C compiler (cl.exe) found in PATH") + return True + else: + print("⚠️ C compiler (cl.exe) not found in PATH") + print(" (This is OK if Visual Studio is installed but not in PATH)") + print(" The build will attempt to use the compiler anyway...") + return True # Return True to allow build attempt - it will fail with better error if missing + else: + # On Linux/Mac, check for gcc + try: + result = subprocess.run( + ['gcc', '--version'], capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + print(f"✓ C compiler (gcc) is available") + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + print("❌ C compiler (gcc) not found!") + print("\n Install with:") + if IS_LINUX: + print(" sudo apt-get install build-essential # Ubuntu/Debian") + print(" sudo yum install gcc gcc-c++ # CentOS/RHEL") + else: + print(" Install Xcode Command Line Tools:") + print(" xcode-select --install") + return False + + +def get_python_executable(version): + """Get the Python executable for a specific version based on platform.""" + current_version = f"{sys.version_info.major}.{sys.version_info.minor}" + if current_version == version: + return [sys.executable] + + if IS_WINDOWS: + username = os.environ.get("USERNAME", "") + possible_paths = [ + f'py -{version}', + f'python{version}', + f'C:\\Users\\{username}\\anaconda3\\envs\\py{version.replace(".", "")}\\python.exe', + f'C:\\Users\\{username}\\anaconda3\\envs\\py{version.replace(".", "")}-env\\python.exe', + f'C:\\Users\\{username}\\anaconda3\\envs\\python{version.replace(".", "")}\\python.exe', + f'C:\\Users\\{username}\\anaconda3\\envs\\orisys-py{version.replace(".", "")}\\python.exe', + f'C:\\Users\\{username}\\miniconda3\\envs\\py{version.replace(".", "")}\\python.exe', + f'C:\\Python{version.replace(".", "")}\\python.exe', + f'C:\\Program Files\\Python{version.replace(".", "")}\\python.exe', + ] + else: + possible_paths = [ + f'python{version}', + f'/usr/bin/python{version}', + f'/usr/local/bin/python{version}', + f'~/.pyenv/versions/{version}/bin/python', + f'/opt/python{version}/bin/python', + ] + + for path in possible_paths: + try: + if path.startswith('~'): + path = os.path.expanduser(path) + + if IS_WINDOWS and path.startswith('py -'): + result = subprocess.run( + path.split() + ['-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'], + capture_output=True, text=True, shell=True + ) + if result.returncode == 0 and result.stdout.strip() == version: + return path.split() + else: + if not os.path.exists(path): + continue + + result = subprocess.run( + [path, '-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'], + capture_output=True, text=True + ) + if result.returncode == 0 and result.stdout.strip() == version: + return [path] + except (FileNotFoundError, subprocess.SubprocessError, PermissionError): + continue + + return None + + +def create_cython_setup_files(): + """Create setup.py files for Cython compilation.""" + print("\n=== Creating Cython setup files ===") + + # Find all Python files to compile (excluding __init__.py and configs) + src_dir = Path('src/orisys') + py_files = [] + + for py_file in src_dir.rglob('*.py'): + # Skip __init__.py files (they need to stay as .py for package structure) + if py_file.name == '__init__.py': + continue + # Skip getDensity.py (excluded from compilation) + if py_file.name == 'getDensity.py': + continue + # Skip configs directory (JSON files only) + if 'configs' in py_file.parts and py_file.name != '__init__.py': + continue + + # Get relative path from src/orisys + rel_path = py_file.relative_to(src_dir) + py_files.append(rel_path) + + print(f"Found {len(py_files)} Python files to compile") + + # Create dist_pyd directory structure + dist_dir = Path('dist_pyd/src/orisys') + dist_dir.mkdir(parents=True, exist_ok=True) + + # Copy ALL Python source files to dist_pyd (needed for Cython compilation) + # Cython needs the source files to compile them + print("Copying Python source files for compilation...") + for py_file in src_dir.rglob('*.py'): + rel_path = py_file.relative_to(src_dir) + # dst_file should be in dist_pyd/src/orisys/ (not dist_pyd/src/) + dst_file = dist_dir / rel_path + dst_file.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(py_file, dst_file) + if py_file.name == '__init__.py': + print(f" Copied: {rel_path} (will remain as .py)") + else: + print(f" Copied: {rel_path} (will be compiled to .pyd/.so)") + + # Copy configs directory (JSON files) + configs_src = src_dir / 'configs' + configs_dst = dist_dir / 'configs' + if configs_src.exists(): + shutil.copytree(configs_src, configs_dst, dirs_exist_ok=True) + print(f" Copied: configs/ directory") + + # Create setup.py for Cython compilation + # Build the py_files list as a string + py_files_str = '[\n' + for py_file in py_files: + # Convert path to module name (e.g., "sensor.py" -> "orisys.sensor") + module_path = str(py_file).replace('\\', '/').replace('.py', '').replace('/', '.') + file_path = f"src/orisys/{py_file}" + py_files_str += f' ("orisys.{module_path}", "{file_path}"),\n' + py_files_str += ']' + + # Build setup.py content using string concatenation to avoid f-string escaping issues + # Note: We need to handle Windows multiprocessing properly + # The if __name__ == '__main__': guard is required for Windows multiprocessing support + setup_content = '''from setuptools import setup, Extension +from Cython.Build import cythonize +from pathlib import Path +import numpy +import sys +import platform +import multiprocessing +import os + +# Package version +VERSION = "''' + VERSION + '''" + +# Get include directories +numpy_include = numpy.get_include() + +# On Windows, find Python library for explicit linking if needed +if platform.system() == 'Windows': + import os + python_libs_dir = os.path.join(sys.base_prefix, 'libs') + python_version = f"{sys.version_info.major}{sys.version_info.minor}" + python_lib_name = f"python{python_version}" + # Check if library file exists + python_lib_path = os.path.join(python_libs_dir, f"{python_lib_name}.lib") + has_python_lib = os.path.exists(python_lib_path) if python_libs_dir and os.path.exists(python_libs_dir) else False +else: + python_libs_dir = None + python_lib_name = None + python_lib_path = None + has_python_lib = False + +# List of Python files to compile (excluding __init__.py) +py_files = ''' + py_files_str + ''' + +# Create Extension objects +extensions = [] +for module_name, file_path in py_files: + # Setup library directories and libraries for Windows + library_dirs = [] + libraries = [] + extra_link_args = [] + + if platform.system() == 'Windows' and has_python_lib: + # Explicitly link Python library on Windows + library_dirs = [python_libs_dir] + # Use libraries parameter to link Python library + # This is the standard way setuptools expects + libraries = [python_lib_name] + + ext = Extension( + module_name, + [file_path], + include_dirs=[numpy_include], + library_dirs=library_dirs, + libraries=libraries, + # Add any additional compiler flags here + extra_compile_args=[], + extra_link_args=extra_link_args, + ) + extensions.append(ext) + +# Determine number of threads for parallel compilation +# On Windows, disable parallel compilation to avoid multiprocessing spawn issues +# On Linux/Mac, use parallel compilation for faster builds +if platform.system() == 'Windows': + nthreads = 1 # Single-threaded on Windows to avoid multiprocessing issues +else: + nthreads = 4 # Parallel compilation on Linux/Mac + +# Windows multiprocessing requires this guard +if __name__ == '__main__': + # Required for Windows multiprocessing support + multiprocessing.freeze_support() + + # On Windows, ensure 64-bit build if Python is 64-bit + if platform.system() == 'Windows': + import sysconfig + # Check if Python is 64-bit + if sys.maxsize > 2**32: + # Force 64-bit build by setting distutils environment + import distutils.util + # This should force 64-bit build + try: + # Try to set the platform to win-amd64 + import distutils._msvccompiler + # The compiler should automatically detect 64-bit Python + pass + except: + pass + + setup( + name="orisys", + version=VERSION, + description="Orisys - Optical Tactile Sensor SDK (Compiled)", + author="Your Name", + ext_modules=cythonize( + extensions, + compiler_directives={ + 'language_level': "3", + 'embedsignature': True, + # Optimize for size and speed + 'optimize.use_switch': True, + 'optimize.unpack_method_calls': True, + }, + build_dir="build_pyd", + # Create .pyd/.so files instead of .py + nthreads=nthreads, # Parallel compilation (disabled on Windows) + ), + zip_safe=False, + include_package_data=True, + package_data={ + "orisys": ["configs/*.json"], + }, + ) +''' + + setup_file = Path('dist_pyd/setup.py') + with open(setup_file, 'w') as f: + f.write(setup_content) + + print(f"Created setup.py with {len(py_files)} modules to compile") + return py_files + + +def compile_with_cython(python_version): + """Compile Python files to .pyd/.so using Cython.""" + print(f"\n{'='*60}") + print(f"Compiling with Cython for Python {python_version} on {PLATFORM_NAME}") + print('='*60) + + python_cmd = get_python_executable(python_version) + + if not python_cmd: + print(f"⚠️ Python {python_version} not found on {PLATFORM_NAME}, skipping...") + return False + + print(f"Using Python: {' '.join(python_cmd)}") + + # Verify Python version + result = subprocess.run( + python_cmd + ['-c', 'import sys; print(f"Python {sys.version}")'], + capture_output=True, text=True + ) + print(result.stdout.strip()) + + # Check if Cython is installed in this Python + result = subprocess.run( + python_cmd + ['-c', 'import Cython; print(f"Cython {Cython.__version__}")'], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"❌ Cython not installed for Python {python_version}") + print(f" Install with: {' '.join(python_cmd)} -m pip install cython") + return False + print(result.stdout.strip()) + + # Check if numpy is installed (required for compilation) + result = subprocess.run( + python_cmd + ['-c', 'import numpy; print(f"NumPy {numpy.__version__}")'], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"❌ NumPy not installed for Python {python_version}") + print(f" Install with: {' '.join(python_cmd)} -m pip install numpy") + return False + print(result.stdout.strip()) + + os.chdir('dist_pyd') + + # On Windows, ensure we use the correct architecture (64-bit) + # Check Python architecture first + if IS_WINDOWS: + arch_check = subprocess.run( + python_cmd + ['-c', 'import sys; print("64bit" if sys.maxsize > 2**32 else "32bit")'], + capture_output=True, text=True + ) + if arch_check.returncode == 0: + python_arch = arch_check.stdout.strip() + print(f"Python architecture: {python_arch}") + if python_arch == "64bit": + # Force 64-bit build on Windows by setting environment variable + # This tells distutils to use the 64-bit toolchain + os.environ['DISTUTILS_USE_SDK'] = '1' + # Also try to find and use vcvars64.bat if available + # But setuptools should handle this automatically + print(" Using 64-bit build configuration") + + # Build extensions + # Don't use --inplace to avoid directory issues + # Build to build/lib directory, then copy to src/orisys + cmd = python_cmd + ['setup.py', 'build_ext'] + print(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + + os.chdir('..') + + if result.returncode != 0: + print(f"❌ Error compiling with Cython") + + # Check for common compiler errors + error_output = result.stderr + "\n" + result.stdout + if "cl.exe" in error_output and "failed" in error_output: + print("\n ⚠️ C compiler (cl.exe) error detected!") + print(" This usually means:") + print(" 1. Visual Studio Build Tools are not installed") + print(" 2. The compiler is not in your PATH") + print(" 3. The Visual Studio environment is not set up") + print("\n Solutions:") + print(" - Install Visual Studio Build Tools with C++ support") + print(" - Or open 'Developer Command Prompt for VS' and run from there") + print(" - Or run: vcvars64.bat before building") + + print(f"\n Full error output:\n{result.stderr}") + if result.stdout and result.stderr != result.stdout: + print(f"\n stdout:\n{result.stdout}") + return False + + print(f"✅ Compilation successful for Python {python_version}!") + + # Copy compiled files from build directory to src/orisys + build_lib_dir = Path('build') + target_dir = Path('src/orisys') + + # Find the build directory (it may be build/lib.win-amd64-cpython-310 or similar) + compiled_files = [] + if build_lib_dir.exists(): + # Find the actual build output directory + for lib_dir in build_lib_dir.glob('lib.*'): + orisys_build_dir = lib_dir / 'orisys' + if orisys_build_dir.exists(): + # Copy all .pyd/.so files to src/orisys + print(f" Copying compiled files from {orisys_build_dir} to {target_dir}...") + target_dir.mkdir(parents=True, exist_ok=True) + for compiled_file in orisys_build_dir.glob(f'*{EXT_EXTENSION}'): + # Copy with original filename (e.g., sensor.cp310-win_amd64.pyd) + target_file = target_dir / compiled_file.name + shutil.copy(compiled_file, target_file) + compiled_files.append(target_file) + + # Also create a symlink/copy with base name (e.g., sensor.pyd) + # Python can import from either name, but base name is more reliable + base_name = compiled_file.stem.split('.')[0] # Get 'sensor' from 'sensor.cp310-win_amd64' + base_file = target_dir / f"{base_name}{EXT_EXTENSION}" + if not base_file.exists(): + shutil.copy(compiled_file, base_file) + compiled_files.append(base_file) + print(f" Copied: {compiled_file.name} -> {target_file} and {base_file.name}") + else: + print(f" Copied: {compiled_file.name} -> {target_file}") + break + + # Verify files are in the target location + if not compiled_files: + compiled_files = list(target_dir.rglob(f'*{EXT_EXTENSION}')) + if compiled_files: + print(f" Found {len(compiled_files)} existing {EXT_EXTENSION} files in target directory") + + print(f" Created {len(compiled_files)} {EXT_EXTENSION} files") + + # List some example files + if compiled_files: + print(f" Examples: {', '.join([f.name for f in compiled_files[:3]])}") + if len(compiled_files) > 3: + print(f" ... and {len(compiled_files) - 3} more") + else: + print(" ⚠️ Warning: No compiled files found in expected locations") + print(" This may indicate a problem with the build process") + + # Remove original .py files (except __init__.py) after successful compilation + # This ensures only compiled .pyd/.so files remain + print("\n=== Removing original .py source files (keeping only compiled extensions) ===") + removed_count = 0 + for py_file in Path('dist_pyd/src/orisys').rglob('*.py'): + # Keep __init__.py files + if py_file.name == '__init__.py': + continue + # Check if corresponding .pyd/.so file exists + compiled_file = py_file.with_suffix(EXT_EXTENSION) + if compiled_file.exists(): + try: + py_file.unlink() + removed_count += 1 + print(f" Removed: {py_file.relative_to('dist_pyd')}") + except Exception as e: + print(f" Warning: Could not remove {py_file.name}: {e}") + + if removed_count > 0: + print(f"Removed {removed_count} original .py files (compiled versions exist)") + else: + print("No .py files removed (compiled files may not have been created)") + + # Return True if compilation was successful (returncode == 0) + # Even if file verification had issues, compilation itself succeeded + return True + + +def create_package_setup(python_version=None): + """Create setup.py for packaging the compiled .pyd/.so files.""" + print(f"\n=== Creating package setup.py (Python {python_version or '3.8-3.10'}) ===") + + if python_version: + version_constraint = f'=={python_version}.*' + py_tag = f'py{python_version.replace(".", "")}' + else: + version_constraint = '>=3.8,<3.11' + py_tag = 'py38.py39.py310' + + # Before packaging, ensure .pyd files are in src/orisys + # They might still be in build directory, so copy them if needed + print("=== Ensuring .pyd files are in src/orisys before packaging ===") + orisys_dir = Path('dist_pyd/src/orisys') + build_dir = Path('dist_pyd/build') + + # Check if files are in build directory and copy them + copied_count = 0 + if build_dir.exists(): + for lib_dir in build_dir.glob('lib.*'): + orisys_build_dir = lib_dir / 'orisys' + if orisys_build_dir.exists(): + print(f" Found .pyd files in {orisys_build_dir}, copying to src/orisys...") + orisys_dir.mkdir(parents=True, exist_ok=True) + for compiled_file in orisys_build_dir.glob(f'*{EXT_EXTENSION}'): + target_file = orisys_dir / compiled_file.name + # Always copy to ensure we have the latest version + shutil.copy(compiled_file, target_file) + copied_count += 1 + print(f" Copied: {compiled_file.name} -> {target_file}") + + if copied_count > 0: + print(f" Successfully copied {copied_count} .pyd files to src/orisys") + else: + print(" ⚠️ No .pyd files found in build directory to copy") + + # Before packaging, ensure all .py files (except __init__.py) are removed + print("=== Final cleanup: Removing any remaining .py source files ===") + removed_count = 0 + if orisys_dir.exists(): + for py_file in orisys_dir.rglob('*.py'): + # Keep __init__.py files + if py_file.name == '__init__.py': + continue + # Check if corresponding .pyd/.so file exists + compiled_file = py_file.with_suffix(EXT_EXTENSION) + # Check for any .pyd file with this base name + base_name = py_file.stem + has_compiled = False + if compiled_file.exists(): + has_compiled = True + else: + # Check for versioned .pyd files (e.g., sensor.cp310-win_amd64.pyd) + for pyd_file in orisys_dir.glob(f'{base_name}.*{EXT_EXTENSION}'): + has_compiled = True + break + + if has_compiled: + try: + py_file.unlink() + removed_count += 1 + print(f" Removed: {py_file.relative_to('dist_pyd')}") + except Exception as e: + print(f" Warning: Could not remove {py_file.name}: {e}") + else: + # If no compiled version exists, this is a problem + print(f" ⚠️ Warning: {py_file.name} has no compiled version, removing anyway") + try: + py_file.unlink() + removed_count += 1 + except Exception as e: + print(f" Warning: Could not remove {py_file.name}: {e}") + + if removed_count > 0: + print(f"Removed {removed_count} .py files before packaging") + + # Find all compiled .pyd/.so files in src/orisys + compiled_files = list(orisys_dir.rglob(f'*{EXT_EXTENSION}')) if orisys_dir.exists() else [] + print(f"Found {len(compiled_files)} compiled {EXT_EXTENSION} files in src/orisys") + + if len(compiled_files) == 0: + print(" ⚠️ WARNING: No .pyd files found in src/orisys!") + print(" This will cause the wheel to be missing compiled modules.") + + # Create MANIFEST.in to ensure .pyd files are included + manifest_content = f"""# Include all compiled extensions +include src/orisys/*{EXT_EXTENSION} +recursive-include src/orisys *{EXT_EXTENSION} + +# Include JSON config files +recursive-include src/orisys/configs *.json + +# Exclude all .py files except __init__.py +global-exclude *.py +include src/orisys/__init__.py +recursive-include src/orisys/configs __init__.py +""" + manifest_file = Path('dist_pyd/MANIFEST.in') + with open(manifest_file, 'w') as f: + f.write(manifest_content) + print("Created MANIFEST.in to ensure .pyd files are included") + + setup_content = f'''from setuptools import setup, find_packages +from setuptools.command.build_py import build_py as _build_py +from pathlib import Path +import os +import shutil +import glob + +# Read requirements +with open('requirements.txt', 'r') as f: + requirements = [line.strip() for line in f if line.strip() and not line.startswith('#')] + +# Find all packages +packages = find_packages(where="src") + +# Collect package data (JSON files and .pyd/.so files) +# IMPORTANT: .pyd/.so files with version tags (e.g., sensor.cp310-win_amd64.pyd) +# are NOT automatically included by setuptools, so we must explicitly list them +package_data = {{}} +for pkg in packages: + pkg_path = Path('src') / pkg.replace('.', '/') + if pkg_path.exists(): + data_files = [] + + # Explicitly include compiled extensions (.pyd/.so) using glob pattern + # This ensures all .pyd files are included regardless of version tags + for pattern in ['*{EXT_EXTENSION}']: + for ext_file in pkg_path.glob(pattern): + rel_path = ext_file.relative_to(pkg_path) + data_files.append(str(rel_path)) + # Also check subdirectories + for ext_file in pkg_path.rglob(pattern): + rel_path = ext_file.relative_to(pkg_path) + if str(rel_path) not in data_files: + data_files.append(str(rel_path)) + + # Include JSON files from configs directory + if pkg == 'orisys.configs': + for json_file in pkg_path.glob('*.json'): + rel_path = json_file.relative_to(pkg_path) + data_files.append(str(rel_path)) + + # For orisys package, include configs subdirectory JSON files + if pkg == 'orisys': + configs_dir = pkg_path / 'configs' + if configs_dir.exists() and configs_dir.is_dir(): + for json_file in configs_dir.glob('*.json'): + rel_path = json_file.relative_to(pkg_path) + data_files.append(str(rel_path)) + + if data_files: + package_data[pkg] = data_files + print(f"Package {{pkg}} package_data: {{len(data_files)}} files") + +# Custom build_py to ensure .pyd files are copied +class build_py(_build_py): + def run(self): + """Override to ensure .pyd files are included""" + super().run() + # After building, verify .pyd files are in the build directory + for pkg in packages: + pkg_path = Path('src') / pkg.replace('.', '/') + build_pkg_path = Path(self.build_lib) / pkg.replace('.', '/') + if pkg_path.exists() and build_pkg_path.exists(): + # Copy any .pyd files that might have been missed + for ext_file in pkg_path.glob('*{EXT_EXTENSION}'): + build_ext_file = build_pkg_path / ext_file.name + if not build_ext_file.exists(): + shutil.copy(ext_file, build_ext_file) + print(f" Copied {{ext_file.name}} to build directory") + +setup( + name="orisys", + version="{VERSION}", + description="Orisys - Optical Tactile Sensor SDK (Compiled)", + author="Your Name", + package_dir={{"": "src"}}, + packages=packages, + package_data=package_data, + include_package_data=True, + install_requires=requirements, + python_requires="{version_constraint}", + zip_safe=False, + cmdclass={{"build_py": build_py}}, + options={{ + 'bdist_wheel': {{ + 'python_tag': '{py_tag}', + }} + }}, +) +''' + + with open('dist_pyd/setup.py', 'w') as f: + f.write(setup_content) + + print("Package setup.py created successfully!") + + +def build_wheel_for_version(python_version): + """Build the wheel distribution for a specific Python version.""" + print(f"\n{'='*60}") + print(f"Building wheel for Python {python_version} on {PLATFORM_NAME}") + print('='*60) + + python_cmd = get_python_executable(python_version) + + if not python_cmd: + print(f"⚠️ Python {python_version} not found, skipping...") + return False + + # Get platform tag + plat_tag = get_platform_tag() + py_tag = f'py{python_version.replace(".", "")}' + + os.chdir('dist_pyd') + + # Build the wheel + cmd = python_cmd + ['setup.py', 'bdist_wheel', + '--python-tag', py_tag, + '--plat-name', plat_tag, + '--dist-dir', f'../dist/{PLATFORM_NAME}'] + print(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + + os.chdir('..') + + if result.returncode != 0: + print(f"❌ Error building wheel:\n{result.stderr}") + if result.stdout: + print(f"stdout:\n{result.stdout}") + return False + + print(f"✅ Wheel for Python {python_version} built successfully!") + print(f" Expected: orisys-{VERSION}-{py_tag}-none-{plat_tag}.whl") + return True + + +def get_platform_tag(): + """Get the platform tag for wheel naming.""" + system = platform.system() + machine = platform.machine().lower() + + if system == 'Windows': + if machine in ('amd64', 'x86_64'): + return 'win_amd64' + elif machine in ('x86', 'i386', 'i686'): + return 'win32' + elif machine in ('arm64', 'aarch64'): + return 'win_arm64' + elif system == 'Linux': + if machine in ('x86_64', 'amd64'): + return 'manylinux2014_x86_64' + elif machine in ('aarch64', 'arm64'): + return 'manylinux2014_aarch64' + elif machine in ('i386', 'i686'): + return 'manylinux2014_i686' + elif system == 'Darwin': + if machine == 'arm64': + return 'macosx_11_0_arm64' + else: + return 'macosx_10_9_x86_64' + + return 'any' + + +def copy_additional_files(): + """Copy non-Python files.""" + print("\n=== Copying additional files ===") + + # Copy requirements.txt + if os.path.exists('requirements.txt'): + shutil.copy('requirements.txt', 'dist_pyd/requirements.txt') + print("Copied requirements.txt") + + # Copy README and LICENSE if they exist + for file in ['README.md', 'LICENSE']: + if os.path.exists(file): + shutil.copy(file, f'dist_pyd/{file}') + print(f"Copied {file}") + + +def main(): + """Main build process.""" + import argparse + + parser = argparse.ArgumentParser(description='Build orisys package with Cython (.pyd/.so files)') + parser.add_argument('--versions', nargs='+', default=PYTHON_VERSIONS, + help='Python versions to build for') + + args = parser.parse_args() + + current_py_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + print("=" * 60) + print("Building Orisys Package with Cython (.pyd/.so files)") + print(f"Platform: {PLATFORM_NAME} ({platform.system()})") + print(f"Current Python: {current_py_version} ({sys.executable})") + print(f"Python Versions: {', '.join(args.versions)}") + print("=" * 60) + + # Check Cython + if not check_cython(): + print("\n❌ Build failed: Cython is required!") + print(" Install with: pip install cython") + sys.exit(1) + + # Check C compiler + if not check_c_compiler(): + print("\n❌ Build failed: C compiler is required!") + print(" Please install the required build tools (see above)") + sys.exit(1) + + # Ensure we're in the right directory + script_dir = Path(__file__).parent + os.chdir(script_dir) + + # Step 1: Clean previous builds + clean_build_dirs() + + # Step 2: Create Cython setup files + py_files = create_cython_setup_files() + if not py_files: + print("\n❌ No Python files found to compile!") + sys.exit(1) + + # Step 3: Copy additional files + copy_additional_files() + + # Step 4: Compile for each Python version + success_count = 0 + for version in args.versions: + # Compile with Cython + if compile_with_cython(version): + # Create package setup.py + create_package_setup(version) + # Build wheel + if build_wheel_for_version(version): + success_count += 1 + + if success_count == 0: + print("\n❌ Build failed - no wheels created!") + sys.exit(1) + + print(f"\n✅ Successfully built {success_count}/{len(args.versions)} wheels") + print("\n" + "=" * 60) + print("✅ Build completed successfully!") + print("=" * 60) + print(f"\nPlatform: {PLATFORM_NAME}") + print(f"Compiled source: dist_pyd/") + print(f"Distribution packages: dist/{PLATFORM_NAME}/") + print("\nGenerated wheels:") + for wheel in Path(f'dist/{PLATFORM_NAME}').glob('*.whl'): + print(f" - {wheel.name}") + print("\nInstall with:") + print(f" pip install dist/{PLATFORM_NAME}/orisys-{VERSION}-*.whl") + print("\nNote: The wheel contains .pyd/.so files instead of .py files!") + + +if __name__ == '__main__': + main() + diff --git a/example1_basic_pipeline.py b/example1_basic_pipeline.py index c588dc3..b6aeb68 100644 --- a/example1_basic_pipeline.py +++ b/example1_basic_pipeline.py @@ -19,10 +19,10 @@ def main(): # 步骤1:创建传感器对象 (必要步骤),config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径) # 方式1: 从视频文件读取 - # sensor = orisys.Sensor("./s1.mp4", isstitch=True, cuda=True, verbose=True,config_name="prototype") + sensor = orisys.Sensor("./s1.mp4", isstitch=True, cuda=True, verbose=True,config_name="prototype") # 方式2: 从摄像头读取(取消注释使用) - sensor = orisys.Sensor(0, cuda=True, verbose=True, config_name="prototype") + # sensor = orisys.Sensor(0, cuda=True, verbose=True, config_name="prototype") print("\n按 'q' 退出程序,按 'r' 重置光流追踪器") diff --git a/src/orisys/UnstructuredGrid.py b/src/orisys/UnstructuredGrid.py index 698247a..ede15bf 100644 --- a/src/orisys/UnstructuredGrid.py +++ b/src/orisys/UnstructuredGrid.py @@ -284,6 +284,8 @@ class UnstructuredGrid(object): self.nsimplices = self.simplices.shape[0] if self.dim != self.simplices.shape[1]-1: + pdim = self.dim + sdim = self.simplices.shape[1]-1 raise SyntaxError("Dimension mismatch! pdim = "+str(pdim)+" and sdim = "+str(sdim)+" do not match!") self.adjacent_faces = [] diff --git a/src/orisys/nPoisson.py b/src/orisys/nPoisson.py index 4bcee3b..4ae9cb0 100644 --- a/src/orisys/nPoisson.py +++ b/src/orisys/nPoisson.py @@ -296,7 +296,7 @@ class PoissonSolver(object): def solve(self, f, verbose=False): if not self.ready: - init(verbose) + self.prepare(verbose) fshape = f.shape @@ -506,7 +506,8 @@ class PoissonSolver(object): print(' - GPU solver not optimized for unstructured points, using CPU') return [self.solve(f, verbose) for f in f_list] - return results + # If we reach here, it's an unsupported case + raise ValueError(f"Unsupported point type '{self.ptype}' for GPU batch solver") def clear_gpu_cache(self): """Clear GPU cache to free GPU memory"""