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.
6 """Class capturing a command invocation as data."""
9 # Done first to setup python module path.
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.
35 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
36 NACL_DIR = os.path.dirname(SCRIPT_DIR)
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_env.py',
42 COMMAND_CODE_FILES += [os.path.join(NACL_DIR, 'build', f)
43 for f in ('directory_storage.py', 'file_tools.py',
44 'gsd_storage.py', 'hashing_tools.py',
45 'local_storage_cache.py', 'log_tools.py',
47 COMMAND_CODE_FILES += [os.path.join(NACL_DIR, 'pynacl', f)
48 for f in ('platform.py',)]
50 def HashBuildSystemSources():
51 """Read the build source files to use in hashes for Callbacks."""
52 global FILE_CONTENTS_HASH
54 for filename in COMMAND_CODE_FILES:
55 with open(filename) as f:
57 FILE_CONTENTS_HASH = h.hexdigest()
59 HashBuildSystemSources()
62 def PlatformEnvironment(extra_paths):
63 """Select the environment variables to run commands with.
66 extra_paths: Extra paths to add to the PATH variable.
68 A dict to be passed as env to subprocess.
70 env = os.environ.copy()
72 if sys.platform == 'win32':
73 if Runnable.use_cygwin:
74 # Use the hermetic cygwin.
75 paths = [os.path.join(NACL_DIR, 'cygwin', 'bin')]
77 # TODO(bradnelson): switch to something hermetic.
78 mingw = os.environ.get('MINGW', r'c:\mingw')
79 msys = os.path.join(mingw, 'msys', '1.0')
80 if not os.path.exists(msys):
81 msys = os.path.join(mingw, 'msys')
82 # We need both msys (posix like build environment) and MinGW (windows
83 # build of tools like gcc). We add <MINGW>/msys/[1.0/]bin to the path to
84 # get sh.exe. We add <MINGW>/bin to allow direct invocation on MinGW
85 # tools. We also add an msys style path (/mingw/bin) to get things like
86 # gcc from inside msys.
89 os.path.join(mingw, 'bin'),
90 os.path.join(msys, 'bin'),
92 env['PATH'] = os.pathsep.join(
93 paths + extra_paths + env.get('PATH', '').split(os.pathsep))
97 class Runnable(object):
98 """An object representing a single command."""
101 def __init__(self, func, *args, **kwargs):
102 """Construct a runnable which will call 'func' with 'args' and 'kwargs'.
105 func: Function which will be called by Invoke
106 args: Positional arguments to be passed to func
107 kwargs: Keyword arguments to be passed to func
109 RUNNABLES SHOULD ONLY BE IMPLEMENTED IN THIS FILE, because their
110 string representation (which is used to calculate whether targets should
111 be rebuilt) is based on this file's hash and does not attempt to capture
112 the code or bound variables of the function itself (the one exception is
113 once_test.py which injects its own callbacks to verify its expectations).
115 When 'func' is called, its first argument will be a substitution object
116 which it can use to substitute %-templates in its arguments.
119 self._args = args or []
120 self._kwargs = kwargs or {}
125 sourcefile = inspect.getsourcefile(self._func)
126 # Check that the code for the runnable is implemented in one of the known
127 # source files of the build system (which are included in its hash). This
128 # isn't a perfect test because it could still import code from an outside
129 # module, so we should be sure to add any new build system files to the list
130 found_match = (os.path.basename(sourcefile) in
131 [os.path.basename(f) for f in
132 COMMAND_CODE_FILES + ['once_test.py']])
134 print 'Function', self._func.func_name, 'in', sourcefile
135 raise Exception('Python Runnable objects must be implemented in one of' +
136 'the following files: ' + str(COMMAND_CODE_FILES))
138 # Like repr(datum), but do something stable for dictionaries.
139 # This only properly handles dictionaries that use simple types
141 def ReprForHash(datum):
142 if isinstance(datum, dict):
143 # The order of a dictionary's items is unpredictable.
144 # Manually make a string in dict syntax, but sorted on keys.
146 ', '.join(repr(key) + ': ' + ReprForHash(value)
147 for key, value in sorted(datum.iteritems(),
148 key=lambda t: t[0])) +
150 elif isinstance(datum, list):
151 # A list is already ordered, but its items might be dictionaries.
153 ', '.join(ReprForHash(value) for value in datum) +
159 values += [ReprForHash(v)]
160 # The order of a dictionary's items is unpredictable.
161 # Sort by key for hashing purposes.
162 for k, v in sorted(self._kwargs.iteritems(), key=lambda t: t[0]):
163 values += [repr(k), ReprForHash(v)]
164 values += [FILE_CONTENTS_HASH]
166 return '\n'.join(values)
168 def Invoke(self, subst):
169 return self._func(subst, *self._args, **self._kwargs)
172 def Command(command, stdout=None, **kwargs):
173 """Return a Runnable which invokes 'command' with check_call.
176 command: List or string with a command suitable for check_call
177 stdout (optional): File name to redirect command's stdout to
178 kwargs: Keyword arguments suitable for check_call (or 'cwd' or 'path_dirs')
180 The command will be %-substituted and paths will be assumed to be relative to
181 the cwd given by Invoke. If kwargs contains 'cwd' it will be appended to the
182 cwd given by Invoke and used as the cwd for the call. If kwargs contains
183 'path_dirs', the directories therein will be added to the paths searched for
184 the command. Any other kwargs will be passed to check_call.
186 def runcmd(subst, command, stdout, **kwargs):
187 check_call_kwargs = kwargs.copy()
190 cwd = subst.SubstituteAbsPaths(check_call_kwargs.get('cwd', '.'))
192 check_call_kwargs['cwd'] = cwd
194 # Extract paths from kwargs and add to the command environment.
196 if 'path_dirs' in check_call_kwargs:
197 path_dirs = [subst.Substitute(dirname) for dirname
198 in check_call_kwargs['path_dirs']]
199 del check_call_kwargs['path_dirs']
200 check_call_kwargs['env'] = PlatformEnvironment(path_dirs)
202 if isinstance(command, str):
203 command = subst.Substitute(command)
205 command = [subst.Substitute(arg) for arg in command]
206 paths = check_call_kwargs['env']['PATH'].split(os.pathsep)
207 command[0] = file_tools.Which(command[0], paths=paths)
209 if stdout is not None:
210 stdout = subst.SubstituteAbsPaths(stdout)
212 log_tools.CheckCall(command, stdout=stdout, **check_call_kwargs)
214 return Runnable(runcmd, command, stdout, **kwargs)
216 def SkipForIncrementalCommand(command, **kwargs):
217 """Return a command which has the skip_for_incremental property set on it.
219 This will cause the command to be skipped for incremental builds, if the
220 working directory is not empty.
222 cmd = Command(command, **kwargs)
223 cmd.skip_for_incremental = True
226 def Mkdir(path, parents=False):
227 """Convenience method for generating mkdir commands."""
228 def mkdir(subst, path):
229 path = subst.SubstituteAbsPaths(path)
234 return Runnable(mkdir, path)
238 """Convenience method for generating cp commands."""
239 def copy(subst, src, dst):
240 shutil.copyfile(subst.SubstituteAbsPaths(src),
241 subst.SubstituteAbsPaths(dst))
242 return Runnable(copy, src, dst)
245 def CopyTree(src, dst, exclude=[]):
246 """Copy a directory tree, excluding a list of top-level entries."""
247 def copyTree(subst, src, dst, exclude):
248 src = subst.SubstituteAbsPaths(src)
249 dst = subst.SubstituteAbsPaths(dst)
250 def ignoreExcludes(dir, files):
255 file_tools.RemoveDirectoryIfPresent(dst)
256 shutil.copytree(src, dst, symlinks=True, ignore=ignoreExcludes)
257 return Runnable(copyTree, src, dst, exclude)
260 def RemoveDirectory(path):
261 """Convenience method for generating a command to remove a directory tree."""
262 def remove(subst, path):
263 file_tools.RemoveDirectoryIfPresent(subst.SubstituteAbsPaths(path))
264 return Runnable(remove, path)
268 """Convenience method for generating a command to remove a file."""
269 def remove(subst, path):
270 path = subst.SubstituteAbsPaths(path)
271 if os.path.exists(path):
273 return Runnable(remove, path)
276 def Rename(src, dst):
277 """Convenience method for generating a command to rename a file."""
278 def rename(subst, src, dst):
279 os.rename(subst.SubstituteAbsPaths(src), subst.SubstituteAbsPaths(dst))
280 return Runnable(rename, src, dst)
283 def WriteData(data, dst):
284 """Convenience method to write a file with fixed contents."""
285 def writedata(subst, dst, data):
286 with open(subst.SubstituteAbsPaths(dst), 'wb') as f:
288 return Runnable(writedata, dst, data)
291 def SyncGitRepo(url, destination, revision, reclone=False, clean=False,
293 def sync(subst, url, dest, rev, reclone, clean, pathspec):
294 repo_tools.SyncGitRepo(url, subst.SubstituteAbsPaths(dest), revision,
295 reclone, clean, pathspec)
296 return Runnable(sync, url, destination, revision, reclone, clean, pathspec)
299 def CleanGitWorkingDir(directory, path):
300 """Clean a path in a git checkout, if the checkout directory exists."""
301 def clean(subst, directory, path):
302 directory = subst.SubstituteAbsPaths(directory)
303 if os.path.exists(directory) and len(os.listdir(directory)) > 0:
304 repo_tools.CleanGitWorkingDir(directory, path)
305 return Runnable(clean, directory, path)
308 def GenerateGitPatches(git_dir, info):
309 """Generate patches from a Git repository.
312 git_dir: bare git repository directory to examine (.../.git)
313 info: dictionary containing:
314 'rev': commit that we build
315 'upstream-name': basename of the upstream baseline release
316 (i.e. what the release tarball would be called before ".tar")
317 'upstream-base': commit corresponding to upstream-name
318 'upstream-branch': tracking branch used for upstream merges
320 This will produce between zero and two patch files (in %(output)s/):
321 <upstream-name>-g<commit-abbrev>.patch: From 'upstream-base' to the common
322 ancestor (merge base) of 'rev' and 'upstream-branch'. Omitted if none.
323 <upstream-name>[-g<commit-abbrev>]-nacl.patch: From the result of that
324 (or from 'upstream-base' if none above) to 'rev'.
326 def generatePatches(subst, git_dir, info):
327 git_dir_flag = '--git-dir=' + subst.SubstituteAbsPaths(git_dir)
328 basename = info['upstream-name']
330 def generatePatch(src_rev, dst_rev, suffix):
331 src_prefix = '--src-prefix=' + basename + '/'
332 dst_prefix = '--dst-prefix=' + basename + suffix + '/'
333 patch_file = subst.SubstituteAbsPaths(
334 path.join('%(output)s', basename + suffix + '.patch'))
335 git_args = [git_dir_flag, 'diff',
336 '--patch-with-stat', '--ignore-space-at-eol', '--full-index',
337 '--no-ext-diff', '--no-color', '--no-renames',
338 '--no-textconv', '--text', src_prefix, dst_prefix,
340 log_tools.CheckCall(repo_tools.GitCmd() + git_args, stdout=patch_file)
343 output = repo_tools.CheckGitOutput([git_dir_flag] + args)
344 lines = output.splitlines()
346 raise Exception('"git %s" did not yield a single commit' %
350 rev = revParse(['rev-parse', info['rev']])
351 upstream_base = revParse(['rev-parse', info['upstream-base']])
352 upstream_branch = revParse(['rev-parse',
353 'refs/remotes/origin/' +
354 info['upstream-branch']])
355 upstream_snapshot = revParse(['merge-base', rev, upstream_branch])
357 if rev == upstream_base:
358 # We're building a stock upstream release. Nothing to do!
361 if upstream_snapshot == upstream_base:
362 # We've forked directly from the upstream baseline release.
365 # We're using an upstream baseline snapshot past the baseline
366 # release, so generate a snapshot patch. The leading seven
367 # hex digits of the commit ID is what Git usually produces
368 # for --abbrev-commit behavior, 'git describe', etc.
369 suffix = '-g' + upstream_snapshot[:7]
370 generatePatch(upstream_base, upstream_snapshot, suffix)
372 if rev != upstream_snapshot:
373 # We're using local changes, so generate a patch of those.
374 generatePatch(upstream_snapshot, rev, suffix + '-nacl')
375 return Runnable(generatePatches, git_dir, info)