ba46006712ac0f90e6dfcaec930bc288ad2d6a5d
[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                                  'platform_tools.py', 'repo_tools.py')]
47
48 def HashBuildSystemSources():
49   """Read the build source files to use in hashes for Callbacks."""
50   global FILE_CONTENTS_HASH
51   h = hashlib.sha1()
52   for filename in COMMAND_CODE_FILES:
53     with open(filename) as f:
54       h.update(f.read())
55   FILE_CONTENTS_HASH = h.hexdigest()
56
57 HashBuildSystemSources()
58
59
60 def PlatformEnvironment(extra_paths):
61   """Select the environment variables to run commands with.
62
63   Args:
64     extra_paths: Extra paths to add to the PATH variable.
65   Returns:
66     A dict to be passed as env to subprocess.
67   """
68   env = os.environ.copy()
69   paths = []
70   if sys.platform == 'win32':
71     if Runnable.use_cygwin:
72       # Use the hermetic cygwin.
73       paths = [os.path.join(NACL_DIR, 'cygwin', 'bin')]
74     else:
75       # TODO(bradnelson): switch to something hermetic.
76       mingw = os.environ.get('MINGW', r'c:\mingw')
77       msys = os.path.join(mingw, 'msys', '1.0')
78       if not os.path.exists(msys):
79         msys = os.path.join(mingw, 'msys')
80       # We need both msys (posix like build environment) and MinGW (windows
81       # build of tools like gcc). We add <MINGW>/msys/[1.0/]bin to the path to
82       # get sh.exe. We add <MINGW>/bin to allow direct invocation on MinGW
83       # tools. We also add an msys style path (/mingw/bin) to get things like
84       # gcc from inside msys.
85       paths = [
86           '/mingw/bin',
87           os.path.join(mingw, 'bin'),
88           os.path.join(msys, 'bin'),
89       ]
90   env['PATH'] = os.pathsep.join(
91       paths + extra_paths + env.get('PATH', '').split(os.pathsep))
92   return env
93
94
95 class Runnable(object):
96   """An object representing a single command."""
97   use_cygwin = False
98
99   def __init__(self, func, *args, **kwargs):
100     """Construct a runnable which will call 'func' with 'args' and 'kwargs'.
101
102     Args:
103       func: Function which will be called by Invoke
104       args: Positional arguments to be passed to func
105       kwargs: Keyword arguments to be passed to func
106
107       RUNNABLES SHOULD ONLY BE IMPLEMENTED IN THIS FILE, because their
108       string representation (which is used to calculate whether targets should
109       be rebuilt) is based on this file's hash and does not attempt to capture
110       the code or bound variables of the function itself (the one exception is
111       once_test.py which injects its own callbacks to verify its expectations).
112
113       When 'func' is called, its first argument will be a substitution object
114       which it can use to substitute %-templates in its arguments.
115     """
116     self._func = func
117     self._args = args or []
118     self._kwargs = kwargs or {}
119
120   def __str__(self):
121     values = []
122
123     sourcefile = inspect.getsourcefile(self._func)
124     # Check that the code for the runnable is implemented in one of the known
125     # source files of the build system (which are included in its hash). This
126     # isn't a perfect test because it could still import code from an outside
127     # module, so we should be sure to add any new build system files to the list
128     found_match = (os.path.basename(sourcefile) in
129                    [os.path.basename(f) for f in
130                     COMMAND_CODE_FILES + ['once_test.py']])
131     if not found_match:
132       print 'Function', self._func.func_name, 'in', sourcefile
133       raise Exception('Python Runnable objects must be implemented in one of' +
134                       'the following files: ' + str(COMMAND_CODE_FILES))
135
136     for v in self._args:
137       values += [repr(v)]
138     for k, v in self._kwargs.iteritems():
139       values += [repr(k), repr(v)]
140     values += [FILE_CONTENTS_HASH]
141
142     return '\n'.join(values)
143
144   def Invoke(self, subst):
145     return self._func(subst, *self._args, **self._kwargs)
146
147
148 def Command(command, **kwargs):
149   """Return a Runnable which invokes 'command' with check_call.
150
151   Args:
152     command: List or string with a command suitable for check_call
153     kwargs: Keyword arguments suitable for check_call (or 'cwd' or 'path_dirs')
154
155   The command will be %-substituted and paths will be assumed to be relative to
156   the cwd given by Invoke. If kwargs contains 'cwd' it will be appended to the
157   cwd given by Invoke and used as the cwd for the call. If kwargs contains
158   'path_dirs', the directories therein will be added to the paths searched for
159   the command. Any other kwargs will be passed to check_call.
160   """
161   def runcmd(subst, command, **kwargs):
162     check_call_kwargs = kwargs.copy()
163     command = command[:]
164
165     cwd = subst.SubstituteAbsPaths(check_call_kwargs.get('cwd', '.'))
166     subst.SetCwd(cwd)
167     check_call_kwargs['cwd'] = cwd
168
169     # Extract paths from kwargs and add to the command environment.
170     path_dirs = []
171     if 'path_dirs' in check_call_kwargs:
172       path_dirs = [subst.Substitute(dirname) for dirname
173                    in check_call_kwargs['path_dirs']]
174       del check_call_kwargs['path_dirs']
175     check_call_kwargs['env'] = PlatformEnvironment(path_dirs)
176
177     if isinstance(command, str):
178       command = subst.Substitute(command)
179     else:
180       command = [subst.Substitute(arg) for arg in command]
181       paths = check_call_kwargs['env']['PATH'].split(os.pathsep)
182       command[0] = file_tools.Which(command[0], paths=paths)
183
184     log_tools.CheckCall(command, **check_call_kwargs)
185
186   return Runnable(runcmd, command, **kwargs)
187
188 def SkipForIncrementalCommand(command, **kwargs):
189   """Return a command which has the skip_for_incremental property set on it.
190
191   This will cause the command to be skipped for incremental builds, if the
192   working directory is not empty.
193   """
194   cmd = Command(command, **kwargs)
195   cmd.skip_for_incremental = True
196   return cmd
197
198 def Mkdir(path, parents=False):
199   """Convenience method for generating mkdir commands."""
200   def mkdir(subst, path):
201     path = subst.SubstituteAbsPaths(path)
202     if parents:
203       os.makedirs(path)
204     else:
205       os.mkdir(path)
206   return Runnable(mkdir, path)
207
208
209 def Copy(src, dst):
210   """Convenience method for generating cp commands."""
211   def copy(subst, src, dst):
212     shutil.copyfile(subst.SubstituteAbsPaths(src),
213                     subst.SubstituteAbsPaths(dst))
214   return Runnable(copy, src, dst)
215
216
217 def CopyTree(src, dst, exclude=[]):
218   """Copy a directory tree, excluding a list of top-level entries."""
219   def copyTree(subst, src, dst, exclude):
220     src = subst.SubstituteAbsPaths(src)
221     dst = subst.SubstituteAbsPaths(dst)
222     def ignoreExcludes(dir, files):
223       if dir == src:
224         return exclude
225       else:
226         return []
227     file_tools.RemoveDirectoryIfPresent(dst)
228     shutil.copytree(src, dst, symlinks=True, ignore=ignoreExcludes)
229   return Runnable(copyTree, src, dst, exclude)
230
231
232 def RemoveDirectory(path):
233   """Convenience method for generating a command to remove a directory tree."""
234   def remove(subst, path):
235     file_tools.RemoveDirectoryIfPresent(subst.SubstituteAbsPaths(path))
236   return Runnable(remove, path)
237
238
239 def Remove(path):
240   """Convenience method for generating a command to remove a file."""
241   def remove(subst, path):
242     path = subst.SubstituteAbsPaths(path)
243     if os.path.exists(path):
244       os.remove(path)
245   return Runnable(remove, path)
246
247
248 def Rename(src, dst):
249   """Convenience method for generating a command to rename a file."""
250   def rename(subst, src, dst):
251     os.rename(subst.SubstituteAbsPaths(src), subst.SubstituteAbsPaths(dst))
252   return Runnable(rename, src, dst)
253
254
255 def WriteData(data, dst):
256   """Convenience method to write a file with fixed contents."""
257   def writedata(subst, dst, data):
258     with open(subst.SubstituteAbsPaths(dst), 'wb') as f:
259       f.write(data)
260   return Runnable(writedata, dst, data)
261
262
263 def SyncGitRepo(url, destination, revision, reclone=False, clean=False,
264                 pathspec=None):
265   def sync(subst, url, dest, rev, reclone, clean, pathspec):
266     repo_tools.SyncGitRepo(url, subst.SubstituteAbsPaths(dest), revision,
267                            reclone, clean, pathspec)
268   return Runnable(sync, url, destination, revision, reclone, clean, pathspec)
269
270
271 def CleanGitWorkingDir(directory, path):
272   """Clean a path in a git checkout, if the checkout directory exists."""
273   def clean(subst, directory, path):
274     directory = subst.SubstituteAbsPaths(directory)
275     if os.path.exists(directory) and len(os.listdir(directory)) > 0:
276       repo_tools.CleanGitWorkingDir(directory, path)
277   return Runnable(clean, directory, path)