blob: dc930644a98502066dbe649980a8ea91e9c5e58c [file] [log] [blame]
kjellander@webrtc.org89256622014-08-20 12:10:11 +00001#!/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
12WebRTC standalone shares a lot of dependencies and build tools with Chromium.
13To do this, many of the paths of a Chromium checkout is emulated by creating
14symlinks to files and directories. This script handles the setup of symlinks to
15achieve this.
16
17It also handles cleanup of the legacy Subversion-based approach that was used
18before Chrome switched over their master repo from Subversion to Git.
19"""
20
21
22import ctypes
23import errno
24import logging
25import optparse
26import os
27import shelve
28import shutil
29import subprocess
30import sys
31import textwrap
32
33
34DIRECTORIES = [
35 'build',
36 'buildtools',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000037 'testing',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000038 'third_party/binutils',
39 'third_party/boringssl',
40 'third_party/colorama',
41 'third_party/drmemory',
42 'third_party/expat',
hbosa9a1d2a2016-01-11 10:19:02 -080043 'third_party/ffmpeg',
kjellander@webrtc.org4e4fe4f2014-10-01 08:03:19 +000044 'third_party/instrumented_libraries',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000045 'third_party/jsoncpp',
Henrik Kjellander26ab91b2015-11-26 10:26:32 +010046 'third_party/libc++-static',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000047 'third_party/libjpeg',
48 'third_party/libjpeg_turbo',
49 'third_party/libsrtp',
kjellander@webrtc.org7d4e6d02014-11-27 10:41:04 +000050 'third_party/libudev',
kjellanderd6024e32015-09-28 21:16:48 -070051 'third_party/libvpx_new',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000052 'third_party/libyuv',
53 'third_party/llvm-build',
Patrik Höglundc92c23d2015-08-31 11:30:14 +020054 'third_party/lss',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000055 'third_party/nss',
tkchin@webrtc.org3a63a3c2015-01-06 07:21:34 +000056 'third_party/ocmock',
hbosa9a1d2a2016-01-11 10:19:02 -080057 'third_party/openh264',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000058 'third_party/openmax_dl',
59 'third_party/opus',
Patrik Höglundc92c23d2015-08-31 11:30:14 +020060 'third_party/proguard',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000061 'third_party/protobuf',
62 'third_party/sqlite',
63 'third_party/syzygy',
64 'third_party/usrsctp',
65 'third_party/yasm',
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000066 'third_party/zlib',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000067 'tools/clang',
68 'tools/generate_library_loader',
hbos5602f652016-01-15 01:38:34 -080069 'tools/generate_stubs',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000070 'tools/gn',
71 'tools/gyp',
72 'tools/memory',
73 'tools/protoc_wrapper',
74 'tools/python',
75 'tools/swarming_client',
76 'tools/valgrind',
Andrew MacDonald65de7d22015-05-19 11:37:34 -070077 'tools/vim',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000078 'tools/win',
Henrik Kjellander9589e2a2015-10-22 06:48:21 +020079 'tools/xdisplaycheck',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000080]
81
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000082from sync_chromium import get_target_os_list
Henrik Kjellanderca843022015-06-09 10:51:22 +020083target_os = get_target_os_list()
84if 'android' in target_os:
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000085 DIRECTORIES += [
86 'base',
Henrik Kjellander94a12322015-06-09 14:56:20 +020087 'third_party/android_platform',
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000088 'third_party/android_tools',
kjellander@webrtc.orgcbe7ca82015-01-06 07:24:27 +000089 'third_party/appurify-python',
kjellander@webrtc.orgb8caf6a2014-09-30 18:05:02 +000090 'third_party/ashmem',
kjellander34a70542015-12-06 10:32:34 -080091 'third_party/catapult',
kjellander53761002015-11-10 10:58:22 -080092 'third_party/icu',
Henrik Kjellandereecbab72015-09-16 19:19:04 +020093 'third_party/ijar',
kjellander@webrtc.orgb8caf6a2014-09-30 18:05:02 +000094 'third_party/jsr-305',
Henrik Kjellander10ba3ee2015-04-29 14:47:53 +020095 'third_party/junit',
kjellander@webrtc.orgb8caf6a2014-09-30 18:05:02 +000096 'third_party/libevent',
97 'third_party/libxml',
Henrik Kjellander10ba3ee2015-04-29 14:47:53 +020098 'third_party/mockito',
kjellander@webrtc.orgb8caf6a2014-09-30 18:05:02 +000099 'third_party/modp_b64',
kjellander@webrtc.orgcbe7ca82015-01-06 07:24:27 +0000100 'third_party/requests',
Henrik Kjellander10ba3ee2015-04-29 14:47:53 +0200101 'third_party/robolectric',
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +0000102 'tools/android',
kjellander@webrtc.orgbfdee692015-02-03 15:23:34 +0000103 'tools/grit',
kjellander34a70542015-12-06 10:32:34 -0800104 'tools/telemetry',
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +0000105 ]
Henrik Kjellanderca843022015-06-09 10:51:22 +0200106if 'ios' in target_os:
107 DIRECTORIES.append('third_party/class-dump')
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +0000108
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000109FILES = {
Henrik Kjellanderd6d27e72015-09-25 22:19:11 +0200110 'tools/isolate_driver.py': None,
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000111 'third_party/BUILD.gn': None,
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000112}
113
kjellander@webrtc.orge94f83a2014-09-18 13:47:23 +0000114ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000115CHROMIUM_CHECKOUT = os.path.join('chromium', 'src')
116LINKS_DB = 'links'
117
118# Version management to make future upgrades/downgrades easier to support.
119SCHEMA_VERSION = 1
120
121
122def query_yes_no(question, default=False):
123 """Ask a yes/no question via raw_input() and return their answer.
124
125 Modified from http://stackoverflow.com/a/3041990.
126 """
127 prompt = " [%s/%%s]: "
128 prompt = prompt % ('Y' if default is True else 'y')
129 prompt = prompt % ('N' if default is False else 'n')
130
131 if default is None:
132 default = 'INVALID'
133
134 while True:
135 sys.stdout.write(question + prompt)
136 choice = raw_input().lower()
137 if choice == '' and default != 'INVALID':
138 return default
139
140 if 'yes'.startswith(choice):
141 return True
142 elif 'no'.startswith(choice):
143 return False
144
145 print "Please respond with 'yes' or 'no' (or 'y' or 'n')."
146
147
148# Actions
149class Action(object):
150 def __init__(self, dangerous):
151 self.dangerous = dangerous
152
153 def announce(self, planning):
154 """Log a description of this action.
155
156 Args:
157 planning - True iff we're in the planning stage, False if we're in the
158 doit stage.
159 """
160 pass
161
162 def doit(self, links_db):
163 """Execute the action, recording what we did to links_db, if necessary."""
164 pass
165
166
167class Remove(Action):
168 def __init__(self, path, dangerous):
169 super(Remove, self).__init__(dangerous)
170 self._priority = 0
171 self._path = path
172
173 def announce(self, planning):
174 log = logging.warn
175 filesystem_type = 'file'
176 if not self.dangerous:
177 log = logging.info
178 filesystem_type = 'link'
179 if planning:
180 log('Planning to remove %s: %s', filesystem_type, self._path)
181 else:
182 log('Removing %s: %s', filesystem_type, self._path)
183
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200184 def doit(self, _):
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000185 os.remove(self._path)
186
187
188class Rmtree(Action):
189 def __init__(self, path):
190 super(Rmtree, self).__init__(dangerous=True)
191 self._priority = 0
192 self._path = path
193
194 def announce(self, planning):
195 if planning:
196 logging.warn('Planning to remove directory: %s', self._path)
197 else:
198 logging.warn('Removing directory: %s', self._path)
199
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200200 def doit(self, _):
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000201 if sys.platform.startswith('win'):
202 # shutil.rmtree() doesn't work on Windows if any of the directories are
203 # read-only, which svn repositories are.
204 subprocess.check_call(['rd', '/q', '/s', self._path], shell=True)
205 else:
206 shutil.rmtree(self._path)
207
208
209class Makedirs(Action):
210 def __init__(self, path):
211 super(Makedirs, self).__init__(dangerous=False)
212 self._priority = 1
213 self._path = path
214
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200215 def doit(self, _):
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000216 try:
217 os.makedirs(self._path)
218 except OSError as e:
219 if e.errno != errno.EEXIST:
220 raise
221
222
223class Symlink(Action):
224 def __init__(self, source_path, link_path):
225 super(Symlink, self).__init__(dangerous=False)
226 self._priority = 2
227 self._source_path = source_path
228 self._link_path = link_path
229
230 def announce(self, planning):
231 if planning:
232 logging.info(
233 'Planning to create link from %s to %s', self._link_path,
234 self._source_path)
235 else:
236 logging.debug(
237 'Linking from %s to %s', self._link_path, self._source_path)
238
239 def doit(self, links_db):
240 # Files not in the root directory need relative path calculation.
241 # On Windows, use absolute paths instead since NTFS doesn't seem to support
242 # relative paths for symlinks.
243 if sys.platform.startswith('win'):
244 source_path = os.path.abspath(self._source_path)
245 else:
246 if os.path.dirname(self._link_path) != self._link_path:
247 source_path = os.path.relpath(self._source_path,
248 os.path.dirname(self._link_path))
249
250 os.symlink(source_path, os.path.abspath(self._link_path))
251 links_db[self._source_path] = self._link_path
252
253
254class LinkError(IOError):
255 """Failed to create a link."""
256 pass
257
258
259# Handles symlink creation on the different platforms.
260if sys.platform.startswith('win'):
261 def symlink(source_path, link_path):
262 flag = 1 if os.path.isdir(source_path) else 0
263 if not ctypes.windll.kernel32.CreateSymbolicLinkW(
264 unicode(link_path), unicode(source_path), flag):
265 raise OSError('Failed to create symlink to %s. Notice that only NTFS '
266 'version 5.0 and up has all the needed APIs for '
267 'creating symlinks.' % source_path)
268 os.symlink = symlink
269
270
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200271class WebRTCLinkSetup(object):
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000272 def __init__(self, links_db, force=False, dry_run=False, prompt=False):
273 self._force = force
274 self._dry_run = dry_run
275 self._prompt = prompt
276 self._links_db = links_db
277
278 def CreateLinks(self, on_bot):
279 logging.debug('CreateLinks')
280 # First, make a plan of action
281 actions = []
282
283 for source_path, link_path in FILES.iteritems():
284 actions += self._ActionForPath(
285 source_path, link_path, check_fn=os.path.isfile, check_msg='files')
286 for source_dir in DIRECTORIES:
287 actions += self._ActionForPath(
288 source_dir, None, check_fn=os.path.isdir,
289 check_msg='directories')
290
kjellander@webrtc.orge94f83a2014-09-18 13:47:23 +0000291 if not on_bot and self._force:
292 # When making the manual switch from legacy SVN checkouts to the new
293 # Git-based Chromium DEPS, the .gclient_entries file that contains cached
294 # URLs for all DEPS entries must be removed to avoid future sync problems.
295 entries_file = os.path.join(os.path.dirname(ROOT_DIR), '.gclient_entries')
296 if os.path.exists(entries_file):
297 actions.append(Remove(entries_file, dangerous=True))
298
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000299 actions.sort()
300
301 if self._dry_run:
302 for action in actions:
303 action.announce(planning=True)
304 logging.info('Not doing anything because dry-run was specified.')
305 sys.exit(0)
306
307 if any(a.dangerous for a in actions):
308 logging.warn('Dangerous actions:')
309 for action in (a for a in actions if a.dangerous):
310 action.announce(planning=True)
311 print
312
313 if not self._force:
314 logging.error(textwrap.dedent("""\
315 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
316 A C T I O N R E Q I R E D
317 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
318
319 Because chromium/src is transitioning to Git (from SVN), we needed to
320 change the way that the WebRTC standalone checkout works. Instead of
321 individually syncing subdirectories of Chromium in SVN, we're now
322 syncing Chromium (and all of its DEPS, as defined by its own DEPS file),
323 into the `chromium/src` directory.
324
325 As such, all Chromium directories which are currently pulled by DEPS are
326 now replaced with a symlink into the full Chromium checkout.
327
328 To avoid disrupting developers, we've chosen to not delete your
329 directories forcibly, in case you have some work in progress in one of
330 them :).
331
332 ACTION REQUIRED:
333 Before running `gclient sync|runhooks` again, you must run:
334 %s%s --force
335
336 Which will replace all directories which now must be symlinks, after
337 prompting with a summary of the work-to-be-done.
338 """), 'python ' if sys.platform.startswith('win') else '', sys.argv[0])
339 sys.exit(1)
340 elif self._prompt:
341 if not query_yes_no('Would you like to perform the above plan?'):
342 sys.exit(1)
343
344 for action in actions:
345 action.announce(planning=False)
346 action.doit(self._links_db)
347
348 if not on_bot and self._force:
349 logging.info('Completed!\n\nNow run `gclient sync|runhooks` again to '
350 'let the remaining hooks (that probably were interrupted) '
351 'execute.')
352
353 def CleanupLinks(self):
354 logging.debug('CleanupLinks')
355 for source, link_path in self._links_db.iteritems():
356 if source == 'SCHEMA_VERSION':
357 continue
358 if os.path.islink(link_path) or sys.platform.startswith('win'):
359 # os.path.islink() always returns false on Windows
360 # See http://bugs.python.org/issue13143.
361 logging.debug('Removing link to %s at %s', source, link_path)
362 if not self._dry_run:
363 if os.path.exists(link_path):
364 if sys.platform.startswith('win') and os.path.isdir(link_path):
Henrik Kjellanderc444de62015-04-29 11:27:22 +0200365 subprocess.check_call(['rmdir', '/q', '/s', link_path],
366 shell=True)
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000367 else:
368 os.remove(link_path)
369 del self._links_db[source]
370
371 @staticmethod
372 def _ActionForPath(source_path, link_path=None, check_fn=None,
373 check_msg=None):
374 """Create zero or more Actions to link to a file or directory.
375
376 This will be a symlink on POSIX platforms. On Windows this requires
377 that NTFS is version 5.0 or higher (Vista or newer).
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('_LinkChromiumPath can only be used to link to %s: '
408 'Tried to link to: %s' % (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
438def _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
453def 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 not is_admin():
492 logging.error('On Windows, you now need to have administrator '
493 'privileges for the shell running %s (or '
494 '`gclient sync|runhooks`).\nPlease start another command '
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200495 'prompt as Administrator and try again.', sys.argv[0])
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000496 return 1
497
498 if not os.path.exists(CHROMIUM_CHECKOUT):
499 logging.error('Cannot find a Chromium checkout at %s. Did you run "gclient '
500 'sync" before running this script?', CHROMIUM_CHECKOUT)
501 return 2
502
503 links_database = _initialize_database(LINKS_DB)
504 try:
505 symlink_creator = WebRTCLinkSetup(links_database, options.force,
506 options.dry_run, options.prompt)
507 symlink_creator.CleanupLinks()
508 if not options.clean_only:
509 symlink_creator.CreateLinks(on_bot)
510 except LinkError as e:
511 print >> sys.stderr, e.message
512 return 3
513 finally:
514 links_database.close()
515 return 0
516
517
518if __name__ == '__main__':
519 sys.exit(main())