""" 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()