summaryrefslogtreecommitdiffstats
path: root/lldb/utils/sync-source/syncsource.py
diff options
context:
space:
mode:
Diffstat (limited to 'lldb/utils/sync-source/syncsource.py')
-rw-r--r--lldb/utils/sync-source/syncsource.py270
1 files changed, 270 insertions, 0 deletions
diff --git a/lldb/utils/sync-source/syncsource.py b/lldb/utils/sync-source/syncsource.py
new file mode 100644
index 00000000000..6cf2612bd7e
--- /dev/null
+++ b/lldb/utils/sync-source/syncsource.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python
+"""
+Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+See https://llvm.org/LICENSE.txt for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+Sync lldb and related source from a local machine to a remote machine.
+
+This facilitates working on the lldb sourcecode on multiple machines
+and multiple OS types, verifying changes across all.
+"""
+
+import argparse
+import io
+import importlib
+import json
+import os.path
+import re
+import sys
+
+# Add the local lib directory to the python path.
+LOCAL_LIB_PATH = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ "lib")
+sys.path.append(LOCAL_LIB_PATH)
+
+import transfer.transfer_spec
+
+
+DOTRC_BASE_FILENAME = ".syncsourcerc"
+
+
+class Configuration(object):
+ """Provides chaining configuration lookup."""
+
+ def __init__(self, rcdata_configs):
+ self.__rcdata_configs = rcdata_configs
+
+ def get_value(self, key):
+ """
+ Return the first value in the parent chain that has the key.
+
+ The traversal starts from the most derived configuration (i.e.
+ child) and works all the way up the parent chain.
+
+ @return the value of the first key in the parent chain that
+ contains a value for the given key.
+ """
+ for config in self.__rcdata_configs:
+ if key in config:
+ return config[key]
+ return None
+
+ def __getitem__(self, key):
+ value = self.get_value(key)
+ if value:
+ return value
+ else:
+ raise KeyError(key)
+
+
+def parse_args():
+ """@return options parsed from the command line."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--config-name", "-c", action="store", default="default",
+ help="specify configuration name to use")
+ parser.add_argument(
+ "--default-excludes", action="store", default="*.git,*.svn,*.pyc",
+ help=("comma-separated list of default file patterns to exclude "
+ "from each source directory and to protect from deletion "
+ "on each destination directory; if starting with forward "
+ "slash, it only matches at the top of the base directory"))
+ parser.add_argument(
+ "--dry-run", "-n", action="store_true",
+ help="do a dry run of the transfer operation, don't really transfer")
+ parser.add_argument(
+ "--rc-file", "-r", action="store",
+ help="specify the sync-source rc file to use for configurations")
+ parser.add_argument(
+ "--verbose", "-v", action="store_true", help="turn on verbose output")
+ return parser.parse_args()
+
+
+def read_rcfile(filename):
+ """Returns the json-parsed contents of the input file."""
+
+ # First parse file contents, removing all comments but
+ # preserving the line count.
+ regex = re.compile(r"#.*$")
+
+ comment_stripped_file = io.StringIO()
+ with open(filename, "r") as json_file:
+ for line in json_file:
+ comment_stripped_file.write(regex.sub("", line))
+ return json.load(io.StringIO(comment_stripped_file.getvalue()))
+
+
+def find_appropriate_rcfile(options):
+ # Use an options-specified rcfile if specified.
+ if options.rc_file and len(options.rc_file) > 0:
+ if not os.path.isfile(options.rc_file):
+ # If it doesn't exist, error out here.
+ raise "rcfile '{}' specified but doesn't exist".format(
+ options.rc_file)
+ return options.rc_file
+
+ # Check if current directory .sync-sourcerc exists. If so, use it.
+ local_rc_filename = os.path.abspath(DOTRC_BASE_FILENAME)
+ if os.path.isfile(local_rc_filename):
+ return local_rc_filename
+
+ # Check if home directory .sync-sourcerc exists. If so, use it.
+ homedir_rc_filename = os.path.abspath(
+ os.path.join(os.path.expanduser("~"), DOTRC_BASE_FILENAME))
+ if os.path.isfile(homedir_rc_filename):
+ return homedir_rc_filename
+
+ # Nothing matched. We don't have an rc filename candidate.
+ return None
+
+
+def get_configuration(options, rcdata, config_name):
+ rcdata_configs = []
+ next_config_name = config_name
+ while next_config_name:
+ # Find the next rcdata configuration for the given name.
+ rcdata_config = next(
+ config for config in rcdata["configurations"]
+ if config["name"] == next_config_name)
+
+ # See if we found it.
+ if rcdata_config:
+ # This is our next configuration to use in the chain.
+ rcdata_configs.append(rcdata_config)
+
+ # If we have a parent, check that next.
+ if "parent" in rcdata_config:
+ next_config_name = rcdata_config["parent"]
+ else:
+ next_config_name = None
+ else:
+ raise "failed to find specified parent config '{}'".format(
+ next_config_name)
+ return Configuration(rcdata_configs)
+
+
+def create_transfer_agent(options, configuration):
+ transfer_class_spec = configuration.get_value("transfer_class")
+ if options.verbose:
+ print("specified transfer class: '{}'".format(transfer_class_spec))
+
+ # Load the module (possibly package-qualified).
+ components = transfer_class_spec.split(".")
+ module = importlib.import_module(".".join(components[:-1]))
+
+ # Create the class name we need to load.
+ clazz = getattr(module, components[-1])
+ return clazz(options, configuration)
+
+
+def sync_configured_sources(options, configuration, default_excludes):
+ # Look up the transfer method.
+ transfer_agent = create_transfer_agent(options, configuration)
+
+ # For each configured dir_names source, do the following transfer:
+ # 1. Start with base_dir + {source-dir-name}_dir
+ # 2. Copy all files recursively, but exclude
+ # all dirs specified by source_excludes:
+ # skip all base_dir + {source-dir-name}_dir +
+ # {source-dir-name}_dir excludes.
+ source_dirs = configuration.get_value("source")
+ source_excludes = configuration.get_value("source_excludes")
+ dest_dirs = configuration.get_value("dest")
+
+ source_base_dir = source_dirs["base_dir"]
+ dest_base_dir = dest_dirs["base_dir"]
+ dir_ids = configuration.get_value("dir_names")
+ transfer_specs = []
+
+ for dir_id in dir_ids:
+ dir_key = "{}_dir".format(dir_id)
+
+ # Build the source dir (absolute) that we're copying from.
+ # Defaults the base-relative source dir to the source id (e.g. lldb)
+ rel_source_dir = source_dirs.get(dir_key, dir_id)
+ transfer_source_dir = os.path.expanduser(
+ os.path.join(source_base_dir, rel_source_dir))
+
+ # Exclude dirs do two things:
+ # 1) stop items from being copied on the source side, and
+ # 2) protect things from being deleted on the dest side.
+ #
+ # In both cases, they are specified relative to the base
+ # directory on either the source or dest side.
+ #
+ # Specifying a leading '/' in the directory will limit it to
+ # be rooted in the base directory. i.e. "/.git" will only
+ # match {base-dir}/.git, not {base-dir}/subdir/.git, but
+ # ".svn" will match {base-dir}/.svn and
+ # {base-dir}/subdir/.svn.
+ #
+ # If excludes are specified for this dir_id, then pass along
+ # the excludes. These are relative to the dir_id directory
+ # source, and get passed along that way as well.
+ transfer_source_excludes = []
+
+ # Add the source excludes for this dir.
+ skip_defaults = False
+ if source_excludes and dir_key in source_excludes:
+ transfer_source_excludes.extend(source_excludes[dir_key])
+ if "<no-defaults>" in source_excludes[dir_key]:
+ skip_defaults = True
+ transfer_source_excludes.remove("<no-defaults>")
+
+ if not skip_defaults and default_excludes is not None:
+ transfer_source_excludes.extend(list(default_excludes))
+
+ # Build the destination-base-relative dest dir into which
+ # we'll be syncing. Relative directory defaults to the
+ # dir id
+ rel_dest_dir = dest_dirs.get(dir_key, dir_id)
+ transfer_dest_dir = os.path.join(dest_base_dir, rel_dest_dir)
+
+ # Add the exploded paths to the list that we'll ask the
+ # transfer agent to transfer for us.
+ transfer_specs.append(
+ transfer.transfer_spec.TransferSpec(
+ transfer_source_dir,
+ transfer_source_excludes,
+ transfer_dest_dir))
+
+ # Do the transfer.
+ if len(transfer_specs) > 0:
+ transfer_agent.transfer(transfer_specs, options.dry_run)
+ else:
+ raise Exception("nothing to transfer, bad configuration?")
+
+
+def main():
+ """Drives the main program."""
+ options = parse_args()
+
+ if options.default_excludes and len(options.default_excludes) > 0:
+ default_excludes = options.default_excludes.split(",")
+ else:
+ default_excludes = []
+
+ # Locate the rc filename to load, then load it.
+ rc_filename = find_appropriate_rcfile(options)
+ if rc_filename:
+ if options.verbose:
+ print("reading rc data from file '{}'".format(rc_filename))
+ rcdata = read_rcfile(rc_filename)
+ else:
+ sys.stderr.write("no rcfile specified, cannot guess configuration")
+ exit(1)
+
+ # Find configuration.
+ configuration = get_configuration(options, rcdata, options.config_name)
+ if not configuration:
+ sys.stderr.write("failed to find configuration for {}".format(
+ options.config_data))
+ exit(2)
+
+ # Kick off the transfer.
+ sync_configured_sources(options, configuration, default_excludes)
+
+if __name__ == "__main__":
+ main()
OpenPOWER on IntegriCloud