| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. | |
| 3 # | |
| 4 # Use of this source code is governed by a BSD-style license | |
| 5 # that can be found in the LICENSE file in the root of the source | |
| 6 # tree. An additional intellectual property rights grant can be found | |
| 7 # in the file PATENTS. All contributing project authors may | |
| 8 # be found in the AUTHORS file in the root of the source tree. | |
| 9 | |
| 10 """Setup links to a Chromium checkout for WebRTC. | |
| 11 | |
| 12 WebRTC standalone shares a lot of dependencies and build tools with Chromium. | |
| 13 To do this, many of the paths of a Chromium checkout is emulated by creating | |
| 14 symlinks to files and directories. This script handles the setup of symlinks to | |
| 15 achieve this. | |
| 16 """ | |
| 17 | |
| 18 | |
| 19 import ctypes | |
| 20 import errno | |
| 21 import logging | |
| 22 import optparse | |
| 23 import os | |
| 24 import shelve | |
| 25 import shutil | |
| 26 import subprocess | |
| 27 import sys | |
| 28 import textwrap | |
| 29 | |
| 30 | |
| 31 DIRECTORIES = [ | |
| 32 'base', | |
| 33 'build', | |
| 34 'buildtools', | |
| 35 'testing', | |
| 36 'third_party/afl', | |
| 37 'third_party/binutils', | |
| 38 'third_party/boringssl', | |
| 39 'third_party/catapult', | |
| 40 'third_party/closure_compiler', | |
| 41 'third_party/colorama', | |
| 42 'third_party/expat', | |
| 43 'third_party/ffmpeg', | |
| 44 'third_party/instrumented_libraries', | |
| 45 'third_party/jsoncpp', | |
| 46 'third_party/libFuzzer', | |
| 47 'third_party/libjpeg', | |
| 48 'third_party/libjpeg_turbo', | |
| 49 'third_party/libsrtp', | |
| 50 'third_party/libvpx', | |
| 51 'third_party/libyuv', | |
| 52 'third_party/lss', | |
| 53 'third_party/ocmock', | |
| 54 'third_party/openh264', | |
| 55 'third_party/openmax_dl', | |
| 56 'third_party/opus', | |
| 57 'third_party/proguard', | |
| 58 'third_party/protobuf', | |
| 59 'third_party/sqlite', | |
| 60 'third_party/usrsctp', | |
| 61 'third_party/yasm', | |
| 62 'third_party/zlib', | |
| 63 'tools/clang', | |
| 64 'tools/clang_format_merge_driver', | |
| 65 'tools/determinism', | |
| 66 'tools/generate_library_loader', | |
| 67 'tools/generate_stubs', | |
| 68 'tools/gn', | |
| 69 'tools/grit', | |
| 70 'tools/gyp', | |
| 71 'tools/luci-go', | |
| 72 'tools/memory', | |
| 73 'tools/protoc_wrapper', | |
| 74 'tools/python', | |
| 75 'tools/swarming_client', | |
| 76 'tools/valgrind', | |
| 77 'tools/vim', | |
| 78 'tools/win', | |
| 79 ] | |
| 80 | |
| 81 from sync_chromium import get_target_os_list | |
| 82 target_os = get_target_os_list() | |
| 83 if 'android' in target_os: | |
| 84 DIRECTORIES += [ | |
| 85 'third_party/accessibility_test_framework', | |
| 86 'third_party/android_platform', | |
| 87 'third_party/android_support_test_runner', | |
| 88 'third_party/android_tools', | |
| 89 'third_party/apache_velocity', | |
| 90 'third_party/ashmem', | |
| 91 'third_party/bouncycastle', | |
| 92 'third_party/byte_buddy', | |
| 93 'third_party/ced', | |
| 94 'third_party/espresso', | |
| 95 'third_party/guava', | |
| 96 'third_party/hamcrest', | |
| 97 'third_party/icu', | |
| 98 'third_party/icu4j', | |
| 99 'third_party/ijar', | |
| 100 'third_party/intellij', | |
| 101 'third_party/javax_inject', | |
| 102 'third_party/jsr-305', | |
| 103 'third_party/junit', | |
| 104 'third_party/libxml', | |
| 105 'third_party/mockito', | |
| 106 'third_party/modp_b64', | |
| 107 'third_party/objenesis', | |
| 108 'third_party/ow2_asm', | |
| 109 'third_party/requests', | |
| 110 'third_party/robolectric', | |
| 111 'third_party/sqlite4java', | |
| 112 'third_party/tcmalloc', | |
| 113 'tools/android', | |
| 114 ] | |
| 115 | |
| 116 FILES = { | |
| 117 'third_party/BUILD.gn': None, | |
| 118 } | |
| 119 | |
| 120 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| 121 CHROMIUM_CHECKOUT = os.path.join('chromium', 'src') | |
| 122 LINKS_DB = 'links' | |
| 123 | |
| 124 # Version management to make future upgrades/downgrades easier to support. | |
| 125 SCHEMA_VERSION = 1 | |
| 126 | |
| 127 | |
| 128 def query_yes_no(question, default=False): | |
| 129 """Ask a yes/no question via raw_input() and return their answer. | |
| 130 | |
| 131 Modified from http://stackoverflow.com/a/3041990. | |
| 132 """ | |
| 133 prompt = " [%s/%%s]: " | |
| 134 prompt = prompt % ('Y' if default is True else 'y') | |
| 135 prompt = prompt % ('N' if default is False else 'n') | |
| 136 | |
| 137 if default is None: | |
| 138 default = 'INVALID' | |
| 139 | |
| 140 while True: | |
| 141 sys.stdout.write(question + prompt) | |
| 142 choice = raw_input().lower() | |
| 143 if choice == '' and default != 'INVALID': | |
| 144 return default | |
| 145 | |
| 146 if 'yes'.startswith(choice): | |
| 147 return True | |
| 148 elif 'no'.startswith(choice): | |
| 149 return False | |
| 150 | |
| 151 print "Please respond with 'yes' or 'no' (or 'y' or 'n')." | |
| 152 | |
| 153 | |
| 154 # Actions | |
| 155 class Action(object): | |
| 156 def __init__(self, dangerous): | |
| 157 self.dangerous = dangerous | |
| 158 | |
| 159 def announce(self, planning): | |
| 160 """Log a description of this action. | |
| 161 | |
| 162 Args: | |
| 163 planning - True iff we're in the planning stage, False if we're in the | |
| 164 doit stage. | |
| 165 """ | |
| 166 pass | |
| 167 | |
| 168 def doit(self, links_db): | |
| 169 """Execute the action, recording what we did to links_db, if necessary.""" | |
| 170 pass | |
| 171 | |
| 172 | |
| 173 class Remove(Action): | |
| 174 def __init__(self, path, dangerous): | |
| 175 super(Remove, self).__init__(dangerous) | |
| 176 self._priority = 0 | |
| 177 self._path = path | |
| 178 | |
| 179 def announce(self, planning): | |
| 180 log = logging.warn | |
| 181 filesystem_type = 'file' | |
| 182 if not self.dangerous: | |
| 183 log = logging.info | |
| 184 filesystem_type = 'link' | |
| 185 if planning: | |
| 186 log('Planning to remove %s: %s', filesystem_type, self._path) | |
| 187 else: | |
| 188 log('Removing %s: %s', filesystem_type, self._path) | |
| 189 | |
| 190 def doit(self, _): | |
| 191 os.remove(self._path) | |
| 192 | |
| 193 | |
| 194 class Rmtree(Action): | |
| 195 def __init__(self, path): | |
| 196 super(Rmtree, self).__init__(dangerous=True) | |
| 197 self._priority = 0 | |
| 198 self._path = path | |
| 199 | |
| 200 def announce(self, planning): | |
| 201 if planning: | |
| 202 logging.warn('Planning to remove directory: %s', self._path) | |
| 203 else: | |
| 204 logging.warn('Removing directory: %s', self._path) | |
| 205 | |
| 206 def doit(self, _): | |
| 207 if sys.platform.startswith('win'): | |
| 208 # shutil.rmtree() doesn't work on Windows if any of the directories are | |
| 209 # read-only, which svn repositories are. | |
| 210 subprocess.check_call(['rd', '/q', '/s', self._path], shell=True) | |
| 211 else: | |
| 212 shutil.rmtree(self._path) | |
| 213 | |
| 214 | |
| 215 class Makedirs(Action): | |
| 216 def __init__(self, path): | |
| 217 super(Makedirs, self).__init__(dangerous=False) | |
| 218 self._priority = 1 | |
| 219 self._path = path | |
| 220 | |
| 221 def doit(self, _): | |
| 222 try: | |
| 223 os.makedirs(self._path) | |
| 224 except OSError as e: | |
| 225 if e.errno != errno.EEXIST: | |
| 226 raise | |
| 227 | |
| 228 | |
| 229 class Symlink(Action): | |
| 230 def __init__(self, source_path, link_path): | |
| 231 super(Symlink, self).__init__(dangerous=False) | |
| 232 self._priority = 2 | |
| 233 self._source_path = source_path | |
| 234 self._link_path = link_path | |
| 235 | |
| 236 def announce(self, planning): | |
| 237 if planning: | |
| 238 logging.info( | |
| 239 'Planning to create link from %s to %s', self._link_path, | |
| 240 self._source_path) | |
| 241 else: | |
| 242 logging.debug( | |
| 243 'Linking from %s to %s', self._link_path, self._source_path) | |
| 244 | |
| 245 def doit(self, links_db): | |
| 246 # Files not in the root directory need relative path calculation. | |
| 247 # On Windows, use absolute paths instead since NTFS doesn't seem to support | |
| 248 # relative paths for symlinks. | |
| 249 if sys.platform.startswith('win'): | |
| 250 source_path = os.path.abspath(self._source_path) | |
| 251 else: | |
| 252 if os.path.dirname(self._link_path) != self._link_path: | |
| 253 source_path = os.path.relpath(self._source_path, | |
| 254 os.path.dirname(self._link_path)) | |
| 255 | |
| 256 os.symlink(source_path, os.path.abspath(self._link_path)) | |
| 257 links_db[self._source_path] = self._link_path | |
| 258 | |
| 259 | |
| 260 class LinkError(IOError): | |
| 261 """Failed to create a link.""" | |
| 262 pass | |
| 263 | |
| 264 | |
| 265 # Use junctions instead of symlinks on the Windows platform. | |
| 266 if sys.platform.startswith('win'): | |
| 267 def symlink(source_path, link_path): | |
| 268 if os.path.isdir(source_path): | |
| 269 subprocess.check_call(['cmd.exe', '/c', 'mklink', '/J', link_path, | |
| 270 source_path]) | |
| 271 else: | |
| 272 # Don't create symlinks to files on Windows, just copy the file instead | |
| 273 # (there's no way to create a link without administrator's privileges). | |
| 274 shutil.copy(source_path, link_path) | |
| 275 os.symlink = symlink | |
| 276 | |
| 277 | |
| 278 class WebRTCLinkSetup(object): | |
| 279 def __init__(self, links_db, force=False, dry_run=False, prompt=False): | |
| 280 self._force = force | |
| 281 self._dry_run = dry_run | |
| 282 self._prompt = prompt | |
| 283 self._links_db = links_db | |
| 284 | |
| 285 def CreateLinks(self, on_bot): | |
| 286 logging.debug('CreateLinks') | |
| 287 # First, make a plan of action | |
| 288 actions = [] | |
| 289 | |
| 290 for source_path, link_path in FILES.iteritems(): | |
| 291 actions += self._ActionForPath( | |
| 292 source_path, link_path, check_fn=os.path.isfile, check_msg='files') | |
| 293 for source_dir in DIRECTORIES: | |
| 294 actions += self._ActionForPath( | |
| 295 source_dir, None, check_fn=os.path.isdir, | |
| 296 check_msg='directories') | |
| 297 | |
| 298 if not on_bot and self._force: | |
| 299 # When making the manual switch from legacy SVN checkouts to the new | |
| 300 # Git-based Chromium DEPS, the .gclient_entries file that contains cached | |
| 301 # URLs for all DEPS entries must be removed to avoid future sync problems. | |
| 302 entries_file = os.path.join(os.path.dirname(ROOT_DIR), '.gclient_entries') | |
| 303 if os.path.exists(entries_file): | |
| 304 actions.append(Remove(entries_file, dangerous=True)) | |
| 305 | |
| 306 actions.sort() | |
| 307 | |
| 308 if self._dry_run: | |
| 309 for action in actions: | |
| 310 action.announce(planning=True) | |
| 311 logging.info('Not doing anything because dry-run was specified.') | |
| 312 sys.exit(0) | |
| 313 | |
| 314 if any(a.dangerous for a in actions): | |
| 315 logging.warn('Dangerous actions:') | |
| 316 for action in (a for a in actions if a.dangerous): | |
| 317 action.announce(planning=True) | |
| 318 print | |
| 319 | |
| 320 if not self._force: | |
| 321 logging.error(textwrap.dedent("""\ | |
| 322 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
| 323 A C T I O N R E Q I R E D | |
| 324 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
| 325 | |
| 326 Setting up the checkout requires creating symlinks to directories in the | |
| 327 Chromium checkout inside chromium/src. | |
| 328 To avoid disrupting developers, we've chosen to not delete directories | |
| 329 forcibly, in case you have some work in progress in one of them :) | |
| 330 | |
| 331 ACTION REQUIRED: | |
| 332 Before running `gclient sync|runhooks` again, you must run: | |
| 333 %s%s --force | |
| 334 | |
| 335 Which will replace all directories which now must be symlinks, after | |
| 336 prompting with a summary of the work-to-be-done. | |
| 337 """), 'python ' if sys.platform.startswith('win') else '', __file__) | |
| 338 sys.exit(1) | |
| 339 elif self._prompt: | |
| 340 if not query_yes_no('Would you like to perform the above plan?'): | |
| 341 sys.exit(1) | |
| 342 | |
| 343 for action in actions: | |
| 344 action.announce(planning=False) | |
| 345 action.doit(self._links_db) | |
| 346 | |
| 347 if not on_bot and self._force: | |
| 348 logging.info('Completed!\n\nNow run `gclient sync|runhooks` again to ' | |
| 349 'let the remaining hooks (that probably were interrupted) ' | |
| 350 'execute.') | |
| 351 | |
| 352 def CleanupLinks(self): | |
| 353 logging.debug('CleanupLinks') | |
| 354 for source, link_path in self._links_db.iteritems(): | |
| 355 if source == 'SCHEMA_VERSION': | |
| 356 continue | |
| 357 if os.path.islink(link_path) or sys.platform.startswith('win'): | |
| 358 # os.path.islink() always returns false on Windows | |
| 359 # See http://bugs.python.org/issue13143. | |
| 360 logging.debug('Removing link to %s at %s', source, link_path) | |
| 361 if not self._dry_run: | |
| 362 if os.path.exists(link_path): | |
| 363 if sys.platform.startswith('win') and os.path.isdir(link_path): | |
| 364 subprocess.check_call(['rmdir', '/q', '/s', link_path], | |
| 365 shell=True) | |
| 366 else: | |
| 367 os.remove(link_path) | |
| 368 del self._links_db[source] | |
| 369 | |
| 370 @staticmethod | |
| 371 def _ActionForPath(source_path, link_path=None, check_fn=None, | |
| 372 check_msg=None): | |
| 373 """Create zero or more Actions to link to a file or directory. | |
| 374 | |
| 375 This will be a symlink on POSIX platforms. On Windows it will result in: | |
| 376 * a junction for directories | |
| 377 * a copied file for single files. | |
| 378 | |
| 379 Args: | |
| 380 source_path: Path relative to the Chromium checkout root. | |
| 381 For readability, the path may contain slashes, which will | |
| 382 automatically be converted to the right path delimiter on Windows. | |
| 383 link_path: The location for the link to create. If omitted it will be the | |
| 384 same path as source_path. | |
| 385 check_fn: A function returning true if the type of filesystem object is | |
| 386 correct for the attempted call. Otherwise an error message with | |
| 387 check_msg will be printed. | |
| 388 check_msg: String used to inform the user of an invalid attempt to create | |
| 389 a file. | |
| 390 Returns: | |
| 391 A list of Action objects. | |
| 392 """ | |
| 393 def fix_separators(path): | |
| 394 if sys.platform.startswith('win'): | |
| 395 return path.replace(os.altsep, os.sep) | |
| 396 else: | |
| 397 return path | |
| 398 | |
| 399 assert check_fn | |
| 400 assert check_msg | |
| 401 link_path = link_path or source_path | |
| 402 link_path = fix_separators(link_path) | |
| 403 | |
| 404 source_path = fix_separators(source_path) | |
| 405 source_path = os.path.join(CHROMIUM_CHECKOUT, source_path) | |
| 406 if os.path.exists(source_path) and not check_fn: | |
| 407 raise LinkError('Can only to link to %s: tried to link to: %s' % | |
| 408 (check_msg, source_path)) | |
| 409 | |
| 410 if not os.path.exists(source_path): | |
| 411 logging.debug('Silently ignoring missing source: %s. This is to avoid ' | |
| 412 'errors on platform-specific dependencies.', source_path) | |
| 413 return [] | |
| 414 | |
| 415 actions = [] | |
| 416 | |
| 417 if os.path.exists(link_path) or os.path.islink(link_path): | |
| 418 if os.path.islink(link_path): | |
| 419 actions.append(Remove(link_path, dangerous=False)) | |
| 420 elif os.path.isfile(link_path): | |
| 421 actions.append(Remove(link_path, dangerous=True)) | |
| 422 elif os.path.isdir(link_path): | |
| 423 actions.append(Rmtree(link_path)) | |
| 424 else: | |
| 425 raise LinkError('Don\'t know how to plan: %s' % link_path) | |
| 426 | |
| 427 # Create parent directories to the target link if needed. | |
| 428 target_parent_dirs = os.path.dirname(link_path) | |
| 429 if (target_parent_dirs and | |
| 430 target_parent_dirs != link_path and | |
| 431 not os.path.exists(target_parent_dirs)): | |
| 432 actions.append(Makedirs(target_parent_dirs)) | |
| 433 | |
| 434 actions.append(Symlink(source_path, link_path)) | |
| 435 | |
| 436 return actions | |
| 437 | |
| 438 def _initialize_database(filename): | |
| 439 links_database = shelve.open(filename) | |
| 440 | |
| 441 # Wipe the database if this version of the script ends up looking at a | |
| 442 # newer (future) version of the links db, just to be sure. | |
| 443 version = links_database.get('SCHEMA_VERSION') | |
| 444 if version and version != SCHEMA_VERSION: | |
| 445 logging.info('Found database with schema version %s while this script only ' | |
| 446 'supports %s. Wiping previous database contents.', version, | |
| 447 SCHEMA_VERSION) | |
| 448 links_database.clear() | |
| 449 links_database['SCHEMA_VERSION'] = SCHEMA_VERSION | |
| 450 return links_database | |
| 451 | |
| 452 | |
| 453 def main(): | |
| 454 on_bot = os.environ.get('CHROME_HEADLESS') == '1' | |
| 455 | |
| 456 parser = optparse.OptionParser() | |
| 457 parser.add_option('-d', '--dry-run', action='store_true', default=False, | |
| 458 help='Print what would be done, but don\'t perform any ' | |
| 459 'operations. This will automatically set logging to ' | |
| 460 'verbose.') | |
| 461 parser.add_option('-c', '--clean-only', action='store_true', default=False, | |
| 462 help='Only clean previously created links, don\'t create ' | |
| 463 'new ones. This will automatically set logging to ' | |
| 464 'verbose.') | |
| 465 parser.add_option('-f', '--force', action='store_true', default=on_bot, | |
| 466 help='Force link creation. CAUTION: This deletes existing ' | |
| 467 'folders and files in the locations where links are ' | |
| 468 'about to be created.') | |
| 469 parser.add_option('-n', '--no-prompt', action='store_false', dest='prompt', | |
| 470 default=(not on_bot), | |
| 471 help='Prompt if we\'re planning to do a dangerous action') | |
| 472 parser.add_option('-v', '--verbose', action='store_const', | |
| 473 const=logging.DEBUG, default=logging.INFO, | |
| 474 help='Print verbose output for debugging.') | |
| 475 options, _ = parser.parse_args() | |
| 476 | |
| 477 if options.dry_run or options.force or options.clean_only: | |
| 478 options.verbose = logging.DEBUG | |
| 479 logging.basicConfig(format='%(message)s', level=options.verbose) | |
| 480 | |
| 481 # Work from the root directory of the checkout. | |
| 482 script_dir = os.path.dirname(os.path.abspath(__file__)) | |
| 483 os.chdir(script_dir) | |
| 484 | |
| 485 if sys.platform.startswith('win'): | |
| 486 def is_admin(): | |
| 487 try: | |
| 488 return os.getuid() == 0 | |
| 489 except AttributeError: | |
| 490 return ctypes.windll.shell32.IsUserAnAdmin() != 0 | |
| 491 if is_admin(): | |
| 492 logging.warning('WARNING: On Windows, you no longer need run as ' | |
| 493 'administrator. Please run with user account privileges.') | |
| 494 | |
| 495 if not os.path.exists(CHROMIUM_CHECKOUT): | |
| 496 logging.warning('Cannot find a Chromium checkout at %s. Did you run ' | |
| 497 '"gclient sync" before running this script?', | |
| 498 CHROMIUM_CHECKOUT) | |
| 499 return 0 | |
| 500 | |
| 501 links_database = _initialize_database(LINKS_DB) | |
| 502 try: | |
| 503 symlink_creator = WebRTCLinkSetup(links_database, options.force, | |
| 504 options.dry_run, options.prompt) | |
| 505 symlink_creator.CleanupLinks() | |
| 506 if not options.clean_only: | |
| 507 symlink_creator.CreateLinks(on_bot) | |
| 508 except LinkError as e: | |
| 509 print >> sys.stderr, e.message | |
| 510 return 3 | |
| 511 finally: | |
| 512 links_database.close() | |
| 513 return 0 | |
| 514 | |
| 515 | |
| 516 if __name__ == '__main__': | |
| 517 sys.exit(main()) | |
| OLD | NEW |