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',
46 'platform_tools.py', 'repo_tools.py')]
48 def HashBuildSystemSources():
49 """Read the build source files to use in hashes for Callbacks."""
50 global FILE_CONTENTS_HASH
52 for filename in COMMAND_CODE_FILES:
53 with open(filename) as f:
55 FILE_CONTENTS_HASH = h.hexdigest()
57 HashBuildSystemSources()
60 def PlatformEnvironment(extra_paths):
61 """Select the environment variables to run commands with.
64 extra_paths: Extra paths to add to the PATH variable.
66 A dict to be passed as env to subprocess.
68 env = os.environ.copy()
70 if sys.platform == 'win32':
71 if Runnable.use_cygwin:
72 # Use the hermetic cygwin.
73 paths = [os.path.join(NACL_DIR, 'cygwin', 'bin')]
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.
87 os.path.join(mingw, 'bin'),
88 os.path.join(msys, 'bin'),
90 env['PATH'] = os.pathsep.join(
91 paths + extra_paths + env.get('PATH', '').split(os.pathsep))
95 class Runnable(object):
96 """An object representing a single command."""
99 def __init__(self, func, *args, **kwargs):
100 """Construct a runnable which will call 'func' with 'args' and 'kwargs'.
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
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).
113 When 'func' is called, its first argument will be a substitution object
114 which it can use to substitute %-templates in its arguments.
117 self._args = args or []
118 self._kwargs = kwargs or {}
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']])
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))
138 for k, v in self._kwargs.iteritems():
139 values += [repr(k), repr(v)]
140 values += [FILE_CONTENTS_HASH]
142 return '\n'.join(values)
144 def Invoke(self, subst):
145 return self._func(subst, *self._args, **self._kwargs)
148 def Command(command, **kwargs):
149 """Return a Runnable which invokes 'command' with check_call.
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')
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.
161 def runcmd(subst, command, **kwargs):
162 check_call_kwargs = kwargs.copy()
165 cwd = subst.SubstituteAbsPaths(check_call_kwargs.get('cwd', '.'))
167 check_call_kwargs['cwd'] = cwd
169 # Extract paths from kwargs and add to the command environment.
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)
177 if isinstance(command, str):
178 command = subst.Substitute(command)
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)
184 log_tools.CheckCall(command, **check_call_kwargs)
186 return Runnable(runcmd, command, **kwargs)
188 def SkipForIncrementalCommand(command, **kwargs):
189 """Return a command which has the skip_for_incremental property set on it.
191 This will cause the command to be skipped for incremental builds, if the
192 working directory is not empty.
194 cmd = Command(command, **kwargs)
195 cmd.skip_for_incremental = True
198 def Mkdir(path, parents=False):
199 """Convenience method for generating mkdir commands."""
200 def mkdir(subst, path):
201 path = subst.SubstituteAbsPaths(path)
206 return Runnable(mkdir, path)
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)
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):
227 file_tools.RemoveDirectoryIfPresent(dst)
228 shutil.copytree(src, dst, symlinks=True, ignore=ignoreExcludes)
229 return Runnable(copyTree, src, dst, exclude)
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)
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):
245 return Runnable(remove, path)
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)
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:
260 return Runnable(writedata, dst, data)
263 def SyncGitRepo(url, destination, revision, reclone=False, clean=False,
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)
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)