clean up
This commit is contained in:
@@ -1,520 +0,0 @@
|
|||||||
"""
|
|
||||||
Build script to create PyArmor-obfuscated distributions for multiple Python versions.
|
|
||||||
|
|
||||||
This script builds separate wheels for Python 3.8, 3.9, and 3.10 for Windows and Linux.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import platform
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
# 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'
|
|
||||||
|
|
||||||
|
|
||||||
def clean_build_dirs():
|
|
||||||
"""Remove previous build artifacts."""
|
|
||||||
dirs_to_clean = ['build', 'dist', 'orisys.egg-info', 'dist_obfuscated']
|
|
||||||
for dir_name in dirs_to_clean:
|
|
||||||
if os.path.exists(dir_name):
|
|
||||||
print(f"Cleaning {dir_name}...")
|
|
||||||
shutil.rmtree(dir_name)
|
|
||||||
|
|
||||||
# Ensure dist directory exists with platform subdirectories
|
|
||||||
os.makedirs(f'dist/{PLATFORM_NAME}', exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_python_executable(version):
|
|
||||||
"""Get the Python executable for a specific version based on platform."""
|
|
||||||
# First check if current Python matches the requested version
|
|
||||||
current_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
||||||
if current_version == version:
|
|
||||||
return [sys.executable]
|
|
||||||
|
|
||||||
if IS_WINDOWS:
|
|
||||||
# Windows paths - including conda environments
|
|
||||||
username = os.environ.get("USERNAME", "")
|
|
||||||
possible_paths = [
|
|
||||||
f'py -{version}', # Windows py launcher
|
|
||||||
f'python{version}', # Direct version command
|
|
||||||
# Conda environments with various naming patterns
|
|
||||||
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',
|
|
||||||
# Standard Python installations
|
|
||||||
f'C:\\Python{version.replace(".", "")}\\python.exe',
|
|
||||||
f'C:\\Program Files\\Python{version.replace(".", "")}\\python.exe',
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
# Linux/Unix paths
|
|
||||||
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:
|
|
||||||
# Expand user home directory
|
|
||||||
if path.startswith('~'):
|
|
||||||
path = os.path.expanduser(path)
|
|
||||||
|
|
||||||
if IS_WINDOWS and path.startswith('py -'):
|
|
||||||
# Test Windows py launcher
|
|
||||||
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:
|
|
||||||
# Check if file exists first
|
|
||||||
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 obfuscate_code():
|
|
||||||
"""Obfuscate the source code using PyArmor (version-independent)."""
|
|
||||||
print("\n=== Obfuscating code with PyArmor ===")
|
|
||||||
|
|
||||||
# PyArmor 9.x generates version-independent obfuscated code
|
|
||||||
# Output directly to dist_obfuscated/src (PyArmor will create orisys subdirectory)
|
|
||||||
# Use --obf-code 1 to enable code obfuscation (default is 1, but explicit is better)
|
|
||||||
# Use --exclude to exclude __init__.py files that don't need obfuscation (optional)
|
|
||||||
if IS_WINDOWS:
|
|
||||||
cmd = 'pyarmor gen --output dist_obfuscated/src --recursive --obf-code 1 src/orisys'
|
|
||||||
print(f"Running: {cmd}")
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
|
|
||||||
else:
|
|
||||||
cmd = [
|
|
||||||
'pyarmor',
|
|
||||||
'gen',
|
|
||||||
'--output', 'dist_obfuscated/src',
|
|
||||||
'--recursive',
|
|
||||||
'--obf-code', '1',
|
|
||||||
'src/orisys'
|
|
||||||
]
|
|
||||||
print(f"Running: {' '.join(cmd)}")
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"Error obfuscating code:\n{result.stderr}")
|
|
||||||
print(f"stdout:\n{result.stdout}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("Code obfuscation successful!")
|
|
||||||
print(result.stdout)
|
|
||||||
|
|
||||||
# Verify obfuscation: check that obfuscated files exist and original .py files are obfuscated
|
|
||||||
obfuscated_dir = Path('dist_obfuscated/src/orisys')
|
|
||||||
if obfuscated_dir.exists():
|
|
||||||
py_files = list(obfuscated_dir.rglob('*.py'))
|
|
||||||
print(f"Found {len(py_files)} obfuscated .py files")
|
|
||||||
# Check a sample file to verify it's obfuscated (should contain PyArmor markers)
|
|
||||||
if py_files:
|
|
||||||
sample_file = py_files[0]
|
|
||||||
try:
|
|
||||||
with open(sample_file, 'rb') as f:
|
|
||||||
content = f.read(100) # Read first 100 bytes
|
|
||||||
# PyArmor obfuscated files typically start with specific markers
|
|
||||||
if b'__pyarmor__' in content or b'pyarmor' in content.lower():
|
|
||||||
print(f"✓ Verified: {sample_file.name} is obfuscated")
|
|
||||||
else:
|
|
||||||
print(f"⚠ Warning: {sample_file.name} may not be properly obfuscated")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not verify obfuscation: {e}")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def copy_additional_files():
|
|
||||||
"""Copy non-Python files that don't need obfuscation."""
|
|
||||||
print("\n=== Copying additional files ===")
|
|
||||||
|
|
||||||
# Ensure configs directory exists in obfuscated output
|
|
||||||
# IMPORTANT: Only copy JSON files, NOT .py files (those should be obfuscated by PyArmor)
|
|
||||||
configs_src = Path('src/orisys/configs')
|
|
||||||
configs_dst = Path('dist_obfuscated/src/orisys/configs')
|
|
||||||
|
|
||||||
if configs_src.exists():
|
|
||||||
print(f"Ensuring configs directory is copied (JSON files only)...")
|
|
||||||
if not configs_dst.exists():
|
|
||||||
configs_dst.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Only copy JSON files, not any .py files
|
|
||||||
json_files_src = list(configs_src.glob('*.json'))
|
|
||||||
json_files_dst = list(configs_dst.glob('*.json'))
|
|
||||||
|
|
||||||
if len(json_files_dst) < len(json_files_src):
|
|
||||||
print(f"Copying JSON files from configs directory ({len(json_files_src)} files)")
|
|
||||||
for json_file in json_files_src:
|
|
||||||
shutil.copy(json_file, configs_dst / json_file.name)
|
|
||||||
else:
|
|
||||||
print(f"Configs directory already present with {len(json_files_dst)} JSON files")
|
|
||||||
|
|
||||||
# Copy config directory (if exists at root level)
|
|
||||||
if os.path.exists('config'):
|
|
||||||
print("Copying config directory...")
|
|
||||||
shutil.copytree('config', 'dist_obfuscated/config', dirs_exist_ok=True)
|
|
||||||
|
|
||||||
# Copy README and other metadata files
|
|
||||||
files_to_copy = ['README.md', 'requirements.txt', 'LICENSE']
|
|
||||||
for file in files_to_copy:
|
|
||||||
if os.path.exists(file):
|
|
||||||
print(f"Copying {file}...")
|
|
||||||
shutil.copy(file, f'dist_obfuscated/{file}')
|
|
||||||
|
|
||||||
# Verify that no original .py files are in dist_obfuscated (except obfuscated ones)
|
|
||||||
print("\n=== Verifying obfuscated output ===")
|
|
||||||
obfuscated_py_files = list(Path('dist_obfuscated/src/orisys').rglob('*.py'))
|
|
||||||
print(f"Found {len(obfuscated_py_files)} .py files in obfuscated output")
|
|
||||||
|
|
||||||
# Check if any files look like original source (not obfuscated)
|
|
||||||
original_like_files = []
|
|
||||||
for py_file in obfuscated_py_files:
|
|
||||||
# Skip __init__.py files as they might be simple
|
|
||||||
if py_file.name == '__init__.py':
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
with open(py_file, 'rb') as f:
|
|
||||||
content = f.read(500) # Read first 500 bytes
|
|
||||||
# Check for common Python source patterns that shouldn't be in obfuscated code
|
|
||||||
if b'def ' in content and b'__pyarmor__' not in content:
|
|
||||||
# Might be original source
|
|
||||||
if b'import ' in content or b'from ' in content:
|
|
||||||
# Check if it has readable imports (obfuscated code has different structure)
|
|
||||||
text_content = content.decode('utf-8', errors='ignore')
|
|
||||||
if 'import ' in text_content and '__pyarmor__' not in text_content:
|
|
||||||
original_like_files.append(py_file)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Could not check {py_file}: {e}")
|
|
||||||
|
|
||||||
if original_like_files:
|
|
||||||
print(f"⚠️ Warning: Found {len(original_like_files)} files that may not be obfuscated:")
|
|
||||||
for f in original_like_files[:5]: # Show first 5
|
|
||||||
print(f" - {f}")
|
|
||||||
if len(original_like_files) > 5:
|
|
||||||
print(f" ... and {len(original_like_files) - 5} more")
|
|
||||||
else:
|
|
||||||
print("✓ All .py files appear to be obfuscated")
|
|
||||||
|
|
||||||
|
|
||||||
def create_setup_for_obfuscated(python_version=None):
|
|
||||||
"""Create a modified setup.py for the obfuscated package."""
|
|
||||||
print(f"\n=== Creating setup.py for obfuscated package (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'
|
|
||||||
|
|
||||||
setup_content = f'''from setuptools import setup, find_packages
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# 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 including pyarmor_runtime
|
|
||||||
packages = find_packages(where="src")
|
|
||||||
|
|
||||||
# Collect all package data including PyArmor runtime files and configs
|
|
||||||
package_data = {{}}
|
|
||||||
for pkg in packages:
|
|
||||||
pkg_path = Path('src') / pkg.replace('.', '/')
|
|
||||||
if pkg_path.exists():
|
|
||||||
data_files = []
|
|
||||||
|
|
||||||
# Include all .pyd, .so, .dll files (PyArmor runtime)
|
|
||||||
for pattern in ['*.pyd', '*.so', '*.dll', '*.dylib']:
|
|
||||||
data_files.extend([str(f.relative_to(pkg_path)) for f in pkg_path.glob(pattern)])
|
|
||||||
|
|
||||||
# Include JSON files from configs directory (for orisys.configs package)
|
|
||||||
if pkg == 'orisys.configs':
|
|
||||||
# Include all JSON files in the configs package directory
|
|
||||||
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
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="orisys",
|
|
||||||
version="{VERSION}",
|
|
||||||
description="Orisys - Optical Tactile Sensor SDK (Protected)",
|
|
||||||
author="Your Name",
|
|
||||||
package_dir={{"": "src"}},
|
|
||||||
packages=packages,
|
|
||||||
package_data=package_data,
|
|
||||||
include_package_data=True,
|
|
||||||
install_requires=requirements,
|
|
||||||
python_requires="{version_constraint}",
|
|
||||||
# Include PyArmor runtime files
|
|
||||||
zip_safe=False,
|
|
||||||
# Exclude original source files - only include obfuscated files from dist_obfuscated
|
|
||||||
exclude_package_data={{'': ['*.pyc', '*.pyo', '__pycache__']}},
|
|
||||||
options={{
|
|
||||||
'bdist_wheel': {{
|
|
||||||
'python_tag': '{py_tag}',
|
|
||||||
}}
|
|
||||||
}},
|
|
||||||
)
|
|
||||||
'''
|
|
||||||
|
|
||||||
with open('dist_obfuscated/setup.py', 'w') as f:
|
|
||||||
f.write(setup_content)
|
|
||||||
|
|
||||||
print("setup.py created successfully!")
|
|
||||||
|
|
||||||
|
|
||||||
def create_manifest_in():
|
|
||||||
"""Create MANIFEST.in to ensure JSON files in configs are included and exclude original source."""
|
|
||||||
print("\n=== Creating MANIFEST.in ===")
|
|
||||||
|
|
||||||
manifest_content = """# Include all JSON files in configs directory
|
|
||||||
include src/orisys/configs/*.json
|
|
||||||
recursive-include src/orisys/configs *.json
|
|
||||||
|
|
||||||
# Include PyArmor runtime files
|
|
||||||
recursive-include src *.pyd
|
|
||||||
recursive-include src *.so
|
|
||||||
recursive-include src *.dll
|
|
||||||
recursive-include src *.dylib
|
|
||||||
|
|
||||||
# Exclude original source files (only obfuscated .py files should be included)
|
|
||||||
# The obfuscated .py files from PyArmor will be included automatically
|
|
||||||
# But we want to make sure no original source files are accidentally included
|
|
||||||
prune src/orisys
|
|
||||||
recursive-include src/orisys *.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
manifest_path = Path('dist_obfuscated/MANIFEST.in')
|
|
||||||
with open(manifest_path, 'w') as f:
|
|
||||||
f.write(manifest_content)
|
|
||||||
|
|
||||||
print("MANIFEST.in created successfully!")
|
|
||||||
print(manifest_content)
|
|
||||||
|
|
||||||
|
|
||||||
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 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 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())
|
|
||||||
|
|
||||||
# Get platform tag
|
|
||||||
plat_tag = get_platform_tag()
|
|
||||||
py_tag = f'py{python_version.replace(".", "")}'
|
|
||||||
|
|
||||||
os.chdir('dist_obfuscated')
|
|
||||||
|
|
||||||
# Build the wheel using setup.py with explicit tags
|
|
||||||
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 for Python {python_version}:\n{result.stderr}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"✅ Wheel for Python {python_version} on {PLATFORM_NAME} built successfully!")
|
|
||||||
print(f" Expected: orisys-{VERSION}-{py_tag}-none-{plat_tag}.whl")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def build_universal_wheel():
|
|
||||||
"""Build a single universal wheel compatible with all Python versions."""
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"Building Universal Wheel (Python 3.8-3.10) for {PLATFORM_NAME}")
|
|
||||||
print('='*60)
|
|
||||||
|
|
||||||
os.chdir('dist_obfuscated')
|
|
||||||
|
|
||||||
# Build the wheel
|
|
||||||
cmd = [sys.executable, '-m', 'pip', 'wheel', '.', '--no-deps', '-w', 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 universal wheel:\n{result.stderr}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"✅ Universal wheel for {PLATFORM_NAME} built successfully!")
|
|
||||||
print(result.stdout)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main build process."""
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Build orisys package with PyArmor')
|
|
||||||
parser.add_argument('--mode', choices=['universal', 'multi-version', 'current'], default='current',
|
|
||||||
help='Build mode: universal (one wheel for all), multi-version (separate wheels), current (use current Python only)')
|
|
||||||
parser.add_argument('--versions', nargs='+', default=PYTHON_VERSIONS,
|
|
||||||
help='Python versions to build for (only for multi-version mode)')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Get current Python version
|
|
||||||
current_py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print("Building Orisys Package with PyArmor Obfuscation")
|
|
||||||
print(f"Platform: {PLATFORM_NAME} ({platform.system()})")
|
|
||||||
print(f"Current Python: {current_py_version} ({sys.executable})")
|
|
||||||
|
|
||||||
if args.mode == 'current':
|
|
||||||
print(f"Mode: Building for current Python version ({current_py_version}) only")
|
|
||||||
args.versions = [current_py_version]
|
|
||||||
# Use multi-version logic but with current version only
|
|
||||||
build_mode = 'multi-version'
|
|
||||||
else:
|
|
||||||
print(f"Python Versions: {', '.join(args.versions)}")
|
|
||||||
print(f"Mode: {args.mode}")
|
|
||||||
build_mode = args.mode
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# 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: Obfuscate the code (once, version-independent)
|
|
||||||
if not obfuscate_code():
|
|
||||||
print("\n❌ Build failed during obfuscation!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Step 3: Copy additional files (including configs)
|
|
||||||
copy_additional_files()
|
|
||||||
|
|
||||||
# Step 3.5: Create MANIFEST.in to ensure JSON files are included
|
|
||||||
create_manifest_in()
|
|
||||||
|
|
||||||
# Step 4 & 5: Build based on mode
|
|
||||||
if build_mode == 'universal':
|
|
||||||
# Build one wheel for all Python versions
|
|
||||||
create_setup_for_obfuscated()
|
|
||||||
if not build_universal_wheel():
|
|
||||||
print("\n❌ Build failed!")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
# Build separate wheels for each Python version (or just current)
|
|
||||||
success_count = 0
|
|
||||||
for version in args.versions:
|
|
||||||
create_setup_for_obfuscated(version)
|
|
||||||
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"Obfuscated source: dist_obfuscated/")
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
-902
@@ -1,902 +0,0 @@
|
|||||||
"""
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 195 KiB |
Binary file not shown.
@@ -1,163 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
import cv2
|
|
||||||
from matplotlib import pyplot as plt
|
|
||||||
import orisys
|
|
||||||
|
|
||||||
sensor = orisys.Sensor(vid_src='stereo2.mp4',config_name='./config/stereo.json',mirror=False)
|
|
||||||
print(sensor.boxes)
|
|
||||||
sensor.get_img()
|
|
||||||
tiles = sensor.stitcher.tile_list
|
|
||||||
print(tiles)
|
|
||||||
|
|
||||||
imgL = tiles[0]
|
|
||||||
imgR = tiles[1]
|
|
||||||
|
|
||||||
|
|
||||||
# imgL = cv.imread('tsukuba_l.png', cv.IMREAD_GRAYSCALE)
|
|
||||||
# imgR = cv.imread('tsukuba_r.png', cv.IMREAD_GRAYSCALE)
|
|
||||||
|
|
||||||
plt.subplot(1,2,1)
|
|
||||||
plt.imshow(imgL,'gray')
|
|
||||||
plt.subplot(1,2,2)
|
|
||||||
plt.imshow(imgR,'gray')
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
# Feature matching for stereo computation
|
|
||||||
def match_features(img1, img2, method='SIFT', max_matches=50):
|
|
||||||
"""
|
|
||||||
Detect and match features between two images.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
img1: Left image (grayscale)
|
|
||||||
img2: Right image (grayscale)
|
|
||||||
method: 'SIFT' or 'ORB'
|
|
||||||
max_matches: Maximum number of matches to show
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
img_matches: Image with matches drawn
|
|
||||||
matches: List of matches
|
|
||||||
kp1, kp2: Keypoints in img1 and img2
|
|
||||||
"""
|
|
||||||
# Convert to grayscale if needed
|
|
||||||
if len(img1.shape) == 3:
|
|
||||||
img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
|
|
||||||
else:
|
|
||||||
img1_gray = img1.copy()
|
|
||||||
|
|
||||||
if len(img2.shape) == 3:
|
|
||||||
img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
|
|
||||||
else:
|
|
||||||
img2_gray = img2.copy()
|
|
||||||
|
|
||||||
# Initialize detector
|
|
||||||
if method == 'SIFT':
|
|
||||||
detector = cv2.SIFT_create()
|
|
||||||
elif method == 'ORB':
|
|
||||||
detector = cv2.ORB_create()
|
|
||||||
else:
|
|
||||||
raise ValueError("method must be 'SIFT' or 'ORB'")
|
|
||||||
|
|
||||||
# Detect keypoints and descriptors
|
|
||||||
kp1, des1 = detector.detectAndCompute(img1_gray, None)
|
|
||||||
kp2, des2 = detector.detectAndCompute(img2_gray, None)
|
|
||||||
|
|
||||||
if des1 is None or des2 is None or len(des1) == 0 or len(des2) == 0:
|
|
||||||
print(f"Warning: No features detected with {method}")
|
|
||||||
return None, [], kp1, kp2
|
|
||||||
|
|
||||||
# Match features
|
|
||||||
if method == 'SIFT':
|
|
||||||
# Check if we have enough descriptors for knnMatch (need at least k=2)
|
|
||||||
min_descriptors = min(len(des1), len(des2))
|
|
||||||
if min_descriptors < 2:
|
|
||||||
print(f"Warning: Not enough descriptors ({min_descriptors}) for knnMatch, using regular matching")
|
|
||||||
# Fallback to BFMatcher
|
|
||||||
matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False)
|
|
||||||
matches = matcher.match(des1, des2)
|
|
||||||
matches = sorted(matches, key=lambda x: x.distance)[:max_matches]
|
|
||||||
else:
|
|
||||||
# Use FLANN matcher for SIFT
|
|
||||||
try:
|
|
||||||
FLANN_INDEX_KDTREE = 1
|
|
||||||
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
|
|
||||||
search_params = dict(checks=50)
|
|
||||||
matcher = cv2.FlannBasedMatcher(index_params, search_params)
|
|
||||||
matches = matcher.knnMatch(des1, des2, k=2)
|
|
||||||
# Apply Lowe's ratio test
|
|
||||||
good_matches = []
|
|
||||||
for match_pair in matches:
|
|
||||||
if len(match_pair) == 2:
|
|
||||||
m, n = match_pair
|
|
||||||
if m.distance < 0.7 * n.distance:
|
|
||||||
good_matches.append(m)
|
|
||||||
matches = sorted(good_matches, key=lambda x: x.distance)[:max_matches]
|
|
||||||
except cv2.error:
|
|
||||||
# Fallback to BFMatcher if FLANN fails
|
|
||||||
print("Warning: FLANN matcher failed, using BFMatcher instead")
|
|
||||||
matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False)
|
|
||||||
matches = matcher.match(des1, des2)
|
|
||||||
matches = sorted(matches, key=lambda x: x.distance)[:max_matches]
|
|
||||||
else: # ORB
|
|
||||||
# Use BF matcher for ORB
|
|
||||||
matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
|
|
||||||
matches = matcher.match(des1, des2)
|
|
||||||
matches = sorted(matches, key=lambda x: x.distance)[:max_matches]
|
|
||||||
|
|
||||||
# Draw matches
|
|
||||||
img_matches = cv2.drawMatches(
|
|
||||||
img1_gray, kp1, img2_gray, kp2, matches, None,
|
|
||||||
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Found {len(matches)} good matches using {method}")
|
|
||||||
return img_matches, matches, kp1, kp2
|
|
||||||
|
|
||||||
# Perform feature matching
|
|
||||||
print("\n=== Feature Matching ===")
|
|
||||||
img_matches, matches, kp1, kp2 = match_features(imgL, imgR, method='SIFT', max_matches=50)
|
|
||||||
|
|
||||||
if img_matches is not None:
|
|
||||||
plt.figure(figsize=(15, 6))
|
|
||||||
plt.imshow(img_matches)
|
|
||||||
plt.title(f'Feature Matches (SIFT) - {len(matches)} matches')
|
|
||||||
plt.axis('off')
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
# Also create a side-by-side visualization with lines connecting matches
|
|
||||||
h1, w1 = imgL.shape[:2]
|
|
||||||
h2, w2 = imgR.shape[:2]
|
|
||||||
h_max = max(h1, h2)
|
|
||||||
combined = np.zeros((h_max, w1 + w2), dtype=np.uint8)
|
|
||||||
combined[:h1, :w1] = imgL
|
|
||||||
combined[:h2, w1:w1+w2] = imgR
|
|
||||||
combined_color = combined
|
|
||||||
|
|
||||||
# Draw lines connecting matched features
|
|
||||||
for match in matches[:30]: # Show top 30 matches
|
|
||||||
pt1 = tuple(map(int, kp1[match.queryIdx].pt))
|
|
||||||
pt2 = tuple(map(int, kp2[match.trainIdx].pt))
|
|
||||||
pt2 = (pt2[0] + w1, pt2[1])
|
|
||||||
cv2.line(combined_color, pt1, pt2, (0, 255, 0), 1)
|
|
||||||
cv2.circle(combined_color, pt1, 3, (0, 0, 255), -1)
|
|
||||||
cv2.circle(combined_color, pt2, 3, (255, 0, 0), -1)
|
|
||||||
|
|
||||||
plt.figure(figsize=(15, 6))
|
|
||||||
plt.imshow(cv2.cvtColor(combined_color, cv2.COLOR_BGR2RGB))
|
|
||||||
plt.title(f'Stereo Feature Matching - {len(matches[:30])} matches shown')
|
|
||||||
plt.axis('off')
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
# Stereo disparity computation
|
|
||||||
print("\n=== Stereo Disparity Computation ===")
|
|
||||||
stereo = cv2.StereoBM.create(numDisparities=32, blockSize=15)
|
|
||||||
disparity = stereo.compute(imgL, imgR)
|
|
||||||
plt.figure(figsize=(12, 5))
|
|
||||||
plt.imshow(disparity, cmap='jet')
|
|
||||||
plt.colorbar(label='Disparity')
|
|
||||||
plt.title('Stereo Disparity Map')
|
|
||||||
plt.axis('off')
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user