Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(288)

Side by Side Diff: tools/bisect_repackage/bisect_repackage_utils.py

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

Powered by Google App Engine
This is Rietveld 408576698