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

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

Powered by Google App Engine
This is Rietveld 408576698