diff options
Diffstat (limited to 'lldb')
-rw-r--r-- | lldb/lldb.xcodeproj/project.pbxproj | 10 | ||||
-rw-r--r-- | lldb/scripts/Python/prepare_binding_Python.py | 435 | ||||
-rwxr-xr-x | lldb/scripts/prepare_bindings.py | 196 |
3 files changed, 636 insertions, 5 deletions
diff --git a/lldb/lldb.xcodeproj/project.pbxproj b/lldb/lldb.xcodeproj/project.pbxproj index 5b7cc9a1590..10d9c88a6e0 100644 --- a/lldb/lldb.xcodeproj/project.pbxproj +++ b/lldb/lldb.xcodeproj/project.pbxproj @@ -5984,7 +5984,7 @@ isa = PBXNativeTarget; buildConfigurationList = 2668020B115FD0EE008E1FE4 /* Build configuration list for PBXNativeTarget "LLDB" */; buildPhases = ( - 26DC6A5813380D4300FF7998 /* Build swig wrapper classes */, + 26DC6A5813380D4300FF7998 /* Prepare Swig Bindings */, 26680202115FD0ED008E1FE4 /* Headers */, 26680203115FD0ED008E1FE4 /* Resources */, 26680204115FD0ED008E1FE4 /* Sources */, @@ -6214,19 +6214,19 @@ shellPath = /bin/sh; shellScript = "perl $SRCROOT/scripts/build-llvm.pl"; }; - 26DC6A5813380D4300FF7998 /* Build swig wrapper classes */ = { + 26DC6A5813380D4300FF7998 /* Prepare Swig Bindings */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Build swig wrapper classes"; + name = "Prepare Swig Bindings"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "$SRCROOT/scripts/build-swig-wrapper-classes.sh $SRCROOT $TARGET_BUILD_DIR $CONFIGURATION_BUILD_DIR \"\"\n"; + shellPath = /bin/bash; + shellScript = "/usr/bin/python $SRCROOT/scripts/prepare_bindings.py --framework --src-root $SRCROOT --target-dir $TARGET_BUILD_DIR --config-build-dir $CONFIGURATION_BUILD_DIR --swig-executable `which swig`"; }; 4959511A1A1ACE9500F6F8FC /* Install Clang compiler headers */ = { isa = PBXShellScriptBuildPhase; diff --git a/lldb/scripts/Python/prepare_binding_Python.py b/lldb/scripts/Python/prepare_binding_Python.py new file mode 100644 index 00000000000..1996841baf1 --- /dev/null +++ b/lldb/scripts/Python/prepare_binding_Python.py @@ -0,0 +1,435 @@ +""" + 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 + + +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 + command = [ + options.swig_executable, + "-c++", + "-shadow", + "-python", + "-threads", + "-I\"%s\"" % os.path.normcase( + os.path.join(options.src_root, "include")), + "-I\"%s\"" % os.path.normcase("./."), + "-D__STDC_LIMIT_MACROS", + "-D__STDC_CONSTANT_MACROS"] + if options.generate_dependency_file: + command.append("-MMD -MF \"%s\"" % temp_dep_file_path) + command.extend([ + "-outdir", "\"%s\"" % config_build_dir, + "-o", "\"%s\"" % settings.output_file, + "\"%s\"" % settings.input_file + ]) + logging.info("running swig with: %s", command) + + # Execute swig + process = subprocess.Popen( + ' '.join(command), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True) + # 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_line = "%s %s" % (sys.executable, script_and_args) + process = subprocess.Popen(command_line, shell=True) + script_stdout, script_stderr = process.communicate() + return_code = process.returncode + if return_code != 0: + logging.error("failed to run '%s': %s", command_line, script_stderr) + sys.exit(return_code) + else: + logging.info("ran script '%s'", command_line) + 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) + + script_invocation = "%s %s" % (script_path, config_build_dir) + run_python_script(script_invocation) + + +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) diff --git a/lldb/scripts/prepare_bindings.py b/lldb/scripts/prepare_bindings.py new file mode 100755 index 00000000000..58b33fd568a --- /dev/null +++ b/lldb/scripts/prepare_bindings.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +""" + The LLVM Compiler Infrastructure + +This file is distributed under the University of Illinois Open Source +License. See LICENSE.TXT for details. + +Prepares language bindings for LLDB build process. Run with --help +to see a description of the supported command line arguments. +""" + +# Python modules: +import argparse +import logging +import os +import sys + + +def prepare_binding_for_language(scripts_dir, script_lang, options): + """Prepares the binding for a specific language. + + @param scripts_dir the full path to the scripts source directory. + @param script_lang the name of the script language. Should be a child + directory within the scripts dir, and should contain a + prepare_scripts_{script_lang}.py script file in it. + @param options the dictionary of parsed command line options. + + There is no return value. If it returns, the process succeeded; otherwise, + the process will exit where it fails. + """ + # Ensure the language-specific prepare module exists. + script_name = "prepare_binding_{}.py".format(script_lang) + lang_path = os.path.join(scripts_dir, script_lang) + script_path = os.path.join(lang_path, script_name) + if not os.path.exists(script_path): + logging.error( + "failed to find prepare script for language '%s' at '%s'", + script_lang, + script_path) + sys.exit(-9) + + # Include this language-specific directory in the Python search + # path. + sys.path.append(os.path.normcase(lang_path)) + + # Execute the specific language script + module_name = os.path.splitext(script_name)[0] + module = __import__(module_name) + module.main(options) + + # Remove the language-specific directory from the Python search path. + sys.path.remove(os.path.normcase(lang_path)) + + +def prepare_all_bindings(options): + """Prepares bindings for each of the languages supported. + + @param options the parsed arguments from the command line + + @return the exit value for the program. 0 is success, all othes + indicate some kind of failure. + """ + # Check for the existence of the SWIG scripts folder + scripts_dir = os.path.join(options.src_root, "scripts") + if not os.path.exists(scripts_dir): + logging.error("failed to find scripts dir: '%s'", scripts_dir) + sys.exit(-8) + + # Collect list of child directories. We expect there to be one + # for each supported script language. + child_dirs = [f for f in os.listdir(scripts_dir) + if os.path.isdir(os.path.join(scripts_dir, f))] + + # Remove directories that do not represent script languages. + for removal_dir in [".svn", "interface", "__pycache__", "sphinx"]: + if removal_dir in child_dirs: + child_dirs.remove(removal_dir) + + logging.info("found script directories: %s", child_dirs) + + # Iterate script directory find any script language directories + for script_lang in child_dirs: + logging.info("executing language script for: '%s'", script_lang) + prepare_binding_for_language(scripts_dir, script_lang, options) + + +def process_args(args): + """Returns options processed from the provided command line. + + @param args the command line to process. + """ + + # Setup the parser arguments that are accepted. + parser = argparse.ArgumentParser( + description="Prepare language bindings for LLDB build.") + + # Arguments to control logging verbosity. + parser.add_argument( + "--debug", "-d", + action="store_true", + help="Set program logging level to DEBUG.") + parser.add_argument( + "--verbose", "-v", + action="count", + default=0, + help=( + "Increase logging verbosity level. Default: only error and " + "higher are displayed. Each -v increases level of verbosity.")) + + # Arguments to control whether we're building an OS X-style + # framework. This is the opposite of the older "-m" (makefile) + # option. + parser.add_argument( + "--config-build-dir", + "--cfgBldDir", + help=( + "Configuration build dir, will use python module path " + "if unspecified.")) + parser.add_argument( + "--framework", + action="store_true", + help="Prepare as OS X-style framework.") + parser.add_argument( + "--generate-dependency-file", + "-M", + action="store_true", + help="Make the dependency (.d) file for the wrappers.") + parser.add_argument( + "--prefix", + help="Override path where the LLDB module is placed.") + parser.add_argument( + "--src-root", + "--srcRoot", + "-s", + # Default to the parent directory of this script's directory. + default=os.path.abspath( + os.path.join( + os.path.dirname(os.path.realpath(__file__)), + os.path.pardir)), + help="Specifies the LLDB source root directory.") + parser.add_argument( + "--swig-executable", + "--swigExecutable", + help="Path to the swig executable.") + parser.add_argument( + "--target-dir", + "--targetDir", + required=True, + help=( + "Specifies the build dir where the language binding " + "should be placed")) + + # Process args. + options = parser.parse_args(args) + + # Set logging level based on verbosity count. + if options.debug: + log_level = logging.DEBUG + else: + # See logging documentation for error levels. We'll default + # to showing ERROR or higher error messages. For each -v + # specified, we'll shift to the next lower-priority log level. + log_level = logging.ERROR - 10 * options.verbose + if log_level < logging.NOTSET: + # Displays all logged messages. + log_level = logging.NOTSET + logging.basicConfig(level=log_level) + logging.info("logging is using level: %d", log_level) + + return options + + +def main(args): + """Drives the main script preparation steps. + + @param args list of command line arguments. + """ + # Process command line arguments. + options = process_args(args) + logging.debug("Processed args: options=%s", options) + + # Check if the swig file exists. + swig_path = os.path.normcase( + os.path.join(options.src_root, "scripts", "lldb.swig")) + if not os.path.isfile(swig_path): + logging.error("swig file not found at '%s'", swig_path) + sys.exit(-3) + + # Prepare bindings for each supported language binding. + # This will error out if it doesn't succeed. + prepare_all_bindings(options) + sys.exit(0) + +if __name__ == "__main__": + # Run the main driver loop. + main(sys.argv[1:]) |