syncsource.py

OVERVIEW

The syncsource.py utility transfers groups of files between
computers. The primary use case is to enable developing LLVM project
software on one machine, transfer it efficiently to other machines ---
possibly of other architectures --- and test it there. syncsource.py
supports configurable, named source-to-destination mappings and has a
transfer agent plug-in architecture. The current distribution provides
an rsync-over-ssh transfer agent.

The primary benefits of using syncsource.py are:

* Provides a simple, reliable way to get a mirror copy of primary-
  machine files onto several different destinations without concern
  of compromising the patch during testing on different machines.

* Handles directory-mapping differences between two machines.  For
  LLDB, this is helpful when going between OS X and any other non-OS X
  target system.

EXAMPLE WORKFLOW

This utility was developed in the context of working on the LLDB
project.  Below we show the transfers we'd like to have happen,
and the configuration that supports it.

Workflow Example:

* Develop on OS X (primary machine)
* Test candidate changes on OS X.
* Test candidate changes on a Linux machine (machine-name: lldb-linux).
* Test candidate changes on a FreeBSD machine (machine-name: lldb-freebsd).
* Do check-ins from OS X machine.

Requirements:

* OS X machine requires the lldb source layout: lldb, lldb/llvm,
  lldb/llvm/tools/clang. Note this is different than the canonical
  llvm, llvm/tools/clang, llvm/tools/lldb layout that we'll want on
  the Linux and FreeBSD test machines.

* Linux machine requires the llvm, llvm/tools/clang and
  llvm/tools/lldb layout.

* FreeBSD machine requires the same layout as the llvm machine.

syncsource.py configuration in ~/.syncsourcerc:

# This is my configuration with a comment.  Configuration
# files are JSON-based.
{ "configurations": [
    # Here we have a collection of named configuration blocks.
    # Configuration blocks can chain back to a parent by name.
    {
        # Every block has a name so it can be referenced from
        # the command line or chained back to by a child block
        # for sharing.
        "name": "base_tot_settings",

        # This directive lists the "directory ids" that we'll care
        # about.  If your local repository has additional directories
        # for other projects that need to ride along, add them here.
        # For defaulting purposes, it makes sense to name the
        # directory IDs as the most likely name for the directory
        # itself.  For stock LLDB from top of tree, we generally only
        # care about lldb, llvm and clang.
        "dir_names": [ "llvm", "clang", "lldb" ],

        # This section describes where the source files live on
        # the primary machine.  There should always be a base_dir
        # entry, which indicates where in the local filesystem the
        # projects are rooted.  For each dir in dir_names, there
        # should be either:
        # 1. an entry named {dir-id}_dir (e.g. llvm_dir), which
        #    specifies the source directory for the given dir id
        #    relative to the base_dir entry, OR
        # 2. no entry, in which case the directory is assumed to
        #    be the same as {dir-id}.  In the example below, the
        #    base_dir-relative directory for the "lldb" dir-id is
        #    defaulted to being "lldb".  That's exactly what
        #    we need in an OS X-style lldb dir layout.
        "source": {
            "base_dir": "~/work/lldb-tot",
            "llvm_dir": "lldb/llvm",
            "clang_dir": "lldb/llvm/tools/clang"
        },

        # source_excludes covers any exclusions that:
        # * should be applied when copying files from the source
        # * should be excluded from deletion on the destination
        #
        # By default, ".git", ".svn" and ".pyc" are added to
        # all dir-id exclusions.  The default excludes can be
        # controlled by the syncsource.py --default-excludes
        # option.
        #
        # Below, I have transfer of the lldb dir skip everything
        # rooted at "/llvm" below the the lldb dir.  This is
        # because we want the source OS X lldb to move to
        # a destination of {some-dest-root}/llvm/tools/lldb, and
        # not have the OS-X-inverted llvm copy over with the lldb
        # transfer portion.  We'll see the complete picture of
        # how this works when we get to specifying destinations
        # later on in the config.
        #
        # We also exclude the "/build" and "/llvm-build" dir rooted in
        # the OS X-side sources.  The Xcode configuration on this
        # OS X machine will dump lldb builds in the /build directory
        # relative to the lldb dir, and it will build llvm+clang in
        # the /llvm-build dir relative to the lldb dir.
        #
        # Note the first forward slash in "/build" indicates to the
        # transfer agent that we only want to exclude the
        # ~/work/lldb-tot/lldb/build dir, not just any file or
        # directory named "build" somewhere underneath the lldb
        # directory.  Without the leading forward slash, any file
        # or directory called build anywhere underneath the lldb dir
        # will be excluded, which is definitely not what we want here.
        #
        # For the llvm dir, we do a source-side exclude for
        # "/tools/clang".  We manage the clang transfer as a separate
        # entity, so we don't want the movement of llvm to also move
        # clang.
        #
        # The llvm_dir exclusion of "/tools/lldb" is the first example
        # of an exclude targeting a requirement of the destination
        # side.  Normally the transfer agent will delete anything on
        # the destination that is not present on the source.  It is
        # trying to mirror, and ensure both sides have the same
        # content.  The source side of llvm on OS X does not have a
        # "/tools/lldb", so at first this exclude looks non-sensical.
        # But on the canonical destination layout, lldb lives in
        # {some-dest-root}/llvm/tools/lldb.  Without this exclude,
        # the transfer agent would blow away the tools/lldb directory
        # on the destination every time we transfer, and then have to
        # copy the lldb dir all over again.  For rsync+ssh, that
        # totally would defeat the huge transfer efficiencies gained
        # by using rsync in the first place.
        #
        # Note the overloading of both source and dest style excludes
        # ultimately comes from the rsync-style exclude mechanism.
        # If it wasn't for that, I would have separated source and
        # dest excludes out better.
        "source_excludes": {
            "lldb_dir": ["/llvm", "/build", "/llvm-build"],
            "llvm_dir": ["/tools/lldb", "/tools/clang"]
        }
    },

    # Top of tree public, common settings for all destinations.
    {
        # The name for this config block.
        "name": "common_tot",

        # Here is our first chaining back to a parent config block.
        # Any settings in "common_tot" not specified here are going
        # to be retrieved from the parent.
        "parent": "base_tot_settings",

        # The transfer agent class to use.  Right now, the only one
        # available is this one here that uses rsync over ssh.
        # If some other mechanism is needed to reach this destination,
        # it can be specified here in full [[package.]module.]class form.
        "transfer_class": "transfer.rsync.RsyncOverSsh",

        # Specifies the destination-root-relative directories.
        # Here our desination is rooted at:
        # {some-yet-to-be-specified-destination-root} + "base_dir".
        # In other words, each destination will have some kind of root
        # for all relative file placement.  We'll see those defined
        # later, as they can change per destination machine.
        # The block below describes the settings relative to that
        # destination root.
        #
        # As before, each dir-id used in this configuration is
        # expected to have either:
        # 1. an entry named {dir-id}_dir (e.g. llvm_dir), which
        #    specifies the destination directory for the given dir id
        #    relative to the dest_root+base_dir entries, OR
        # 2. no entry, in which case the directory is assumed to
        #    be the same as {dir-id}.  In the example below, the
        #    dest_root+base_dir-relative directory for the "llvm" dir-id is
        #    defaulted to being "llvm".  That's exactly what
        #    we need in a canonical llvm/clang/lldb setup on
        #    Linux/FreeBSD.
        #
        #    Note we see the untangling of the OS X lldb-centric
        #    directory structure to the canonical llvm,
        #    llvm/tools/clang, llvm/tools/lldb structure below.
        #    We are mapping lldb into a subdirectory of the llvm
        #    directory.
        #
        #    The transfer logic figures out which directories to copy
        #    first by finding the shortest destination absolute path
        #    and doing them in that order.  For our case, this ensures
        #    llvm is copied over before lldb or clang.
        "dest": {
            "base_dir": "work/mirror/git",
            "lldb_dir": "llvm/tools/lldb",
            "clang_dir": "llvm/tools/clang"
        }
    },

    # Describe the lldb-linux destination.  With this,
    # we're done with the mapping for transfer setup
    # for the lldb-linux box.  This configuration can
    # be used either by:
    # 1. having a parent "default" blockthat points to this one,
    #    which then gets used by default, or
    # 2. using the --configuration/-c CONFIG option to
    #    specify using this name on the syncsource.py command line.
    {
        "name": "lldb-linux"
        "parent": "common_tot",

        # The ssh block is understood by the rsync+ssh transfer
        # agent.  Other agents would probably require different
        # agent-specific details that they could read from
        # other blocks.
        "ssh": {
            # This specifies the host name (or IP address) as would
            # be used as the target for an ssh command.
            "dest_host": "lldb-linux.example.com",

            # root_dir specifies the global root directory for
            # this destination.  All destinations on this target
            # will be in a directory that is built from
            # root_dir + base_dir + {dir_id}_dir.
            "root_dir" : "/home/tfiala",

            # The ssh user is specified here.
            "user": "tfiala",

            # The ssh port is specified here.
            "port": 22
        }
    },

    # Describe the lldb-freebsd destination.
    # Very similar to the lldb-linux one.
    {
        "name": "lldb-freebsd"
        "parent": "common_tot",
        "ssh": {
            "dest_host": "lldb-freebsd.example.com",
            # Specify a different destination-specific root dir here.
            "root_dir" : "/mnt/ssd02/fialato",
            "user": "fialato",
            # The ssh port is specified here.
            "port": 2022
        }
    },

    # If a block named "default" exists, and if no configuration
    # is specified on the command line, then the default block
    # will be used.  Use this block to point to the most common
    # transfer destination you would use.
    {
        "name": "default",
        "parent": "lldb-linux"
    }
]
}

Using it

Now that we have a .syncsourcerc file set up, we can do a transfer.
The .syncsourcerc file will be searched for as follows, using the
first one that is found:

* First check the --rc-file RCFILE option.  If this is specified
  and doesn't exist, it will raise an error and quit.

* Check if the current directory has a .syncsourcerc file.  If so,
  use that.

* Use the .syncsourcerc file from the user's home directory.

Run the command:
python /path/to/syncsource.rc -c {configuration-name}

The -c {configuration-name} can be left off, in which case a
configuration with the name 'default' will be used.

After that, the transfer will occur.  With the rsync-over-ssh
transfer agent, one rsync per dir-id will be used.  rsync output
is redirected to the console.

FEEDBACK

Feel free to pass feedback along to Todd Fiala (todd.fiala@gmail.com).
