"""Class capturing a command invocation as data."""
-
-# Done first to setup python module path.
-import toolchain_env
-
-import multiprocessing
+import inspect
+import glob
+import hashlib
+import logging
import os
+import shutil
import sys
-import file_tools
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import pynacl.file_tools
+import pynacl.log_tools
+import pynacl.repo_tools
+
+import substituter
# MSYS tools do not always work with combinations of Windows and MSYS
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
NACL_DIR = os.path.dirname(SCRIPT_DIR)
-
-def FixPath(path):
- """Convert to msys paths on windows."""
- if sys.platform != 'win32':
- return path
- drive, path = os.path.splitdrive(path)
- # Replace X:\... with /x/....
- # Msys does not like x:\ style paths (especially with mixed slashes).
- if drive:
- drive = '/' + drive.lower()[0]
- path = drive + path
- path = path.replace('\\', '/')
- return path
-
-
-def PrepareCommandValues(cwd, inputs, output):
- values = {}
- values['cwd'] = FixPath(os.path.abspath(cwd))
- for key, value in inputs.iteritems():
- if key.startswith('abs_'):
- raise Exception('Invalid key starts with "abs_": %s' % key)
- values['abs_' + key] = FixPath(os.path.abspath(value))
- values[key] = FixPath(os.path.relpath(value, cwd))
- values['abs_output'] = FixPath(os.path.abspath(output))
- values['output'] = FixPath(os.path.relpath(output, cwd))
- return values
-
-
-class Command(object):
+COMMAND_CODE_FILES = [os.path.join(SCRIPT_DIR, f)
+ for f in ('command.py', 'once.py', 'substituter.py',
+ 'pnacl_commands.py', 'toolchain_main.py',)]
+COMMAND_CODE_FILES += [os.path.join(NACL_DIR, 'pynacl', f)
+ for f in ('platform.py','directory_storage.py',
+ 'file_tools.py', 'gsd_storage.py',
+ 'hashing_tools.py', 'local_storage_cache.py',
+ 'log_tools.py', 'repo_tools.py',)]
+
+def HashBuildSystemSources():
+ """Read the build source files to use in hashes for Callbacks."""
+ global FILE_CONTENTS_HASH
+ h = hashlib.sha1()
+ for filename in COMMAND_CODE_FILES:
+ with open(filename) as f:
+ h.update(f.read())
+ FILE_CONTENTS_HASH = h.hexdigest()
+
+HashBuildSystemSources()
+
+
+def PlatformEnvironment(extra_paths):
+ """Select the environment variables to run commands with.
+
+ Args:
+ extra_paths: Extra paths to add to the PATH variable.
+ Returns:
+ A dict to be passed as env to subprocess.
+ """
+ env = os.environ.copy()
+ paths = []
+ if sys.platform == 'win32':
+ # TODO(bradnelson): switch to something hermetic.
+ mingw = os.environ.get('MINGW', r'c:\mingw')
+ msys = os.path.join(mingw, 'msys', '1.0')
+ if not os.path.exists(msys):
+ msys = os.path.join(mingw, 'msys')
+ # We need both msys (posix like build environment) and MinGW (windows
+ # build of tools like gcc). We add <MINGW>/msys/[1.0/]bin to the path to
+ # get sh.exe. We add <MINGW>/bin to allow direct invocation on MinGW
+ # tools. We also add an msys style path (/mingw/bin) to get things like
+ # gcc from inside msys.
+ paths = [
+ '/mingw/bin',
+ os.path.join(mingw, 'bin'),
+ os.path.join(msys, 'bin'),
+ ]
+ env['PATH'] = os.pathsep.join(
+ paths + extra_paths + env.get('PATH', '').split(os.pathsep))
+ return env
+
+
+class Runnable(object):
"""An object representing a single command."""
-
- def __init__(self, command, **kwargs):
- self._command = command
- self._kwargs = kwargs
+ def __init__(self, run_cond, func, *args, **kwargs):
+ """Construct a runnable which will call 'func' with 'args' and 'kwargs'.
+
+ Args:
+ run_cond: If not None, expects a function which takes a CommandOptions
+ object and returns whether or not to run the command.
+ func: Function which will be called by Invoke
+ args: Positional arguments to be passed to func
+ kwargs: Keyword arguments to be passed to func
+
+ RUNNABLES SHOULD ONLY BE IMPLEMENTED IN THIS FILE, because their
+ string representation (which is used to calculate whether targets should
+ be rebuilt) is based on this file's hash and does not attempt to capture
+ the code or bound variables of the function itself (the one exception is
+ once_test.py which injects its own callbacks to verify its expectations).
+
+ When 'func' is called, its first argument will be a substitution object
+ which it can use to substitute %-templates in its arguments.
+ """
+ self._run_cond = run_cond
+ self._func = func
+ self._args = args or []
+ self._kwargs = kwargs or {}
def __str__(self):
values = []
- # TODO(bradnelson): Do something more reasoned here.
- values += [repr(self._command)]
- for k, v in self._kwargs.iteritems():
- values += [repr(k), repr(v)]
+
+ sourcefile = inspect.getsourcefile(self._func)
+ # Check that the code for the runnable is implemented in one of the known
+ # source files of the build system (which are included in its hash). This
+ # isn't a perfect test because it could still import code from an outside
+ # module, so we should be sure to add any new build system files to the list
+ found_match = (os.path.basename(sourcefile) in
+ [os.path.basename(f) for f in
+ COMMAND_CODE_FILES + ['once_test.py']])
+ if not found_match:
+ print 'Function', self._func.func_name, 'in', sourcefile
+ raise Exception('Python Runnable objects must be implemented in one of' +
+ 'the following files: ' + str(COMMAND_CODE_FILES))
+
+ # Like repr(datum), but do something stable for dictionaries.
+ # This only properly handles dictionaries that use simple types
+ # as keys.
+ def ReprForHash(datum):
+ if isinstance(datum, dict):
+ # The order of a dictionary's items is unpredictable.
+ # Manually make a string in dict syntax, but sorted on keys.
+ return ('{' +
+ ', '.join(repr(key) + ': ' + ReprForHash(value)
+ for key, value in sorted(datum.iteritems(),
+ key=lambda t: t[0])) +
+ '}')
+ elif isinstance(datum, list):
+ # A list is already ordered, but its items might be dictionaries.
+ return ('[' +
+ ', '.join(ReprForHash(value) for value in datum) +
+ ']')
+ else:
+ return repr(datum)
+
+ for v in self._args:
+ values += [ReprForHash(v)]
+ # The order of a dictionary's items is unpredictable.
+ # Sort by key for hashing purposes.
+ for k, v in sorted(self._kwargs.iteritems(), key=lambda t: t[0]):
+ values += [repr(k), ReprForHash(v)]
+ values += [FILE_CONTENTS_HASH]
+
return '\n'.join(values)
- def Invoke(self, check_call, package, inputs, output, cwd,
- build_signature=None):
- # TODO(bradnelson): Instead of allowing full subprocess functionality,
- # move execution here and use polymorphism to implement things like
- # mkdir, copy directly in python.
- kwargs = self._kwargs.copy()
- kwargs['cwd'] = os.path.join(os.path.abspath(cwd), kwargs.get('cwd', '.'))
- values = PrepareCommandValues(kwargs['cwd'], inputs, output)
- try:
- values['cores'] = multiprocessing.cpu_count()
- except NotImplementedError:
- values['cores'] = 4 # Assume 4 if we can't measure.
- values['package'] = package
- if build_signature is not None:
- values['build_signature'] = build_signature
- values['top_srcdir'] = FixPath(os.path.relpath(NACL_DIR, kwargs['cwd']))
- values['abs_top_srcdir'] = FixPath(os.path.abspath(NACL_DIR))
-
- # Use mingw on windows.
- if sys.platform == 'win32':
- # TODO(bradnelson): switch to something hermetic.
- mingw = os.environ.get('MINGW', r'c:\mingw')
- msys = os.path.join(mingw, 'msys', '1.0')
- if not os.path.exists(msys):
- msys = os.path.join(mingw, 'msys')
- # We need both msys (posix like build environment) and MinGW (windows
- # build of tools like gcc). We add <MINGW>/msys/[1.0/]bin to the path to
- # get sh.exe. We also add an msys style path (/mingw/bin) to get things
- # like gcc from inside msys.
- kwargs['path_dirs'] = (
- ['/mingw/bin', os.path.join(msys, 'bin')] +
- kwargs.get('path_dirs', []))
-
- if 'path_dirs' in kwargs:
- path_dirs = [dirname % values for dirname in kwargs['path_dirs']]
- del kwargs['path_dirs']
- env = os.environ.copy()
- env['PATH'] = os.pathsep.join(path_dirs + env['PATH'].split(os.pathsep))
- kwargs['env'] = env
-
- if isinstance(self._command, str):
- command = self._command % values
+ def CheckRunCond(self, cmd_options):
+ if self._run_cond and not self._run_cond(cmd_options):
+ return False
+ return True
+
+ def Invoke(self, logger, subst):
+ return self._func(logger, subst, *self._args, **self._kwargs)
+
+
+def Command(command, stdout=None, run_cond=None, **kwargs):
+ """Return a Runnable which invokes 'command' with check_call.
+
+ Args:
+ command: List or string with a command suitable for check_call
+ stdout (optional): File name to redirect command's stdout to
+ kwargs: Keyword arguments suitable for check_call (or 'cwd' or 'path_dirs')
+
+ The command will be %-substituted and paths will be assumed to be relative to
+ the cwd given by Invoke. If kwargs contains 'cwd' it will be appended to the
+ cwd given by Invoke and used as the cwd for the call. If kwargs contains
+ 'path_dirs', the directories therein will be added to the paths searched for
+ the command. Any other kwargs will be passed to check_call.
+ """
+ def runcmd(logger, subst, command, stdout, **kwargs):
+ check_call_kwargs = kwargs.copy()
+ command = command[:]
+
+ cwd = subst.SubstituteAbsPaths(check_call_kwargs.get('cwd', '.'))
+ subst.SetCwd(cwd)
+ check_call_kwargs['cwd'] = cwd
+
+ # Extract paths from kwargs and add to the command environment.
+ path_dirs = []
+ if 'path_dirs' in check_call_kwargs:
+ path_dirs = [subst.Substitute(dirname) for dirname
+ in check_call_kwargs['path_dirs']]
+ del check_call_kwargs['path_dirs']
+ check_call_kwargs['env'] = PlatformEnvironment(path_dirs)
+
+ if isinstance(command, str):
+ command = subst.Substitute(command)
else:
- command = [arg % values for arg in self._command]
- paths = kwargs.get('env', os.environ).get('PATH', '').split(os.pathsep)
- command[0] = file_tools.Which(command[0], paths=paths)
- check_call(command, **kwargs)
+ command = [subst.Substitute(arg) for arg in command]
+ paths = check_call_kwargs['env']['PATH'].split(os.pathsep)
+ command[0] = pynacl.file_tools.Which(command[0], paths=paths)
+ if stdout is not None:
+ stdout = subst.SubstituteAbsPaths(stdout)
-def Mkdir(path, parents=False, **kwargs):
- """Convenience method for generating mkdir commands."""
- # TODO(bradnelson): Replace with something less hacky.
- func = 'os.mkdir'
- if parents:
- func = 'os.makedirs'
- return Command([
- sys.executable, '-c',
- 'import sys,os; ' + func + '(sys.argv[1])', path],
- **kwargs)
+ pynacl.log_tools.CheckCall(command, stdout=stdout, logger=logger,
+ **check_call_kwargs)
+ return Runnable(run_cond, runcmd, command, stdout, **kwargs)
-def Copy(src, dst, **kwargs):
- """Convenience method for generating cp commands."""
- # TODO(bradnelson): Replace with something less hacky.
- return Command([
- sys.executable, '-c',
- 'import sys,shutil; shutil.copyfile(sys.argv[1], sys.argv[2])', src, dst],
- **kwargs)
+def SkipForIncrementalCommand(command, run_cond=None, **kwargs):
+ """Return a command which gets skipped for incremental builds.
-def RemoveDirectory(path):
- """Convenience method for generating a command to remove a directory tree."""
- # TODO(mcgrathr): Windows
- return Command(['rm', '-rf', path])
+ Incremental builds are defined to be when the clobber flag is not on and
+ the working directory is not empty.
+ """
+ def SkipForIncrementalCondition(cmd_opts):
+ # Check if caller passed their own run_cond.
+ if run_cond and not run_cond(cmd_opts):
+ return False
+
+ # Only run when clobbering working directory or working directory is empty.
+ return (cmd_opts.IsClobberWorking() or
+ not os.path.isdir(cmd_opts.GetWorkDir()) or
+ len(os.listdir(cmd_opts.GetWorkDir())) == 0)
+
+ return Command(command, run_cond=SkipForIncrementalCondition, **kwargs)
-def Remove(path):
- """Convenience method for generating a command to remove a file."""
- # TODO(mcgrathr): Replace with something less hacky.
- return Command([
- sys.executable, '-c',
- 'import sys, os\n'
- 'if os.path.exists(sys.argv[1]): os.remove(sys.argv[1])', path
- ])
+def Mkdir(path, parents=False, run_cond=None):
+ """Convenience method for generating mkdir commands."""
+ def mkdir(logger, subst, path):
+ path = subst.SubstituteAbsPaths(path)
+ logger.debug('Making Directory: %s', path)
+ if parents:
+ os.makedirs(path)
+ else:
+ os.mkdir(path)
+ return Runnable(run_cond, mkdir, path)
-def Rename(src, dst):
+def Copy(src, dst, run_cond=None):
+ """Convenience method for generating cp commands."""
+ def copy(logger, subst, src, dst):
+ src = subst.SubstituteAbsPaths(src)
+ dst = subst.SubstituteAbsPaths(dst)
+ logger.debug('Copying: %s -> %s', src, dst)
+ shutil.copyfile(src, dst)
+
+ return Runnable(run_cond, copy, src, dst)
+
+
+def CopyTree(src, dst, exclude=[], run_cond=None):
+ """Copy a directory tree, excluding a list of top-level entries."""
+ def copyTree(logger, subst, src, dst, exclude):
+ src = subst.SubstituteAbsPaths(src)
+ dst = subst.SubstituteAbsPaths(dst)
+ def ignoreExcludes(dir, files):
+ if dir == src:
+ return exclude
+ else:
+ return []
+ logger.debug('Copying Tree: %s -> %s', src, dst)
+ pynacl.file_tools.RemoveDirectoryIfPresent(dst)
+ shutil.copytree(src, dst, symlinks=True, ignore=ignoreExcludes)
+ return Runnable(run_cond, copyTree, src, dst, exclude)
+
+
+def RemoveDirectory(path, run_cond=None):
+ """Convenience method for generating a command to remove a directory tree."""
+ def remove(logger, subst, path):
+ path = subst.SubstituteAbsPaths(path)
+ logger.debug('Removing Directory: %s', path)
+ pynacl.file_tools.RemoveDirectoryIfPresent(path)
+ return Runnable(run_cond, remove, path)
+
+
+def Remove(*args):
+ """Convenience method for generating a command to remove files."""
+ def remove(logger, subst, *args):
+ for arg in args:
+ path = subst.SubstituteAbsPaths(arg)
+ logger.debug('Removing Pattern: %s', path)
+ expanded = glob.glob(path)
+ if len(expanded) == 0:
+ logger.debug('command.Remove: argument %s (substituted from %s) '
+ 'does not match any file' %
+ (path, arg))
+ for f in expanded:
+ logger.debug('Removing File: %s', f)
+ os.remove(f)
+ return Runnable(None, remove, *args)
+
+
+def Rename(src, dst, run_cond=None):
"""Convenience method for generating a command to rename a file."""
- # TODO(mcgrathr): Replace with something less hacky.
- return Command([
- sys.executable, '-c',
- 'import sys, os; os.rename(sys.argv[1], sys.argv[2])', src, dst
- ])
+ def rename(logger, subst, src, dst):
+ src = subst.SubstituteAbsPaths(src)
+ dst = subst.SubstituteAbsPaths(dst)
+ logger.debug('Renaming: %s -> %s', src, dst)
+ os.rename(src, dst)
+ return Runnable(run_cond, rename, src, dst)
-def WriteData(data, dst):
+def WriteData(data, dst, run_cond=None):
"""Convenience method to write a file with fixed contents."""
- # TODO(mcgrathr): Replace with something less hacky.
- return Command([
- sys.executable, '-c',
- 'import sys; open(sys.argv[1], "wb").write(%r)' % data, dst
- ])
+ def writedata(logger, subst, dst, data):
+ dst = subst.SubstituteAbsPaths(dst)
+ logger.debug('Writing Data to File: %s', dst)
+ with open(subst.SubstituteAbsPaths(dst), 'wb') as f:
+ f.write(data)
+ return Runnable(run_cond, writedata, dst, data)
+
+
+def SyncGitRepoCmds(url, destination, revision, clobber_invalid_repo=False,
+ reclone=False, clean=False, pathspec=None, git_cache=None,
+ push_url=None, known_mirrors=[], push_mirrors=[],
+ run_cond=None):
+ """Returns a list of commands to sync and validate a git repo.
+
+ Args:
+ url: Git repo URL to sync from.
+ destination: Local git repo directory to sync to.
+ revision: If not None, will sync the git repository to this revision.
+ clobber_invalid_repo: Always True for bots, but can be forced for users.
+ reclone: If True, delete the destination directory and re-clone the repo.
+ clean: If True, discard local changes and untracked files.
+ Otherwise the checkout will fail if there are uncommitted changes.
+ pathspec: If not None, add the path to the git checkout command, which
+ causes it to just update the working tree without switching
+ branches.
+ known_mirrors: List of tuples specifying known mirrors for a subset of the
+ git URL. IE: [('http://mirror.com/mirror', 'http://git.com')]
+ push_mirrors: List of tuples specifying known push mirrors, see
+ known_mirrors argument for the format.
+ git_cache: If not None, will use git_cache directory as a cache for the git
+ repository and share the objects with any other destination with
+ the same URL.
+ push_url: If not None, specifies what the push URL should be set to.
+ run_cond: Run condition for when to sync the git repo.
+
+ Returns:
+ List of commands, this is a little different from the other command funcs.
+ """
+ def update_valid_mirrors(logger, subst, url, push_url, directory,
+ known_mirrors, push_mirrors):
+ if push_url is None:
+ push_url = url
+
+ abs_dir = subst.SubstituteAbsPaths(directory)
+ git_dir = os.path.join(abs_dir, '.git')
+ if os.path.exists(git_dir):
+ fetch_list = pynacl.repo_tools.GitRemoteRepoList(abs_dir,
+ include_fetch=True,
+ include_push=False,
+ logger=logger)
+ tracked_fetch_url = dict(fetch_list).get('origin', 'None')
+
+ push_list = pynacl.repo_tools.GitRemoteRepoList(abs_dir,
+ include_fetch=False,
+ include_push=True,
+ logger=logger)
+ tracked_push_url = dict(push_list).get('origin', 'None')
+
+ if ((known_mirrors and tracked_fetch_url != url) or
+ (push_mirrors and tracked_push_url != push_url)):
+ updated_fetch_url = tracked_fetch_url
+ for mirror, url_subset in known_mirrors:
+ if mirror in updated_fetch_url:
+ updated_fetch_url = updated_fetch_url.replace(mirror, url_subset)
+
+ updated_push_url = tracked_push_url
+ for mirror, url_subset in push_mirrors:
+ if mirror in updated_push_url:
+ updated_push_url = updated_push_url.replace(mirror, url_subset)
+
+ if ((updated_fetch_url != tracked_fetch_url) or
+ (updated_push_url != tracked_push_url)):
+ logger.warn('Your git repo is using an old mirror: %s', abs_dir)
+ logger.warn('Updating git repo using known mirror:')
+ logger.warn(' [FETCH] %s -> %s',
+ tracked_fetch_url, updated_fetch_url)
+ logger.warn(' [PUSH] %s -> %s',
+ tracked_push_url, updated_push_url)
+ pynacl.repo_tools.GitSetRemoteRepo(updated_fetch_url, abs_dir,
+ push_url=updated_push_url,
+ logger=logger)
+
+ def populate_cache(logger, subst, git_cache, url):
+ if git_cache:
+ abs_git_cache = subst.SubstituteAbsPaths(git_cache)
+ logger.debug('Populating Cache: %s [%s]', abs_git_cache, url)
+ if abs_git_cache:
+ pynacl.repo_tools.PopulateGitCache(abs_git_cache, [url],
+ logger=logger)
+
+ def validate(logger, subst, url, directory):
+ abs_dir = subst.SubstituteAbsPaths(directory)
+ logger.debug('Validating Repo: %s [%s]', abs_dir, url)
+ pynacl.repo_tools.ValidateGitRepo(url,
+ subst.SubstituteAbsPaths(directory),
+ clobber_mismatch=True,
+ logger=logger)
+
+ def sync(logger, subst, url, dest, revision, reclone, clean, pathspec,
+ git_cache, push_url):
+ abs_dest = subst.SubstituteAbsPaths(dest)
+ if git_cache:
+ git_cache = subst.SubstituteAbsPaths(git_cache)
+
+ logger.debug('Syncing Git Repo: %s [%s]', abs_dest, url)
+ try:
+ pynacl.repo_tools.SyncGitRepo(url, abs_dest, revision,
+ reclone=reclone, clean=clean,
+ pathspec=pathspec, git_cache=git_cache,
+ push_url=push_url, logger=logger)
+ except pynacl.repo_tools.InvalidRepoException, e:
+ remote_repos = dict(pynacl.repo_tools.GitRemoteRepoList(abs_dest,
+ logger=logger))
+ tracked_url = remote_repos.get('origin', 'None')
+ logger.error('Invalid Git Repo: %s' % e)
+ logger.error('Destination Directory: %s', abs_dest)
+ logger.error('Currently Tracked Repo: %s', tracked_url)
+ logger.error('Expected Repo: %s', e.expected_repo)
+ logger.warn('Possible solutions:')
+ logger.warn(' 1. The simplest way if you have no local changes is to'
+ ' simply delete the directory and let the tool resync.')
+ logger.warn(' 2. If the tracked repo is merely a mirror, simply go to'
+ ' the directory and run "git remote set-url origin %s"',
+ e.expected_repo)
+ raise Exception('Could not validate local git repository.')
+
+ def ClobberInvalidRepoCondition(cmd_opts):
+ # Check if caller passed their own run_cond
+ if run_cond and not run_cond(cmd_opts):
+ return False
+ elif clobber_invalid_repo:
+ return True
+ return cmd_opts.IsBot()
+
+ commands = []
+ if git_cache:
+ commands.append(Runnable(run_cond, populate_cache, git_cache, url))
+
+ commands.extend([Runnable(run_cond, update_valid_mirrors, url, push_url,
+ destination, known_mirrors, push_mirrors),
+ Runnable(ClobberInvalidRepoCondition, validate, url,
+ destination),
+ Runnable(run_cond, sync, url, destination, revision, reclone,
+ clean, pathspec, git_cache, push_url)])
+ return commands
+
+
+def CleanGitWorkingDir(directory, path, run_cond=None):
+ """Clean a path in a git checkout, if the checkout directory exists."""
+ def clean(logger, subst, directory, path):
+ directory = subst.SubstituteAbsPaths(directory)
+ logger.debug('Cleaning Git Working Directory: %s', directory)
+ if os.path.exists(directory) and len(os.listdir(directory)) > 0:
+ pynacl.repo_tools.CleanGitWorkingDir(directory, path, logger=logger)
+ return Runnable(run_cond, clean, directory, path)
+
+
+def GenerateGitPatches(git_dir, info, run_cond=None):
+ """Generate patches from a Git repository.
+
+ Args:
+ git_dir: bare git repository directory to examine (.../.git)
+ info: dictionary containing:
+ 'rev': commit that we build
+ 'upstream-name': basename of the upstream baseline release
+ (i.e. what the release tarball would be called before ".tar")
+ 'upstream-base': commit corresponding to upstream-name
+ 'upstream-branch': tracking branch used for upstream merges
+
+ This will produce between zero and two patch files (in %(output)s/):
+ <upstream-name>-g<commit-abbrev>.patch: From 'upstream-base' to the common
+ ancestor (merge base) of 'rev' and 'upstream-branch'. Omitted if none.
+ <upstream-name>[-g<commit-abbrev>]-nacl.patch: From the result of that
+ (or from 'upstream-base' if none above) to 'rev'.
+ """
+ def generatePatches(logger, subst, git_dir, info, run_cond=None):
+ git_dir = subst.SubstituteAbsPaths(git_dir)
+ git_dir_flag = '--git-dir=' + git_dir
+ basename = info['upstream-name']
+ logger.debug('Generating Git Patches: %s', git_dir)
+
+ patch_files = []
+
+ def generatePatch(description, src_rev, dst_rev, suffix):
+ src_prefix = '--src-prefix=' + basename + '/'
+ dst_prefix = '--dst-prefix=' + basename + suffix + '/'
+ patch_name = basename + suffix + '.patch'
+ patch_file = subst.SubstituteAbsPaths(path.join('%(output)s', patch_name))
+ git_args = [git_dir_flag, 'diff',
+ '--patch-with-stat', '--ignore-space-at-eol', '--full-index',
+ '--no-ext-diff', '--no-color', '--no-renames',
+ '--no-textconv', '--text', src_prefix, dst_prefix,
+ src_rev, dst_rev]
+ pynacl.log_tools.CheckCall(
+ pynacl.repo_tools.GitCmd() + git_args,
+ stdout=patch_file,
+ logger=logger,
+ )
+ patch_files.append((description, patch_name))
+
+ def revParse(args):
+ output = pynacl.repo_tools.CheckGitOutput([git_dir_flag] + args)
+ lines = output.splitlines()
+ if len(lines) != 1:
+ raise Exception('"git %s" did not yield a single commit' %
+ ' '.join(args))
+ return lines[0]
+
+ rev = revParse(['rev-parse', info['rev']])
+ upstream_base = revParse(['rev-parse', info['upstream-base']])
+ upstream_branch = revParse(['rev-parse',
+ 'refs/remotes/origin/' +
+ info['upstream-branch']])
+ upstream_snapshot = revParse(['merge-base', rev, upstream_branch])
+
+ if rev == upstream_base:
+ # We're building a stock upstream release. Nothing to do!
+ return
+
+ if upstream_snapshot == upstream_base:
+ # We've forked directly from the upstream baseline release.
+ suffix = ''
+ else:
+ # We're using an upstream baseline snapshot past the baseline
+ # release, so generate a snapshot patch. The leading seven
+ # hex digits of the commit ID is what Git usually produces
+ # for --abbrev-commit behavior, 'git describe', etc.
+ suffix = '-g' + upstream_snapshot[:7]
+ generatePatch('Patch the release up to the upstream snapshot version.',
+ upstream_base, upstream_snapshot, suffix)
+
+ if rev != upstream_snapshot:
+ # We're using local changes, so generate a patch of those.
+ generatePatch('Apply NaCl-specific changes.',
+ upstream_snapshot, rev, suffix + '-nacl')
+
+ with open(subst.SubstituteAbsPaths(path.join('%(output)s',
+ info['name'] + '.series')),
+ 'w') as f:
+ f.write("""\
+# This is a "series file" in the style used by the "quilt" tool.
+# It describes how to unpack and apply patches to produce the source
+# tree of the %(name)s component of a toolchain targetting Native Client.
+
+# Source: %(upstream-name)s.tar
+"""
+ % info)
+ for patch in patch_files:
+ f.write('\n# %s\n%s\n' % patch)
+
+ return Runnable(run_cond, generatePatches, git_dir, info)