diff --git a/scripts/compile.py b/scripts/compile.py new file mode 100755 index 0000000..77e787a --- /dev/null +++ b/scripts/compile.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +import os +import sys +import shutil +from setuptools import setup, Extension +from Cython.Build import cythonize + + +def build_extensions(target_dirs): + """ + Compiles all .py files in the target directories (recursively) into .so/.pyd extensions. + Args: + target_dirs (list or str): List of directories or single directory path to scan. + """ + # Ensure input is a list + if isinstance(target_dirs, str): + target_dirs = [target_dirs] + + extensions = [] + project_root = os.getcwd() + + print(f"Scanning directories: {target_dirs}") + + for target_dir in target_dirs: + # Ensure target directory exists + if not os.path.exists(target_dir): + print(f"Warning: Directory '{target_dir}' not found. Skipping.") + continue + + # Get the absolute path of the target directory + abs_target_dir = os.path.abspath(target_dir) + + print(f"Scanning {abs_target_dir} for Python files...") + + # Walk through the directory + for root, dirs, files in os.walk(abs_target_dir): + for file in files: + if file.endswith(".py"): + file_path = os.path.join(root, file) + + # Skip this script if it happens to be in the target dir + if os.path.abspath(file_path) == os.path.abspath(__file__): + continue + + # Skip setup.py if it exists + if file == "setup.py": + continue + + # Determine the module name based on path relative to project root + # This ensures imports like 'from app.services import ...' work + try: + rel_path = os.path.relpath(file_path, project_root) + except ValueError: + # If file is not under project root, we can't easily determine module name + # relative to project root. Skip or warn. + print( + f"Skipping {file_path}: cannot determine relative path to {project_root}" + ) + continue + + # Convert file path to module name (e.g. app/services/foo.py -> app.services.foo) + module_name = os.path.splitext(rel_path)[0].replace(os.sep, ".") + + print(f"Found: {rel_path} -> {module_name}") + + extensions.append(Extension(module_name, [file_path])) + + if not extensions: + print("No Python files found to compile.") + return + + print(f"\nCompiling {len(extensions)} modules...") + + # Build options + # compiler_directives: language_level=3 for Python 3 + # force=True: force recompilation even if timestamps are up to date + try: + setup( + ext_modules=cythonize( + extensions, + compiler_directives={"language_level": "3"}, + build_dir="build", # Put intermediate files in build/ directory + force=True, + ), + script_args=["build_ext", "--inplace"], + ) + print("\nCompilation successful!") + except Exception as e: + print(f"\nCompilation failed: {e}") + sys.exit(1) + finally: + # Cleanup build directory (intermediate C files) + if os.path.exists("build"): + print("Cleaning up build artifacts...") + try: + shutil.rmtree("build") + except OSError as e: + print(f"Warning: Failed to clean up build directory: {e}") + + +if __name__ == "__main__": + # Default directories to compile if none provided + DEFAULT_TARGETS = ["app/services", "app/native/wndb", "app/services"] + + # Check for help flag + if len(sys.argv) > 1 and sys.argv[1] in ["--help", "-h"]: + print("Usage: python scripts/build_extensions.py [directory1] [directory2] ...") + print( + "Compiles all Python files in the given directories into C extensions (.so/.pyd)." + ) + print(f"Default directories if none provided: {DEFAULT_TARGETS}") + sys.exit(0) + + if len(sys.argv) > 1: + target_directories = sys.argv[1:] + else: + print(f"No directories specified. Using defaults: {DEFAULT_TARGETS}") + target_directories = DEFAULT_TARGETS + + build_extensions(target_directories)