""" The LLVM Compiler Infrastructure This file is distributed under the University of Illinois Open Source License. See LICENSE.TXT for details. Python binding preparation script. """ # Python modules: from __future__ import print_function import logging import os import re import shutil import subprocess import sys import platform class SwigSettings(object): """Provides a single object to represent swig files and settings.""" def __init__(self): self.extensions_file = None self.header_files = None self.input_file = None self.interface_files = None self.output_file = None self.safecast_file = None self.typemaps_file = None self.wrapper_file = None @classmethod def _any_files_newer(cls, files, check_mtime): """Returns if any of the given files has a newer modified time. @param cls the class @param files a list of zero or more file paths to check @param check_mtime the modification time to use as a reference. @return True if any file's modified time is newer than check_mtime. """ for path in files: path_mtime = os.path.getmtime(path) if path_mtime > check_mtime: # This path was modified more recently than the # check_mtime. return True # If we made it here, nothing was newer than the check_mtime return False @classmethod def _file_newer(cls, path, check_mtime): """Tests how recently a file has been modified. @param cls the class @param path a file path to check @param check_mtime the modification time to use as a reference. @return True if the file's modified time is newer than check_mtime. """ path_mtime = os.path.getmtime(path) return path_mtime > check_mtime def output_out_of_date(self): """Returns whether the output file is out of date. Compares output file time to all the input files. @return True if any of the input files are newer than the output file, or if the output file doesn't exist; False otherwise. """ if not os.path.exists(self.output_file): logging.info("will generate, missing binding output file") return True output_mtime = os.path.getmtime(self.output_file) if self._any_files_newer(self.header_files, output_mtime): logging.info("will generate, header files newer") return True if self._any_files_newer(self.interface_files, output_mtime): logging.info("will generate, interface files newer") return True if self._file_newer(self.input_file, output_mtime): logging.info("will generate, swig input file newer") return True if self._file_newer(self.extensions_file, output_mtime): logging.info("will generate, swig extensions file newer") return True if self._file_newer(self.wrapper_file, output_mtime): logging.info("will generate, swig wrapper file newer") return True if self._file_newer(self.typemaps_file, output_mtime): logging.info("will generate, swig typemaps file newer") return True if self._file_newer(self.safecast_file, output_mtime): logging.info("will generate, swig safecast file newer") return True # If we made it here, nothing is newer than the output file. # Thus, the output file is not out of date. return False def get_header_files(options): """Returns a list of paths to C++ header files for the LLDB API. These are the files that define the C++ API that will be wrapped by Python. @param options the dictionary of options parsed from the command line. @return a list of full paths to the include files used to define the public LLDB C++ API. """ header_file_paths = [] header_base_dir = os.path.join(options.src_root, "include", "lldb") # Specify the include files in include/lldb that are not easy to # grab programatically. for header in [ "lldb-defines.h", "lldb-enumerations.h", "lldb-forward.h", "lldb-types.h"]: header_file_paths.append(os.path.normcase( os.path.join(header_base_dir, header))) # Include the main LLDB.h file. api_dir = os.path.join(header_base_dir, "API") header_file_paths.append(os.path.normcase( os.path.join(api_dir, "LLDB.h"))) filename_regex = re.compile(r"^SB.+\.h$") # Include all the SB*.h files in the API dir. for filename in os.listdir(api_dir): if filename_regex.match(filename): header_file_paths.append( os.path.normcase(os.path.join(api_dir, filename))) logging.debug("found public API header file paths: %s", header_file_paths) return header_file_paths def get_interface_files(options): """Returns a list of interface files used as input to swig. @param options the options dictionary parsed from the command line args. @return a list of full paths to the interface (.i) files used to describe the public API language binding. """ interface_file_paths = [] interface_dir = os.path.join(options.src_root, "scripts", "interface") for filepath in [f for f in os.listdir(interface_dir) if os.path.splitext(f)[1] == ".i"]: interface_file_paths.append( os.path.normcase(os.path.join(interface_dir, filepath))) logging.debug("found swig interface files: %s", interface_file_paths) return interface_file_paths def remove_ignore_enoent(filename): """Removes given file, ignoring error if it doesn't exist. @param filename the path of the file to remove. """ try: os.remove(filename) except OSError as error: import errno if error.errno != errno.ENOENT: raise def do_swig_rebuild(options, dependency_file, config_build_dir, settings): """Generates Python bindings file from swig. This method will do a sys.exit() if something fails. If it returns to the caller, it succeeded. @param options the parsed command line options structure. @param dependency_file path to the bindings dependency file to be generated; otherwise, None if a dependency file is not to be generated. @param config_build_dir used as the output directory used by swig @param settings the SwigSettings that specify a number of aspects used to configure building the Python binding with swig (mostly paths) """ if options.generate_dependency_file: temp_dep_file_path = dependency_file + ".tmp" # Build the SWIG args list is_darwin = options.target_platform == "Darwin" gen_deps = options.generate_dependency_file darwin_extras = ["-D__APPLE__"] if is_darwin else [] deps_args = ["-MMD", "-MF", temp_dep_file_path] if gen_deps else [] command = ([ options.swig_executable, "-c++", "-shadow", "-python", "-threads", "-I" + os.path.normpath(os.path.join(options.src_root, "include")), "-I" + os.path.curdir, "-D__STDC_LIMIT_MACROS", "-D__STDC_CONSTANT_MACROS" ] + darwin_extras + deps_args + [ "-outdir", config_build_dir, "-o", settings.output_file, settings.input_file ] ) logging.info("running swig with: %r", command) # Execute swig process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Wait for SWIG process to terminate swig_stdout, swig_stderr = process.communicate() return_code = process.returncode if return_code != 0: logging.error( "swig failed with error code %d: stdout=%s, stderr=%s", return_code, swig_stdout, swig_stderr) logging.error( "command line:\n%s", ' '.join(command)) sys.exit(return_code) logging.info("swig generation succeeded") if swig_stdout is not None and len(swig_stdout) > 0: logging.info("swig output: %s", swig_stdout) # Move the depedency file we just generated to the proper location. if options.generate_dependency_file: if os.path.exists(temp_dep_file_path): shutil.move(temp_dep_file_path, dependency_file) else: logging.error( "failed to generate Python binding depedency file '%s'", temp_dep_file_path) if os.path.exists(dependency_file): # Delete the old one. os.remove(dependency_file) sys.exit(-10) def run_python_script(script_and_args): """Runs a python script, logging appropriately. If the command returns anything non-zero, it is registered as an error and exits the program. @param script_and_args the python script to execute, along with the command line arguments to pass to it. """ command = [sys.executable] + script_and_args process = subprocess.Popen(command) script_stdout, script_stderr = process.communicate() return_code = process.returncode if return_code != 0: logging.error("failed to run %r: %r", command, script_stderr) sys.exit(return_code) else: logging.info("ran script %r'", command) if script_stdout is not None: logging.info("output: %s", script_stdout) def do_modify_python_lldb(options, config_build_dir): """Executes the modify-python-lldb.py script. @param options the parsed command line arguments @param config_build_dir the directory where the Python output was created. """ script_path = os.path.normcase( os.path.join( options.src_root, "scripts", "Python", "modify-python-lldb.py")) if not os.path.exists(script_path): logging.error("failed to find python script: '%s'", script_path) sys.exit(-11) run_python_script([script_path, config_build_dir]) def get_python_module_path(options): """Returns the location where the lldb Python module should be placed. @param options dictionary of options parsed from the command line. @return the directory where the lldb module should be placed. """ if options.framework: # Caller wants to use the OS X framework packaging. # We are packaging in an OS X-style framework bundle. The # module dir will be within the # LLDB.framework/Resources/Python subdirectory. return os.path.join( options.target_dir, "LLDB.framework", "Resources", "Python", "lldb") else: from distutils.sysconfig import get_python_lib if options.prefix is not None: module_path = get_python_lib(True, False, options.prefix) else: module_path = get_python_lib(True, False) return os.path.normcase( os.path.join(module_path, "lldb")) def main(options): """Pepares the Python language binding to LLDB. @param options the parsed command line argument dictionary """ # Setup generated dependency file options. if options.generate_dependency_file: dependency_file = os.path.normcase(os.path.join( options.target_dir, "LLDBWrapPython.cpp.d")) else: dependency_file = None # Keep track of all the swig-related settings. settings = SwigSettings() # Determine the final binding file path. settings.output_file = os.path.normcase( os.path.join(options.target_dir, "LLDBWrapPython.cpp")) # Touch the output file (but don't really generate it) if python # is disabled. disable_python = os.getenv("LLDB_DISABLE_PYTHON", None) if disable_python is not None and disable_python == "1": remove_ignore_enoent(settings.output_file) # Touch the file. open(settings.output_file, 'w').close() logging.info( "Created empty python binding file due to LLDB_DISABLE_PYTHON " "being set") return # We also check the GCC_PREPROCESSOR_DEFINITIONS to see if it # contains LLDB_DISABLE_PYTHON. If so, we skip generating # the binding. gcc_preprocessor_defs = os.getenv("GCC_PREPROCESSOR_DEFINITIONS", None) if gcc_preprocessor_defs is not None: if re.search(r"LLDB_DISABLE_PYTHON", gcc_preprocessor_defs): remove_ignore_enoent(settings.output_file) # Touch the file open(settings.output_file, 'w').close() logging.info( "Created empty python binding file due to " "finding LLDB_DISABLE_PYTHON in GCC_PREPROCESSOR_DEFINITIONS") return # Setup paths used during swig invocation. settings.input_file = os.path.normcase( os.path.join(options.src_root, "scripts", "lldb.swig")) scripts_python_dir = os.path.dirname(os.path.realpath(__file__)) settings.extensions_file = os.path.normcase( os.path.join(scripts_python_dir, "python-extensions.swig")) settings.wrapper_file = os.path.normcase( os.path.join(scripts_python_dir, "python-wrapper.swig")) settings.typemaps_file = os.path.normcase( os.path.join(scripts_python_dir, "python-typemaps.swig")) settings.safecast_file = os.path.normcase( os.path.join(scripts_python_dir, "python-swigsafecast.swig")) settings.header_files = get_header_files(options) settings.interface_files = get_interface_files(options) generate_output = settings.output_out_of_date() # Determine where to put the module. python_module_path = get_python_module_path(options) logging.info("python module path: %s", python_module_path) # Handle the configuration build dir. if options.config_build_dir is not None: config_build_dir = options.config_build_dir else: config_build_dir = python_module_path # Allow missing/non-link _lldb.so to force regeneration. if not generate_output: # Ensure the _lldb.so file exists. so_path = os.path.join(python_module_path, "_lldb.so") if not os.path.exists(so_path) or not os.path.islink(so_path): logging.info("_lldb.so doesn't exist or isn't a symlink") generate_output = True # Allow missing __init__.py to force regeneration. if not generate_output: # Ensure the __init__.py for the lldb module can be found. init_path = os.path.join(python_module_path, "__init__.py") if not os.path.exists(init_path): logging.info("__init__.py doesn't exist") generate_output = True if not generate_output: logging.info( "Skipping Python binding generation: everything is up to date") return # Generate the Python binding with swig. logging.info("Python binding is out of date, regenerating") do_swig_rebuild(options, dependency_file, config_build_dir, settings) if options.generate_dependency_file: return # Post process the swig-generated file. do_modify_python_lldb(options, config_build_dir) # This script can be called by another Python script by calling the main() # function directly if __name__ == "__main__": print("Script cannot be called directly.") sys.exit(-1)