Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / native_client / toolchain_build / command.py
1 #!/usr/bin/python
2 # Copyright (c) 2012 The Native Client Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Class capturing a command invocation as data."""
7
8 import inspect
9 import glob
10 import hashlib
11 import logging
12 import os
13 import shutil
14 import sys
15
16 sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
17 import pynacl.file_tools
18 import pynacl.log_tools
19 import pynacl.repo_tools
20
21 import substituter
22
23
24 # MSYS tools do not always work with combinations of Windows and MSYS
25 # path conventions, e.g. '../foo\\bar' doesn't find '../foo/bar'.
26 # Since we convert all the directory names to MSYS conventions, we
27 # should not be using Windows separators with those directory names.
28 # As this is really an implementation detail of this module, we export
29 # 'command.path' to use in place of 'os.path', rather than having
30 # users of the module know which flavor to use.
31 import posixpath
32 path = posixpath
33
34
35 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
36 NACL_DIR = os.path.dirname(SCRIPT_DIR)
37
38 COMMAND_CODE_FILES = [os.path.join(SCRIPT_DIR, f)
39                       for f in ('command.py', 'once.py', 'substituter.py',
40                                 'pnacl_commands.py', 'toolchain_main.py',)]
41 COMMAND_CODE_FILES += [os.path.join(NACL_DIR, 'pynacl', f)
42                        for f in ('platform.py','directory_storage.py',
43                                  'file_tools.py', 'gsd_storage.py',
44                                  'hashing_tools.py', 'local_storage_cache.py',
45                                  'log_tools.py', 'repo_tools.py',)]
46
47 def HashBuildSystemSources():
48   """Read the build source files to use in hashes for Callbacks."""
49   global FILE_CONTENTS_HASH
50   h = hashlib.sha1()
51   for filename in COMMAND_CODE_FILES:
52     with open(filename) as f:
53       h.update(f.read())
54   FILE_CONTENTS_HASH = h.hexdigest()
55
56 HashBuildSystemSources()
57
58
59 def PlatformEnvironment(extra_paths):
60   """Select the environment variables to run commands with.
61
62   Args:
63     extra_paths: Extra paths to add to the PATH variable.
64   Returns:
65     A dict to be passed as env to subprocess.
66   """
67   env = os.environ.copy()
68   paths = []
69   if sys.platform == 'win32':
70     # TODO(bradnelson): switch to something hermetic.
71     mingw = os.environ.get('MINGW', r'c:\mingw')
72     msys = os.path.join(mingw, 'msys', '1.0')
73     if not os.path.exists(msys):
74       msys = os.path.join(mingw, 'msys')
75     # We need both msys (posix like build environment) and MinGW (windows
76     # build of tools like gcc). We add <MINGW>/msys/[1.0/]bin to the path to
77     # get sh.exe. We add <MINGW>/bin to allow direct invocation on MinGW
78     # tools. We also add an msys style path (/mingw/bin) to get things like
79     # gcc from inside msys.
80     paths = [
81         '/mingw/bin',
82         os.path.join(mingw, 'bin'),
83         os.path.join(msys, 'bin'),
84     ]
85   env['PATH'] = os.pathsep.join(
86       paths + extra_paths + env.get('PATH', '').split(os.pathsep))
87   return env
88
89
90 class Runnable(object):
91   """An object representing a single command."""
92   def __init__(self, run_cond, func, *args, **kwargs):
93     """Construct a runnable which will call 'func' with 'args' and 'kwargs'.
94
95     Args:
96       run_cond: If not None, expects a function which takes a CommandOptions
97                 object and returns whether or not to run the command.
98       func: Function which will be called by Invoke
99       args: Positional arguments to be passed to func
100       kwargs: Keyword arguments to be passed to func
101
102       RUNNABLES SHOULD ONLY BE IMPLEMENTED IN THIS FILE, because their
103       string representation (which is used to calculate whether targets should
104       be rebuilt) is based on this file's hash and does not attempt to capture
105       the code or bound variables of the function itself (the one exception is
106       once_test.py which injects its own callbacks to verify its expectations).
107
108       When 'func' is called, its first argument will be a substitution object
109       which it can use to substitute %-templates in its arguments.
110     """
111     self._run_cond = run_cond
112     self._func = func
113     self._args = args or []
114     self._kwargs = kwargs or {}
115
116   def __str__(self):
117     values = []
118
119     sourcefile = inspect.getsourcefile(self._func)
120     # Check that the code for the runnable is implemented in one of the known
121     # source files of the build system (which are included in its hash). This
122     # isn't a perfect test because it could still import code from an outside
123     # module, so we should be sure to add any new build system files to the list
124     found_match = (os.path.basename(sourcefile) in
125                    [os.path.basename(f) for f in
126                     COMMAND_CODE_FILES + ['once_test.py']])
127     if not found_match:
128       print 'Function', self._func.func_name, 'in', sourcefile
129       raise Exception('Python Runnable objects must be implemented in one of' +
130                       ' the following files: ' + str(COMMAND_CODE_FILES))
131
132     # Like repr(datum), but do something stable for dictionaries.
133     # This only properly handles dictionaries that use simple types
134     # as keys.
135     def ReprForHash(datum):
136       if isinstance(datum, dict):
137         # The order of a dictionary's items is unpredictable.
138         # Manually make a string in dict syntax, but sorted on keys.
139         return ('{' +
140                 ', '.join(repr(key) + ': ' + ReprForHash(value)
141                           for key, value in sorted(datum.iteritems(),
142                                                    key=lambda t: t[0])) +
143                 '}')
144       elif isinstance(datum, list):
145         # A list is already ordered, but its items might be dictionaries.
146         return ('[' +
147                 ', '.join(ReprForHash(value) for value in datum) +
148                 ']')
149       else:
150         return repr(datum)
151
152     for v in self._args:
153       values += [ReprForHash(v)]
154     # The order of a dictionary's items is unpredictable.
155     # Sort by key for hashing purposes.
156     for k, v in sorted(self._kwargs.iteritems(), key=lambda t: t[0]):
157       values += [repr(k), ReprForHash(v)]
158     values += [FILE_CONTENTS_HASH]
159
160     return '\n'.join(values)
161
162   def CheckRunCond(self, cmd_options):
163     if self._run_cond and not self._run_cond(cmd_options):
164       return False
165     return True
166
167   def Invoke(self, logger, subst):
168     return self._func(logger, subst, *self._args, **self._kwargs)
169
170
171 def Command(command, stdout=None, run_cond=None, **kwargs):
172   """Return a Runnable which invokes 'command' with check_call.
173
174   Args:
175     command: List or string with a command suitable for check_call
176     stdout (optional): File name to redirect command's stdout to
177     kwargs: Keyword arguments suitable for check_call (or 'cwd' or 'path_dirs')
178
179   The command will be %-substituted and paths will be assumed to be relative to
180   the cwd given by Invoke. If kwargs contains 'cwd' it will be appended to the
181   cwd given by Invoke and used as the cwd for the call. If kwargs contains
182   'path_dirs', the directories therein will be added to the paths searched for
183   the command. Any other kwargs will be passed to check_call.
184   """
185   def runcmd(logger, subst, command, stdout, **kwargs):
186     check_call_kwargs = kwargs.copy()
187     command = command[:]
188
189     cwd = subst.SubstituteAbsPaths(check_call_kwargs.get('cwd', '.'))
190     subst.SetCwd(cwd)
191     check_call_kwargs['cwd'] = cwd
192
193     # Extract paths from kwargs and add to the command environment.
194     path_dirs = []
195     if 'path_dirs' in check_call_kwargs:
196       path_dirs = [subst.Substitute(dirname) for dirname
197                    in check_call_kwargs['path_dirs']]
198       del check_call_kwargs['path_dirs']
199     check_call_kwargs['env'] = PlatformEnvironment(path_dirs)
200
201     if isinstance(command, str):
202       command = subst.Substitute(command)
203     else:
204       command = [subst.Substitute(arg) for arg in command]
205       paths = check_call_kwargs['env']['PATH'].split(os.pathsep)
206       command[0] = pynacl.file_tools.Which(command[0], paths=paths)
207
208     if stdout is not None:
209       stdout = subst.SubstituteAbsPaths(stdout)
210
211     pynacl.log_tools.CheckCall(command, stdout=stdout, logger=logger,
212                                **check_call_kwargs)
213
214   return Runnable(run_cond, runcmd, command, stdout, **kwargs)
215
216
217 def SkipForIncrementalCommand(command, run_cond=None, **kwargs):
218   """Return a command which gets skipped for incremental builds.
219
220   Incremental builds are defined to be when the clobber flag is not on and
221   the working directory is not empty.
222   """
223   def SkipForIncrementalCondition(cmd_opts):
224     # Check if caller passed their own run_cond.
225     if run_cond and not run_cond(cmd_opts):
226       return False
227
228     dir_list = os.listdir(cmd_opts.GetWorkDir())
229     # Only run when clobbering working directory or working directory is empty.
230     return (cmd_opts.IsClobberWorking() or
231             not os.path.isdir(cmd_opts.GetWorkDir()) or
232             len(dir_list) == 0 or
233             (len(dir_list) == 1 and dir_list[0].endswith('.log')))
234
235   return Command(command, run_cond=SkipForIncrementalCondition, **kwargs)
236
237
238 def Mkdir(path, parents=False, run_cond=None):
239   """Convenience method for generating mkdir commands."""
240   def mkdir(logger, subst, path):
241     path = subst.SubstituteAbsPaths(path)
242     if os.path.isdir(path):
243       return
244     logger.debug('Making Directory: %s', path)
245     if parents:
246       os.makedirs(path)
247     else:
248       os.mkdir(path)
249   return Runnable(run_cond, mkdir, path)
250
251
252 def Copy(src, dst, run_cond=None):
253   """Convenience method for generating cp commands."""
254   def copy(logger, subst, src, dst):
255     src = subst.SubstituteAbsPaths(src)
256     dst = subst.SubstituteAbsPaths(dst)
257     logger.debug('Copying: %s -> %s', src, dst)
258     shutil.copyfile(src, dst)
259
260   return Runnable(run_cond, copy, src, dst)
261
262
263 def CopyRecursive(src, dst, run_cond=None):
264   """Recursively copy items in a directory tree.
265
266      If src is a file, the semantics are like shutil.copyfile+copymode.
267      If src is a directory, the semantics are like shutil.copytree, except
268      that the destination may exist (it must be a directory) and will not be
269      clobbered. There must be no files in dst which have names/subpaths which
270      match files in src.
271   """
272   def rcopy(logger, subst, src, dst):
273     src = subst.SubstituteAbsPaths(src)
274     dst = subst.SubstituteAbsPaths(dst)
275     if os.path.isfile(src):
276       shutil.copyfile(src, dst)
277       shutil.copymode(src, dst)
278     elif os.path.isdir(src):
279       logger.debug('Copying directory: %s -> %s', src, dst)
280       pynacl.file_tools.MakeDirectoryIfAbsent(dst)
281       for item in os.listdir(src):
282         rcopy(logger, subst, os.path.join(src, item), os.path.join(dst, item))
283   return Runnable(run_cond, rcopy, src, dst)
284
285 def CopyTree(src, dst, exclude=[], run_cond=None):
286   """Copy a directory tree, excluding a list of top-level entries.
287
288      The destination directory will be clobbered if it exists.
289   """
290   def copyTree(logger, subst, src, dst, exclude):
291     src = subst.SubstituteAbsPaths(src)
292     dst = subst.SubstituteAbsPaths(dst)
293     def ignoreExcludes(dir, files):
294       if dir == src:
295         return exclude
296       else:
297         return []
298     logger.debug('Copying Tree: %s -> %s', src, dst)
299     pynacl.file_tools.RemoveDirectoryIfPresent(dst)
300     shutil.copytree(src, dst, symlinks=True, ignore=ignoreExcludes)
301   return Runnable(run_cond, copyTree, src, dst, exclude)
302
303
304 def RemoveDirectory(path, run_cond=None):
305   """Convenience method for generating a command to remove a directory tree."""
306   def remove(logger, subst, path):
307     path = subst.SubstituteAbsPaths(path)
308     logger.debug('Removing Directory: %s', path)
309     pynacl.file_tools.RemoveDirectoryIfPresent(path)
310   return Runnable(run_cond, remove, path)
311
312
313 def Remove(*args):
314   """Convenience method for generating a command to remove files."""
315   def remove(logger, subst, *args):
316     for arg in args:
317       path = subst.SubstituteAbsPaths(arg)
318       logger.debug('Removing Pattern: %s', path)
319       expanded = glob.glob(path)
320       if len(expanded) == 0:
321         logger.debug('command.Remove: argument %s (substituted from %s) '
322                      'does not match any file' %
323                       (path, arg))
324       for f in expanded:
325         logger.debug('Removing File: %s', f)
326         os.remove(f)
327   return Runnable(None, remove, *args)
328
329
330 def Rename(src, dst, run_cond=None):
331   """Convenience method for generating a command to rename a file."""
332   def rename(logger, subst, src, dst):
333     src = subst.SubstituteAbsPaths(src)
334     dst = subst.SubstituteAbsPaths(dst)
335     logger.debug('Renaming: %s -> %s', src, dst)
336     os.rename(src, dst)
337   return Runnable(run_cond, rename, src, dst)
338
339
340 def WriteData(data, dst, run_cond=None):
341   """Convenience method to write a file with fixed contents."""
342   def writedata(logger, subst, dst, data):
343     dst = subst.SubstituteAbsPaths(dst)
344     logger.debug('Writing Data to File: %s', dst)
345     with open(subst.SubstituteAbsPaths(dst), 'wb') as f:
346       f.write(data)
347   return Runnable(run_cond, writedata, dst, data)
348
349
350 def SyncGitRepoCmds(url, destination, revision, clobber_invalid_repo=False,
351                     reclone=False, pathspec=None, git_cache=None, push_url=None,
352                     known_mirrors=[], push_mirrors=[],
353                     run_cond=None):
354   """Returns a list of commands to sync and validate a git repo.
355
356   Args:
357     url: Git repo URL to sync from.
358     destination: Local git repo directory to sync to.
359     revision: If not None, will sync the git repository to this revision.
360     clobber_invalid_repo: Always True for bots, but can be forced for users.
361     reclone: If True, delete the destination directory and re-clone the repo.
362     pathspec: If not None, add the path to the git checkout command, which
363               causes it to just update the working tree without switching
364               branches.
365     known_mirrors: List of tuples specifying known mirrors for a subset of the
366                    git URL. IE: [('http://mirror.com/mirror', 'http://git.com')]
367     push_mirrors: List of tuples specifying known push mirrors, see
368                   known_mirrors argument for the format.
369     git_cache: If not None, will use git_cache directory as a cache for the git
370                repository and share the objects with any other destination with
371                the same URL.
372     push_url: If not None, specifies what the push URL should be set to.
373     run_cond: Run condition for when to sync the git repo.
374
375   Returns:
376     List of commands, this is a little different from the other command funcs.
377   """
378   def update_valid_mirrors(logger, subst, url, push_url, directory,
379                            known_mirrors, push_mirrors):
380     if push_url is None:
381       push_url = url
382
383     abs_dir = subst.SubstituteAbsPaths(directory)
384     git_dir = os.path.join(abs_dir, '.git')
385     if os.path.exists(git_dir):
386       fetch_list = pynacl.repo_tools.GitRemoteRepoList(abs_dir,
387                                                        include_fetch=True,
388                                                        include_push=False,
389                                                        logger=logger)
390       tracked_fetch_url = dict(fetch_list).get('origin', 'None')
391
392       push_list = pynacl.repo_tools.GitRemoteRepoList(abs_dir,
393                                                       include_fetch=False,
394                                                       include_push=True,
395                                                       logger=logger)
396       tracked_push_url = dict(push_list).get('origin', 'None')
397
398       if ((known_mirrors and tracked_fetch_url != url) or
399           (push_mirrors and tracked_push_url != push_url)):
400         updated_fetch_url = tracked_fetch_url
401         for mirror, url_subset in known_mirrors:
402           if mirror in updated_fetch_url:
403             updated_fetch_url = updated_fetch_url.replace(mirror, url_subset)
404
405         updated_push_url = tracked_push_url
406         for mirror, url_subset in push_mirrors:
407           if mirror in updated_push_url:
408             updated_push_url = updated_push_url.replace(mirror, url_subset)
409
410         if ((updated_fetch_url != tracked_fetch_url) or
411             (updated_push_url != tracked_push_url)):
412           logger.warn('Your git repo is using an old mirror: %s', abs_dir)
413           logger.warn('Updating git repo using known mirror:')
414           logger.warn('  [FETCH] %s -> %s',
415                       tracked_fetch_url, updated_fetch_url)
416           logger.warn('  [PUSH] %s -> %s',
417                       tracked_push_url, updated_push_url)
418           pynacl.repo_tools.GitSetRemoteRepo(updated_fetch_url, abs_dir,
419                                              push_url=updated_push_url,
420                                              logger=logger)
421
422   def populate_cache(logger, subst, git_cache, url):
423     if git_cache:
424       abs_git_cache = subst.SubstituteAbsPaths(git_cache)
425       logger.debug('Populating Cache: %s [%s]', abs_git_cache, url)
426       if abs_git_cache:
427         pynacl.repo_tools.PopulateGitCache(abs_git_cache, [url],
428                                            logger=logger)
429
430   def validate(logger, subst, url, directory):
431     abs_dir = subst.SubstituteAbsPaths(directory)
432     logger.debug('Validating Repo: %s [%s]', abs_dir, url)
433     pynacl.repo_tools.ValidateGitRepo(url,
434                                       subst.SubstituteAbsPaths(directory),
435                                       clobber_mismatch=True,
436                                       logger=logger)
437
438   def sync(logger, subst, url, dest, revision, reclone, pathspec, git_cache,
439            push_url):
440     abs_dest = subst.SubstituteAbsPaths(dest)
441     if git_cache:
442       git_cache = subst.SubstituteAbsPaths(git_cache)
443
444     logger.debug('Syncing Git Repo: %s [%s]', abs_dest, url)
445     try:
446       pynacl.repo_tools.SyncGitRepo(url, abs_dest, revision,
447                                     reclone=reclone,
448                                     pathspec=pathspec, git_cache=git_cache,
449                                     push_url=push_url, logger=logger)
450     except pynacl.repo_tools.InvalidRepoException, e:
451       remote_repos = dict(pynacl.repo_tools.GitRemoteRepoList(abs_dest,
452                                                               logger=logger))
453       tracked_url = remote_repos.get('origin', 'None')
454       logger.error('Invalid Git Repo: %s' % e)
455       logger.error('Destination Directory: %s', abs_dest)
456       logger.error('Currently Tracked Repo: %s', tracked_url)
457       logger.error('Expected Repo: %s', e.expected_repo)
458       logger.warn('Possible solutions:')
459       logger.warn('  1. The simplest way if you have no local changes is to'
460                   ' simply delete the directory and let the tool resync.')
461       logger.warn('  2. If the tracked repo is merely a mirror, simply go to'
462                   ' the directory and run "git remote set-url origin %s"',
463                   e.expected_repo)
464       raise Exception('Could not validate local git repository.')
465
466   def ClobberInvalidRepoCondition(cmd_opts):
467     # Check if caller passed their own run_cond
468     if run_cond and not run_cond(cmd_opts):
469       return False
470     elif clobber_invalid_repo:
471       return True
472     return cmd_opts.IsBot()
473
474   def CleanOnBotCondition(cmd_opts):
475     # Check if caller passed their own run_cond
476     if run_cond and not run_cond(cmd_opts):
477       return False
478     return cmd_opts.IsBot()
479
480   commands = [CleanGitWorkingDir(destination, reset=True, path=None,
481                                  run_cond=CleanOnBotCondition)]
482   if git_cache:
483     commands.append(Runnable(run_cond, populate_cache, git_cache, url))
484
485   commands.extend([Runnable(run_cond, update_valid_mirrors, url, push_url,
486                             destination, known_mirrors, push_mirrors),
487                    Runnable(ClobberInvalidRepoCondition, validate, url,
488                             destination),
489                    Runnable(run_cond, sync, url, destination, revision, reclone,
490                             pathspec, git_cache, push_url)])
491   return commands
492
493
494 def CleanGitWorkingDir(directory, reset=False, path=None, run_cond=None):
495   """Clean a path in a git checkout, if the checkout directory exists."""
496   def clean(logger, subst, directory, reset, path):
497     directory = subst.SubstituteAbsPaths(directory)
498     logger.debug('Cleaning Git Working Directory: %s', directory)
499     if os.path.exists(directory) and len(os.listdir(directory)) > 0:
500       pynacl.repo_tools.CleanGitWorkingDir(directory, reset, path,logger=logger)
501   return Runnable(run_cond, clean, directory, reset, path)
502
503
504 def GenerateGitPatches(git_dir, info, run_cond=None):
505   """Generate patches from a Git repository.
506
507   Args:
508     git_dir: bare git repository directory to examine (.../.git)
509     info: dictionary containing:
510       'rev': commit that we build
511       'upstream-name': basename of the upstream baseline release
512         (i.e. what the release tarball would be called before ".tar")
513       'upstream-base': commit corresponding to upstream-name
514       'upstream-branch': tracking branch used for upstream merges
515
516   This will produce between zero and two patch files (in %(output)s/):
517     <upstream-name>-g<commit-abbrev>.patch: From 'upstream-base' to the common
518       ancestor (merge base) of 'rev' and 'upstream-branch'.  Omitted if none.
519     <upstream-name>[-g<commit-abbrev>]-nacl.patch: From the result of that
520       (or from 'upstream-base' if none above) to 'rev'.
521   """
522   def generatePatches(logger, subst, git_dir, info, run_cond=None):
523     git_dir = subst.SubstituteAbsPaths(git_dir)
524     git_dir_flag = '--git-dir=' + git_dir
525     basename = info['upstream-name']
526     logger.debug('Generating Git Patches: %s', git_dir)
527
528     patch_files = []
529
530     def generatePatch(description, src_rev, dst_rev, suffix):
531       src_prefix = '--src-prefix=' + basename + '/'
532       dst_prefix = '--dst-prefix=' + basename + suffix + '/'
533       patch_name = basename + suffix + '.patch'
534       patch_file = subst.SubstituteAbsPaths(path.join('%(output)s', patch_name))
535       git_args = [git_dir_flag, 'diff',
536                   '--patch-with-stat', '--ignore-space-at-eol', '--full-index',
537                   '--no-ext-diff', '--no-color', '--no-renames',
538                   '--no-textconv', '--text', src_prefix, dst_prefix,
539                   src_rev, dst_rev]
540       pynacl.log_tools.CheckCall(
541           pynacl.repo_tools.GitCmd() + git_args,
542           stdout=patch_file,
543           logger=logger,
544       )
545       patch_files.append((description, patch_name))
546
547     def revParse(args):
548       output = pynacl.repo_tools.CheckGitOutput([git_dir_flag] + args)
549       lines = output.splitlines()
550       if len(lines) != 1:
551         raise Exception('"git %s" did not yield a single commit' %
552                         ' '.join(args))
553       return lines[0]
554
555     rev = revParse(['rev-parse', info['rev']])
556     upstream_base = revParse(['rev-parse', info['upstream-base']])
557     upstream_branch = revParse(['rev-parse',
558                                 'refs/remotes/origin/' +
559                                 info['upstream-branch']])
560     upstream_snapshot = revParse(['merge-base', rev, upstream_branch])
561
562     if rev == upstream_base:
563       # We're building a stock upstream release.  Nothing to do!
564       return
565
566     if upstream_snapshot == upstream_base:
567       # We've forked directly from the upstream baseline release.
568       suffix = ''
569     else:
570       # We're using an upstream baseline snapshot past the baseline
571       # release, so generate a snapshot patch.  The leading seven
572       # hex digits of the commit ID is what Git usually produces
573       # for --abbrev-commit behavior, 'git describe', etc.
574       suffix = '-g' + upstream_snapshot[:7]
575       generatePatch('Patch the release up to the upstream snapshot version.',
576                     upstream_base, upstream_snapshot, suffix)
577
578     if rev != upstream_snapshot:
579       # We're using local changes, so generate a patch of those.
580       generatePatch('Apply NaCl-specific changes.',
581                     upstream_snapshot, rev, suffix + '-nacl')
582
583     with open(subst.SubstituteAbsPaths(path.join('%(output)s',
584                                                  info['name'] + '.series')),
585               'w') as f:
586       f.write("""\
587 # This is a "series file" in the style used by the "quilt" tool.
588 # It describes how to unpack and apply patches to produce the source
589 # tree of the %(name)s component of a toolchain targetting Native Client.
590
591 # Source: %(upstream-name)s.tar
592 """
593               % info)
594       for patch in patch_files:
595         f.write('\n# %s\n%s\n' % patch)
596
597   return Runnable(run_cond, generatePatches, git_dir, info)