OLD | NEW |
---|---|
(Empty) | |
1 # Copyright (c) 2016 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 """ Set of basic operations/utilities that are used by repacakging builds | |
dimu
2016/08/20 00:35:15
add a comment where most functions moved from.
| |
6 for bisect """ | |
7 | |
8 import os | |
9 import ast | |
10 import base64 | |
11 import cStringIO | |
12 import copy | |
13 import errno | |
14 import fnmatch | |
15 import glob | |
16 import math | |
17 import multiprocessing | |
18 import os | |
19 import re | |
20 import shutil | |
21 import socket | |
22 import stat | |
23 import string # pylint: disable=W0402 | |
24 import subprocess | |
25 import sys | |
26 import threading | |
27 import time | |
28 import traceback | |
29 import urllib | |
30 import zipfile | |
31 import zlib | |
32 | |
33 CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with ' | |
34 'no configured credentials') | |
35 | |
36 class ExternalError(Exception): | |
37 pass | |
38 def IsWindows(): | |
39 return sys.platform == 'cygwin' or sys.platform.startswith('win') | |
40 | |
41 def IsLinux(): | |
42 return sys.platform.startswith('linux') | |
43 | |
44 def IsMac(): | |
45 return sys.platform.startswith('darwin') | |
46 | |
47 WIN_LINK_FUNC = None | |
48 | |
49 try: | |
50 if sys.platform.startswith('win'): | |
51 import ctypes | |
52 # There's 4 possibilities on Windows for links: | |
53 # 1. Symbolic file links; | |
54 # 2. Symbolic directory links; | |
55 # 3. Hardlinked files; | |
56 # 4. Junctioned directories. | |
57 # (Hardlinked directories don't really exist.) | |
58 # | |
59 # 7-Zip does not handle symbolic file links as we want (it puts the | |
60 # content of the link, not what it refers to, and reports "CRC Error" on | |
61 # extraction). It does work as expected for symbolic directory links. | |
62 # Because the majority of the large files are in the root of the staging | |
63 # directory, we do however need to handle file links, so we do this with | |
64 # hardlinking. Junctioning requires a huge whack of code, so we take the | |
65 # slightly odd tactic of using #2 and #3, but not #1 and #4. That is, | |
66 # hardlinks for files, but symbolic links for directories. | |
67 def _WIN_LINK_FUNC(src, dst): | |
68 print 'linking %s -> %s' % (src, dst) | |
69 if os.path.isdir(src): | |
70 if not ctypes.windll.kernel32.CreateSymbolicLinkA( | |
71 str(dst), str(os.path.abspath(src)), 1): | |
72 raise ctypes.WinError() | |
73 else: | |
74 if not ctypes.windll.kernel32.CreateHardLinkA(str(dst), str(src), 0): | |
75 raise ctypes.WinError() | |
76 WIN_LINK_FUNC = _WIN_LINK_FUNC | |
77 except ImportError: | |
78 # If we don't have ctypes or aren't on Windows, leave WIN_LINK_FUNC as None. | |
79 pass | |
80 | |
81 | |
82 | |
83 class PathNotFound(Exception): | |
84 pass | |
85 | |
86 | |
87 def isGitCommitHash(regex_match): | |
88 """Checks if match is correct SHA1 hash""" | |
89 matched_re = re.match(r"^[0-9,A-F]{40}$", regex_match.upper()) | |
90 if matched_re: return True | |
91 return False | |
92 | |
93 def isCommitPosition(regex_match): | |
94 """Checks if match is correct revision(Cp number) format""" | |
95 matched_re = re.match(r"^[0-9]{6}$", regex_match) | |
96 if matched_re: return True | |
97 return False | |
98 | |
99 def MaybeMakeDirectory(*path): | |
100 """Creates an entire path, if it doesn't already exist.""" | |
101 file_path = os.path.join(*path) | |
102 try: | |
103 os.makedirs(file_path) | |
104 except OSError, e: | |
105 if e.errno != errno.EEXIST: | |
106 raise | |
107 | |
108 def RemovePath(*path): | |
109 """Removes the file or directory at 'path', if it exists.""" | |
110 file_path = os.path.join(*path) | |
111 if os.path.exists(file_path): | |
112 if os.path.isdir(file_path): | |
113 RemoveDirectory(file_path) | |
114 else: | |
115 RemoveFile(file_path) | |
116 | |
117 def MoveFile(path, new_path): | |
118 """Moves the file located at 'path' to 'new_path', if it exists.""" | |
119 try: | |
120 RemoveFile(new_path) | |
121 os.rename(path, new_path) | |
122 except OSError, e: | |
123 if e.errno != errno.ENOENT: | |
124 raise | |
125 | |
126 def RemoveFile(*path): | |
127 """Removes the file located at 'path', if it exists.""" | |
128 file_path = os.path.join(*path) | |
129 try: | |
130 os.remove(file_path) | |
131 except OSError, e: | |
132 if e.errno != errno.ENOENT: | |
133 raise | |
134 | |
135 def CheckDepotToolsInPath(): | |
136 delimiter = ';' if sys.platform.startswith('win') else ':' | |
137 path_list = os.environ['PATH'].split(delimiter) | |
138 for path in path_list: | |
139 if path.rstrip(os.path.sep).endswith('depot_tools'): | |
140 return path | |
141 return None | |
142 | |
143 def RunGsutilCommand(args): | |
144 gsutil_path = CheckDepotToolsInPath() | |
145 if gsutil_path is None: | |
146 print ('Follow the instructions in this document ' | |
147 'http://dev.chromium.org/developers/how-tos/install-depot-tools' | |
148 ' to install depot_tools and then try again.') | |
149 sys.exit(1) | |
150 gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil') | |
151 gsutil = subprocess.Popen([sys.executable, gsutil_path] + args, | |
152 stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
153 env=None) | |
154 stdout, stderr = gsutil.communicate() | |
155 if gsutil.returncode: | |
156 if (re.findall(r'status[ |=]40[1|3]', stderr) or | |
157 stderr.startswith(CREDENTIAL_ERROR_MESSAGE)): | |
158 print ('Follow these steps to configure your credentials and try' | |
159 ' running the bisect-builds.py again.:\n' | |
160 ' 1. Run "python %s config" and follow its instructions.\n' | |
161 ' 2. If you have a @google.com account, use that account.\n' | |
162 ' 3. For the project-id, just enter 0.' % gsutil_path) | |
163 sys.exit(1) | |
164 else: | |
165 raise Exception('Error running the gsutil command: %s' % stderr) | |
166 return stdout | |
167 | |
168 def GSutilList(bucket): | |
169 query = '%s/' %(bucket) | |
170 stdout = RunGsutilCommand(['ls', query]) | |
171 return [url[len(query):].strip('/') for url in stdout.splitlines()] | |
172 | |
173 def GSUtilDownloadFile(src, dst): | |
174 command = ['cp', src, dst] | |
175 return RunGsutilCommand(command) | |
176 | |
177 def GSUtilCopy(source, dest): | |
178 if not source.startswith('gs://') and not source.startswith('file://'): | |
179 source = 'file://' + source | |
180 if not dest.startswith('gs://') and not dest.startswith('file://'): | |
181 dest = 'file://' + dest | |
182 command = ['cp'] | |
183 command.extend([source, dest]) | |
184 return RunGsutilCommand(command) | |
185 | |
186 def RunCommand(cmd, cwd=None): | |
187 """Runs the given command and returns the exit code. | |
188 | |
189 Args: | |
190 cmd: list of command arguments. | |
191 cwd: working directory to execute the command, or None if the current | |
192 working directory should be used. | |
193 | |
194 Returns: | |
195 The exit code of the command. | |
196 """ | |
197 process = subprocess.Popen(cmd, cwd=cwd) | |
198 process.wait() | |
199 return process.returncode | |
200 | |
201 | |
202 | |
203 def CopyFileToDir(src_path, dest_dir, dest_fn=None, link_ok=False): | |
204 """Copies the file found at src_path to the dest_dir directory, with metadata. | |
205 | |
206 If dest_fn is specified, the src_path is copied to that name in dest_dir, | |
207 otherwise it is copied to a file of the same name. | |
208 | |
209 Raises PathNotFound if either the file or the directory is not found. | |
210 """ | |
211 # Verify the file and directory separately so we can tell them apart and | |
212 # raise PathNotFound rather than shutil.copyfile's IOError. | |
213 if not os.path.isfile(src_path): | |
214 raise PathNotFound('Unable to find file %s' % src_path) | |
215 if not os.path.isdir(dest_dir): | |
216 raise PathNotFound('Unable to find dir %s' % dest_dir) | |
217 src_file = os.path.basename(src_path) | |
218 if dest_fn: | |
219 # If we have ctypes and the caller doesn't mind links, use that to | |
220 # try to make the copy faster on Windows. http://crbug.com/418702. | |
221 if link_ok and WIN_LINK_FUNC: | |
222 WIN_LINK_FUNC(src_path, os.path.join(dest_dir, dest_fn)) | |
223 else: | |
224 shutil.copy2(src_path, os.path.join(dest_dir, dest_fn)) | |
225 else: | |
226 shutil.copy2(src_path, os.path.join(dest_dir, src_file)) | |
227 | |
228 def RemoveDirectory(*path): | |
229 """Recursively removes a directory, even if it's marked read-only. | |
230 | |
231 Remove the directory located at *path, if it exists. | |
232 | |
233 shutil.rmtree() doesn't work on Windows if any of the files or directories | |
234 are read-only, which svn repositories and some .svn files are. We need to | |
235 be able to force the files to be writable (i.e., deletable) as we traverse | |
236 the tree. | |
237 | |
238 Even with all this, Windows still sometimes fails to delete a file, citing | |
239 a permission error (maybe something to do with antivirus scans or disk | |
240 indexing). The best suggestion any of the user forums had was to wait a | |
241 bit and try again, so we do that too. It's hand-waving, but sometimes it | |
242 works. :/ | |
243 """ | |
244 file_path = os.path.join(*path) | |
245 if not os.path.exists(file_path): | |
246 return | |
247 | |
248 if sys.platform == 'win32': | |
249 # Give up and use cmd.exe's rd command. | |
250 file_path = os.path.normcase(file_path) | |
251 for _ in xrange(3): | |
252 print 'RemoveDirectory running %s' % (' '.join( | |
253 ['cmd.exe', '/c', 'rd', '/q', '/s', file_path])) | |
254 if not subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', file_path]): | |
255 break | |
256 print ' Failed' | |
257 time.sleep(3) | |
258 return | |
259 | |
260 def RemoveWithRetry_non_win(rmfunc, path): | |
261 if os.path.islink(path): | |
262 return os.remove(path) | |
263 else: | |
264 return rmfunc(path) | |
265 | |
266 remove_with_retry = RemoveWithRetry_non_win | |
267 | |
268 def RmTreeOnError(function, path, excinfo): | |
269 r"""This works around a problem whereby python 2.x on Windows has no ability | |
270 to check for symbolic links. os.path.islink always returns False. But | |
271 shutil.rmtree will fail if invoked on a symbolic link whose target was | |
272 deleted before the link. E.g., reproduce like this: | |
273 > mkdir test | |
274 > mkdir test\1 | |
275 > mklink /D test\current test\1 | |
276 > python -c "import chromium_utils; chromium_utils.RemoveDirectory('test')" | |
277 To avoid this issue, we pass this error-handling function to rmtree. If | |
278 we see the exact sort of failure, we ignore it. All other failures we re- | |
279 raise. | |
280 """ | |
281 | |
282 exception_type = excinfo[0] | |
283 exception_value = excinfo[1] | |
284 # If shutil.rmtree encounters a symbolic link on Windows, os.listdir will | |
285 # fail with a WindowsError exception with an ENOENT errno (i.e., file not | |
286 # found). We'll ignore that error. Note that WindowsError is not defined | |
287 # for non-Windows platforms, so we use OSError (of which it is a subclass) | |
288 # to avoid lint complaints about an undefined global on non-Windows | |
289 # platforms. | |
290 if (function is os.listdir) and issubclass(exception_type, OSError): | |
291 if exception_value.errno == errno.ENOENT: | |
292 # File does not exist, and we're trying to delete, so we can ignore the | |
293 # failure. | |
294 print 'WARNING: Failed to list %s during rmtree. Ignoring.\n' % path | |
295 else: | |
296 raise | |
297 else: | |
298 raise | |
299 | |
300 for root, dirs, files in os.walk(file_path, topdown=False): | |
301 # For POSIX: making the directory writable guarantees removability. | |
302 # Windows will ignore the non-read-only bits in the chmod value. | |
303 os.chmod(root, 0770) | |
304 for name in files: | |
305 remove_with_retry(os.remove, os.path.join(root, name)) | |
306 for name in dirs: | |
307 remove_with_retry(lambda p: shutil.rmtree(p, onerror=RmTreeOnError), | |
308 os.path.join(root, name)) | |
309 | |
310 remove_with_retry(os.rmdir, file_path) | |
311 | |
312 | |
313 | |
314 def MakeZip(output_dir, archive_name, file_list, file_relative_dir, | |
315 raise_error=True, remove_archive_directory=True, strip_files=None): | |
316 """Packs files into a new zip archive. | |
317 | |
318 Files are first copied into a directory within the output_dir named for | |
319 the archive_name, which will be created if necessary and emptied if it | |
320 already exists. The files are then then packed using archive names | |
321 relative to the output_dir. That is, if the zipfile is unpacked in place, | |
322 it will create a directory identical to the new archive_name directory, in | |
323 the output_dir. The zip file will be named as the archive_name, plus | |
324 '.zip'. | |
325 | |
326 Args: | |
327 output_dir: Absolute path to the directory in which the archive is to | |
328 be created. | |
329 archive_dir: Subdirectory of output_dir holding files to be added to | |
330 the new zipfile. | |
331 file_list: List of paths to files or subdirectories, relative to the | |
332 file_relative_dir. | |
333 file_relative_dir: Absolute path to the directory containing the files | |
334 and subdirectories in the file_list. | |
335 raise_error: Whether to raise a PathNotFound error if one of the files in | |
336 the list is not found. | |
337 remove_archive_directory: Whether to remove the archive staging directory | |
338 before copying files over to it. | |
339 strip_files: List of executable files to strip symbols when zipping | |
340 | |
341 Returns: | |
342 A tuple consisting of (archive_dir, zip_file_path), where archive_dir | |
343 is the full path to the newly created archive_name subdirectory. | |
344 | |
345 Raises: | |
346 PathNotFound if any of the files in the list is not found, unless | |
347 raise_error is False, in which case the error will be ignored. | |
348 """ | |
349 | |
350 start_time = time.clock() | |
351 # Collect files into the archive directory. | |
352 archive_dir = os.path.join(output_dir, archive_name) | |
353 print 'output_dir: %s, archive_name: %s' % (output_dir, archive_name) | |
354 print 'archive_dir: %s, remove_archive_directory: %s, exists: %s' % ( | |
355 archive_dir, remove_archive_directory, os.path.exists(archive_dir)) | |
356 if remove_archive_directory and os.path.exists(archive_dir): | |
357 # Move it even if it's not a directory as expected. This can happen with | |
358 # FILES.cfg archive creation where we create an archive staging directory | |
359 # that is the same name as the ultimate archive name. | |
360 if not os.path.isdir(archive_dir): | |
361 print 'Moving old "%s" file to create same name directory.' % archive_dir | |
362 previous_archive_file = '%s.old' % archive_dir | |
363 MoveFile(archive_dir, previous_archive_file) | |
364 else: | |
365 print 'Removing %s' % archive_dir | |
366 RemoveDirectory(archive_dir) | |
367 print 'Now, os.path.exists(%s): %s' % ( | |
368 archive_dir, os.path.exists(archive_dir)) | |
369 MaybeMakeDirectory(archive_dir) | |
370 for needed_file in file_list: | |
371 needed_file = needed_file.rstrip() | |
372 # These paths are relative to the file_relative_dir. We need to copy | |
373 # them over maintaining the relative directories, where applicable. | |
374 src_path = os.path.join(file_relative_dir, needed_file) | |
375 dirname, basename = os.path.split(needed_file) | |
376 try: | |
377 if os.path.isdir(src_path): | |
378 if WIN_LINK_FUNC: | |
379 WIN_LINK_FUNC(src_path, os.path.join(archive_dir, needed_file)) | |
380 else: | |
381 shutil.copytree(src_path, os.path.join(archive_dir, needed_file), | |
382 symlinks=True) | |
383 elif dirname != '' and basename != '': | |
384 dest_dir = os.path.join(archive_dir, dirname) | |
385 MaybeMakeDirectory(dest_dir) | |
386 CopyFileToDir(src_path, dest_dir, basename, link_ok=True) | |
387 if strip_files and basename in strip_files: | |
388 cmd = ['strip', os.path.join(dest_dir, basename)] | |
389 RunCommand(cmd) | |
390 else: | |
391 CopyFileToDir(src_path, archive_dir, basename, link_ok=True) | |
392 if strip_files and basename in strip_files: | |
393 cmd = ['strip', os.path.join(archive_dir, basename)] | |
394 RunCommand(cmd) | |
395 except PathNotFound: | |
396 if raise_error: | |
397 raise | |
398 end_time = time.clock() | |
399 print 'Took %f seconds to create archive directory.' % (end_time - start_time) | |
400 | |
401 # Pack the zip file. | |
402 output_file = '%s.zip' % archive_dir | |
403 previous_file = '%s_old.zip' % archive_dir | |
404 MoveFile(output_file, previous_file) | |
405 | |
406 # If we have 7z, use that as it's much faster. See http://crbug.com/418702. | |
407 windows_zip_cmd = None | |
408 if os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'): | |
409 windows_zip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'a', '-y', '-mx1'] | |
410 | |
411 # On Windows we use the python zip module; on Linux and Mac, we use the zip | |
412 # command as it will handle links and file bits (executable). Which is much | |
413 # easier then trying to do that with ZipInfo options. | |
414 start_time = time.clock() | |
415 if IsWindows() and not windows_zip_cmd: | |
416 print 'Creating %s' % output_file | |
417 | |
418 def _Addfiles(to_zip_file, dirname, files_to_add): | |
419 for this_file in files_to_add: | |
420 archive_name = this_file | |
421 this_path = os.path.join(dirname, this_file) | |
422 if os.path.isfile(this_path): | |
423 # Store files named relative to the outer output_dir. | |
424 archive_name = this_path.replace(output_dir + os.sep, '') | |
425 if os.path.getsize(this_path) == 0: | |
426 compress_method = zipfile.ZIP_STORED | |
427 else: | |
428 compress_method = zipfile.ZIP_DEFLATED | |
429 to_zip_file.write(this_path, archive_name, compress_method) | |
430 print 'Adding %s' % archive_name | |
431 zip_file = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED, | |
432 allowZip64=True) | |
433 try: | |
434 os.path.walk(archive_dir, _Addfiles, zip_file) | |
435 finally: | |
436 zip_file.close() | |
437 else: | |
438 if IsMac() or IsLinux(): | |
439 zip_cmd = ['zip', '-yr1'] | |
440 else: | |
441 zip_cmd = windows_zip_cmd | |
442 saved_dir = os.getcwd() | |
443 os.chdir(os.path.dirname(archive_dir)) | |
444 command = zip_cmd + [output_file, os.path.basename(archive_dir)] | |
445 result = RunCommand(command) | |
446 os.chdir(saved_dir) | |
447 if result and raise_error: | |
448 raise ExternalError('zip failed: %s => %s' % | |
449 (str(command), result)) | |
450 end_time = time.clock() | |
451 print 'Took %f seconds to create zip.' % (end_time - start_time) | |
452 return (archive_dir, output_file) | |
453 | |
454 | |
455 def ExtractZip(filename, output_dir, verbose=True): | |
456 """ Extract the zip archive in the output directory. | |
457 """ | |
458 MaybeMakeDirectory(output_dir) | |
459 | |
460 # On Linux and Mac, we use the unzip command as it will | |
461 # handle links and file bits (executable), which is much | |
462 # easier then trying to do that with ZipInfo options. | |
463 # | |
464 # The Mac Version of unzip unfortunately does not support Zip64, whereas | |
465 # the python module does, so we have to fallback to the python zip module | |
466 # on Mac if the filesize is greater than 4GB. | |
467 # | |
468 # On Windows, try to use 7z if it is installed, otherwise fall back to python | |
469 # zip module and pray we don't have files larger than 512MB to unzip. | |
470 unzip_cmd = None | |
471 if ((IsMac() and os.path.getsize(filename) < 4 * 1024 * 1024 * 1024) | |
472 or IsLinux()): | |
473 unzip_cmd = ['unzip', '-o'] | |
474 elif IsWindows() and os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'): | |
475 unzip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y'] | |
476 | |
477 if unzip_cmd: | |
478 # Make sure path is absolute before changing directories. | |
479 filepath = os.path.abspath(filename) | |
480 saved_dir = os.getcwd() | |
481 os.chdir(output_dir) | |
482 command = unzip_cmd + [filepath] | |
483 result = RunCommand(command) | |
484 os.chdir(saved_dir) | |
485 if result: | |
486 raise ExternalError('unzip failed: %s => %s' % (str(command), result)) | |
487 else: | |
488 assert IsWindows() or IsMac() | |
489 zf = zipfile.ZipFile(filename) | |
490 # TODO(hinoka): This can be multiprocessed. | |
491 for name in zf.namelist(): | |
492 if verbose: | |
493 print 'Extracting %s' % name | |
494 zf.extract(name, output_dir) | |
495 if IsMac(): | |
496 # Restore permission bits. | |
497 os.chmod(os.path.join(output_dir, name), | |
498 zf.getinfo(name).external_attr >> 16L) | |
OLD | NEW |