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

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

Powered by Google App Engine
This is Rietveld 408576698