Upstream version 5.34.104.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
9 # Done first to setup python module path.
10 import toolchain_env
11
12 import inspect
13 import hashlib
14 import os
15 import shutil
16 import sys
17
18 import file_tools
19 import log_tools
20 import repo_tools
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_env.py',
41                                 'toolchain_main.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',
46                                  'repo_tools.py')]
47 COMMAND_CODE_FILES += [os.path.join(NACL_DIR, 'pynacl', f)
48                        for f in ('platform.py',)]
49
50 def HashBuildSystemSources():
51   """Read the build source files to use in hashes for Callbacks."""
52   global FILE_CONTENTS_HASH
53   h = hashlib.sha1()
54   for filename in COMMAND_CODE_FILES:
55     with open(filename) as f:
56       h.update(f.read())
57   FILE_CONTENTS_HASH = h.hexdigest()
58
59 HashBuildSystemSources()
60
61
62 def PlatformEnvironment(extra_paths):
63   """Select the environment variables to run commands with.
64
65   Args:
66     extra_paths: Extra paths to add to the PATH variable.
67   Returns:
68     A dict to be passed as env to subprocess.
69   """
70   env = os.environ.copy()
71   paths = []
72   if sys.platform == 'win32':
73     if Runnable.use_cygwin:
74       # Use the hermetic cygwin.
75       paths = [os.path.join(NACL_DIR, 'cygwin', 'bin')]
76     else:
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.
87       paths = [
88           '/mingw/bin',
89           os.path.join(mingw, 'bin'),
90           os.path.join(msys, 'bin'),
91       ]
92   env['PATH'] = os.pathsep.join(
93       paths + extra_paths + env.get('PATH', '').split(os.pathsep))
94   return env
95
96
97 class Runnable(object):
98   """An object representing a single command."""
99   use_cygwin = False
100
101   def __init__(self, func, *args, **kwargs):
102     """Construct a runnable which will call 'func' with 'args' and 'kwargs'.
103
104     Args:
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
108
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).
114
115       When 'func' is called, its first argument will be a substitution object
116       which it can use to substitute %-templates in its arguments.
117     """
118     self._func = func
119     self._args = args or []
120     self._kwargs = kwargs or {}
121
122   def __str__(self):
123     values = []
124
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']])
133     if not found_match:
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))
137
138     # Like repr(datum), but do something stable for dictionaries.
139     # This only properly handles dictionaries that use simple types
140     # as keys.
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.
145         return ('{' +
146                 ', '.join(repr(key) + ': ' + ReprForHash(value)
147                           for key, value in sorted(datum.iteritems(),
148                                                    key=lambda t: t[0])) +
149                 '}')
150       elif isinstance(datum, list):
151         # A list is already ordered, but its items might be dictionaries.
152         return ('[' +
153                 ', '.join(ReprForHash(value) for value in datum) +
154                 ']')
155       else:
156         return repr(datum)
157
158     for v in self._args:
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]
165
166     return '\n'.join(values)
167
168   def Invoke(self, subst):
169     return self._func(subst, *self._args, **self._kwargs)
170
171
172 def Command(command, stdout=None, **kwargs):
173   """Return a Runnable which invokes 'command' with check_call.
174
175   Args:
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')
179
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.
185   """
186   def runcmd(subst, command, stdout, **kwargs):
187     check_call_kwargs = kwargs.copy()
188     command = command[:]
189
190     cwd = subst.SubstituteAbsPaths(check_call_kwargs.get('cwd', '.'))
191     subst.SetCwd(cwd)
192     check_call_kwargs['cwd'] = cwd
193
194     # Extract paths from kwargs and add to the command environment.
195     path_dirs = []
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)
201
202     if isinstance(command, str):
203       command = subst.Substitute(command)
204     else:
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)
208
209     if stdout is not None:
210       stdout = subst.SubstituteAbsPaths(stdout)
211
212     log_tools.CheckCall(command, stdout=stdout, **check_call_kwargs)
213
214   return Runnable(runcmd, command, stdout, **kwargs)
215
216 def SkipForIncrementalCommand(command, **kwargs):
217   """Return a command which has the skip_for_incremental property set on it.
218
219   This will cause the command to be skipped for incremental builds, if the
220   working directory is not empty.
221   """
222   cmd = Command(command, **kwargs)
223   cmd.skip_for_incremental = True
224   return cmd
225
226 def Mkdir(path, parents=False):
227   """Convenience method for generating mkdir commands."""
228   def mkdir(subst, path):
229     path = subst.SubstituteAbsPaths(path)
230     if parents:
231       os.makedirs(path)
232     else:
233       os.mkdir(path)
234   return Runnable(mkdir, path)
235
236
237 def Copy(src, dst):
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)
243
244
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):
251       if dir == src:
252         return exclude
253       else:
254         return []
255     file_tools.RemoveDirectoryIfPresent(dst)
256     shutil.copytree(src, dst, symlinks=True, ignore=ignoreExcludes)
257   return Runnable(copyTree, src, dst, exclude)
258
259
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)
265
266
267 def 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):
272       os.remove(path)
273   return Runnable(remove, path)
274
275
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)
281
282
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:
287       f.write(data)
288   return Runnable(writedata, dst, data)
289
290
291 def SyncGitRepo(url, destination, revision, reclone=False, clean=False,
292                 pathspec=None):
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)
297
298
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)
306
307
308 def GenerateGitPatches(git_dir, info):
309   """Generate patches from a Git repository.
310
311   Args:
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
319
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'.
325   """
326   def generatePatches(subst, git_dir, info):
327     git_dir_flag = '--git-dir=' + subst.SubstituteAbsPaths(git_dir)
328     basename = info['upstream-name']
329
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,
339                   src_rev, dst_rev]
340       log_tools.CheckCall(repo_tools.GitCmd() + git_args, stdout=patch_file)
341
342     def revParse(args):
343       output = repo_tools.CheckGitOutput([git_dir_flag] + args)
344       lines = output.splitlines()
345       if len(lines) != 1:
346         raise Exception('"git %s" did not yield a single commit' %
347                         ' '.join(args))
348       return lines[0]
349
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])
356
357     if rev == upstream_base:
358       # We're building a stock upstream release.  Nothing to do!
359       return
360
361     if upstream_snapshot == upstream_base:
362       # We've forked directly from the upstream baseline release.
363       suffix = ''
364     else:
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)
371
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)