blob: f985e80f14a984f671eee352be6d5e0f6aa9ae8f [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',
kjellander@webrtc.org4e4fe4f2014-10-01 08:03:19 +000043 'third_party/instrumented_libraries',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000044 'third_party/jsoncpp',
Henrik Kjellander26ab91b2015-11-26 10:26:32 +010045 'third_party/libc++-static',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000046 'third_party/libjpeg',
47 'third_party/libjpeg_turbo',
48 'third_party/libsrtp',
kjellander@webrtc.org7d4e6d02014-11-27 10:41:04 +000049 'third_party/libudev',
kjellanderd6024e32015-09-28 21:16:48 -070050 'third_party/libvpx_new',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000051 'third_party/libyuv',
52 'third_party/llvm-build',
Patrik Höglundc92c23d2015-08-31 11:30:14 +020053 'third_party/lss',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000054 'third_party/nss',
tkchin@webrtc.org3a63a3c2015-01-06 07:21:34 +000055 'third_party/ocmock',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000056 'third_party/openmax_dl',
57 'third_party/opus',
Patrik Höglundc92c23d2015-08-31 11:30:14 +020058 'third_party/proguard',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000059 'third_party/protobuf',
60 'third_party/sqlite',
61 'third_party/syzygy',
62 'third_party/usrsctp',
63 'third_party/yasm',
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000064 'third_party/zlib',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000065 'tools/clang',
66 'tools/generate_library_loader',
67 'tools/gn',
68 'tools/gyp',
69 'tools/memory',
70 'tools/protoc_wrapper',
71 'tools/python',
72 'tools/swarming_client',
73 'tools/valgrind',
Andrew MacDonald65de7d22015-05-19 11:37:34 -070074 'tools/vim',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000075 'tools/win',
Henrik Kjellander9589e2a2015-10-22 06:48:21 +020076 'tools/xdisplaycheck',
kjellander@webrtc.org89256622014-08-20 12:10:11 +000077]
78
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000079from sync_chromium import get_target_os_list
Henrik Kjellanderca843022015-06-09 10:51:22 +020080target_os = get_target_os_list()
81if 'android' in target_os:
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000082 DIRECTORIES += [
83 'base',
Henrik Kjellander94a12322015-06-09 14:56:20 +020084 'third_party/android_platform',
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000085 'third_party/android_tools',
kjellander@webrtc.orgcbe7ca82015-01-06 07:24:27 +000086 'third_party/appurify-python',
kjellander@webrtc.orgb8caf6a2014-09-30 18:05:02 +000087 'third_party/ashmem',
kjellander53761002015-11-10 10:58:22 -080088 'third_party/icu',
Henrik Kjellandereecbab72015-09-16 19:19:04 +020089 'third_party/ijar',
kjellander@webrtc.orgb8caf6a2014-09-30 18:05:02 +000090 'third_party/jsr-305',
Henrik Kjellander10ba3ee2015-04-29 14:47:53 +020091 'third_party/junit',
kjellander@webrtc.orgb8caf6a2014-09-30 18:05:02 +000092 'third_party/libevent',
93 'third_party/libxml',
Henrik Kjellander10ba3ee2015-04-29 14:47:53 +020094 'third_party/mockito',
kjellander@webrtc.orgb8caf6a2014-09-30 18:05:02 +000095 'third_party/modp_b64',
kjellander@webrtc.orgcbe7ca82015-01-06 07:24:27 +000096 'third_party/requests',
Henrik Kjellander10ba3ee2015-04-29 14:47:53 +020097 'third_party/robolectric',
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +000098 'tools/android',
kjellander@webrtc.orgbfdee692015-02-03 15:23:34 +000099 'tools/grit',
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +0000100 ]
Henrik Kjellanderca843022015-06-09 10:51:22 +0200101if 'ios' in target_os:
102 DIRECTORIES.append('third_party/class-dump')
kjellander@webrtc.org3bd41562014-09-01 11:06:37 +0000103
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000104FILES = {
Henrik Kjellanderd6d27e72015-09-25 22:19:11 +0200105 'tools/isolate_driver.py': None,
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000106 'third_party/BUILD.gn': None,
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000107}
108
kjellander@webrtc.orge94f83a2014-09-18 13:47:23 +0000109ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000110CHROMIUM_CHECKOUT = os.path.join('chromium', 'src')
111LINKS_DB = 'links'
112
113# Version management to make future upgrades/downgrades easier to support.
114SCHEMA_VERSION = 1
115
116
117def query_yes_no(question, default=False):
118 """Ask a yes/no question via raw_input() and return their answer.
119
120 Modified from http://stackoverflow.com/a/3041990.
121 """
122 prompt = " [%s/%%s]: "
123 prompt = prompt % ('Y' if default is True else 'y')
124 prompt = prompt % ('N' if default is False else 'n')
125
126 if default is None:
127 default = 'INVALID'
128
129 while True:
130 sys.stdout.write(question + prompt)
131 choice = raw_input().lower()
132 if choice == '' and default != 'INVALID':
133 return default
134
135 if 'yes'.startswith(choice):
136 return True
137 elif 'no'.startswith(choice):
138 return False
139
140 print "Please respond with 'yes' or 'no' (or 'y' or 'n')."
141
142
143# Actions
144class Action(object):
145 def __init__(self, dangerous):
146 self.dangerous = dangerous
147
148 def announce(self, planning):
149 """Log a description of this action.
150
151 Args:
152 planning - True iff we're in the planning stage, False if we're in the
153 doit stage.
154 """
155 pass
156
157 def doit(self, links_db):
158 """Execute the action, recording what we did to links_db, if necessary."""
159 pass
160
161
162class Remove(Action):
163 def __init__(self, path, dangerous):
164 super(Remove, self).__init__(dangerous)
165 self._priority = 0
166 self._path = path
167
168 def announce(self, planning):
169 log = logging.warn
170 filesystem_type = 'file'
171 if not self.dangerous:
172 log = logging.info
173 filesystem_type = 'link'
174 if planning:
175 log('Planning to remove %s: %s', filesystem_type, self._path)
176 else:
177 log('Removing %s: %s', filesystem_type, self._path)
178
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200179 def doit(self, _):
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000180 os.remove(self._path)
181
182
183class Rmtree(Action):
184 def __init__(self, path):
185 super(Rmtree, self).__init__(dangerous=True)
186 self._priority = 0
187 self._path = path
188
189 def announce(self, planning):
190 if planning:
191 logging.warn('Planning to remove directory: %s', self._path)
192 else:
193 logging.warn('Removing directory: %s', self._path)
194
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200195 def doit(self, _):
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000196 if sys.platform.startswith('win'):
197 # shutil.rmtree() doesn't work on Windows if any of the directories are
198 # read-only, which svn repositories are.
199 subprocess.check_call(['rd', '/q', '/s', self._path], shell=True)
200 else:
201 shutil.rmtree(self._path)
202
203
204class Makedirs(Action):
205 def __init__(self, path):
206 super(Makedirs, self).__init__(dangerous=False)
207 self._priority = 1
208 self._path = path
209
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200210 def doit(self, _):
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000211 try:
212 os.makedirs(self._path)
213 except OSError as e:
214 if e.errno != errno.EEXIST:
215 raise
216
217
218class Symlink(Action):
219 def __init__(self, source_path, link_path):
220 super(Symlink, self).__init__(dangerous=False)
221 self._priority = 2
222 self._source_path = source_path
223 self._link_path = link_path
224
225 def announce(self, planning):
226 if planning:
227 logging.info(
228 'Planning to create link from %s to %s', self._link_path,
229 self._source_path)
230 else:
231 logging.debug(
232 'Linking from %s to %s', self._link_path, self._source_path)
233
234 def doit(self, links_db):
235 # Files not in the root directory need relative path calculation.
236 # On Windows, use absolute paths instead since NTFS doesn't seem to support
237 # relative paths for symlinks.
238 if sys.platform.startswith('win'):
239 source_path = os.path.abspath(self._source_path)
240 else:
241 if os.path.dirname(self._link_path) != self._link_path:
242 source_path = os.path.relpath(self._source_path,
243 os.path.dirname(self._link_path))
244
245 os.symlink(source_path, os.path.abspath(self._link_path))
246 links_db[self._source_path] = self._link_path
247
248
249class LinkError(IOError):
250 """Failed to create a link."""
251 pass
252
253
254# Handles symlink creation on the different platforms.
255if sys.platform.startswith('win'):
256 def symlink(source_path, link_path):
257 flag = 1 if os.path.isdir(source_path) else 0
258 if not ctypes.windll.kernel32.CreateSymbolicLinkW(
259 unicode(link_path), unicode(source_path), flag):
260 raise OSError('Failed to create symlink to %s. Notice that only NTFS '
261 'version 5.0 and up has all the needed APIs for '
262 'creating symlinks.' % source_path)
263 os.symlink = symlink
264
265
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200266class WebRTCLinkSetup(object):
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000267 def __init__(self, links_db, force=False, dry_run=False, prompt=False):
268 self._force = force
269 self._dry_run = dry_run
270 self._prompt = prompt
271 self._links_db = links_db
272
273 def CreateLinks(self, on_bot):
274 logging.debug('CreateLinks')
275 # First, make a plan of action
276 actions = []
277
278 for source_path, link_path in FILES.iteritems():
279 actions += self._ActionForPath(
280 source_path, link_path, check_fn=os.path.isfile, check_msg='files')
281 for source_dir in DIRECTORIES:
282 actions += self._ActionForPath(
283 source_dir, None, check_fn=os.path.isdir,
284 check_msg='directories')
285
kjellander@webrtc.orge94f83a2014-09-18 13:47:23 +0000286 if not on_bot and self._force:
287 # When making the manual switch from legacy SVN checkouts to the new
288 # Git-based Chromium DEPS, the .gclient_entries file that contains cached
289 # URLs for all DEPS entries must be removed to avoid future sync problems.
290 entries_file = os.path.join(os.path.dirname(ROOT_DIR), '.gclient_entries')
291 if os.path.exists(entries_file):
292 actions.append(Remove(entries_file, dangerous=True))
293
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000294 actions.sort()
295
296 if self._dry_run:
297 for action in actions:
298 action.announce(planning=True)
299 logging.info('Not doing anything because dry-run was specified.')
300 sys.exit(0)
301
302 if any(a.dangerous for a in actions):
303 logging.warn('Dangerous actions:')
304 for action in (a for a in actions if a.dangerous):
305 action.announce(planning=True)
306 print
307
308 if not self._force:
309 logging.error(textwrap.dedent("""\
310 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
311 A C T I O N R E Q I R E D
312 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
313
314 Because chromium/src is transitioning to Git (from SVN), we needed to
315 change the way that the WebRTC standalone checkout works. Instead of
316 individually syncing subdirectories of Chromium in SVN, we're now
317 syncing Chromium (and all of its DEPS, as defined by its own DEPS file),
318 into the `chromium/src` directory.
319
320 As such, all Chromium directories which are currently pulled by DEPS are
321 now replaced with a symlink into the full Chromium checkout.
322
323 To avoid disrupting developers, we've chosen to not delete your
324 directories forcibly, in case you have some work in progress in one of
325 them :).
326
327 ACTION REQUIRED:
328 Before running `gclient sync|runhooks` again, you must run:
329 %s%s --force
330
331 Which will replace all directories which now must be symlinks, after
332 prompting with a summary of the work-to-be-done.
333 """), 'python ' if sys.platform.startswith('win') else '', sys.argv[0])
334 sys.exit(1)
335 elif self._prompt:
336 if not query_yes_no('Would you like to perform the above plan?'):
337 sys.exit(1)
338
339 for action in actions:
340 action.announce(planning=False)
341 action.doit(self._links_db)
342
343 if not on_bot and self._force:
344 logging.info('Completed!\n\nNow run `gclient sync|runhooks` again to '
345 'let the remaining hooks (that probably were interrupted) '
346 'execute.')
347
348 def CleanupLinks(self):
349 logging.debug('CleanupLinks')
350 for source, link_path in self._links_db.iteritems():
351 if source == 'SCHEMA_VERSION':
352 continue
353 if os.path.islink(link_path) or sys.platform.startswith('win'):
354 # os.path.islink() always returns false on Windows
355 # See http://bugs.python.org/issue13143.
356 logging.debug('Removing link to %s at %s', source, link_path)
357 if not self._dry_run:
358 if os.path.exists(link_path):
359 if sys.platform.startswith('win') and os.path.isdir(link_path):
Henrik Kjellanderc444de62015-04-29 11:27:22 +0200360 subprocess.check_call(['rmdir', '/q', '/s', link_path],
361 shell=True)
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000362 else:
363 os.remove(link_path)
364 del self._links_db[source]
365
366 @staticmethod
367 def _ActionForPath(source_path, link_path=None, check_fn=None,
368 check_msg=None):
369 """Create zero or more Actions to link to a file or directory.
370
371 This will be a symlink on POSIX platforms. On Windows this requires
372 that NTFS is version 5.0 or higher (Vista or newer).
373
374 Args:
375 source_path: Path relative to the Chromium checkout root.
376 For readability, the path may contain slashes, which will
377 automatically be converted to the right path delimiter on Windows.
378 link_path: The location for the link to create. If omitted it will be the
379 same path as source_path.
380 check_fn: A function returning true if the type of filesystem object is
381 correct for the attempted call. Otherwise an error message with
382 check_msg will be printed.
383 check_msg: String used to inform the user of an invalid attempt to create
384 a file.
385 Returns:
386 A list of Action objects.
387 """
388 def fix_separators(path):
389 if sys.platform.startswith('win'):
390 return path.replace(os.altsep, os.sep)
391 else:
392 return path
393
394 assert check_fn
395 assert check_msg
396 link_path = link_path or source_path
397 link_path = fix_separators(link_path)
398
399 source_path = fix_separators(source_path)
400 source_path = os.path.join(CHROMIUM_CHECKOUT, source_path)
401 if os.path.exists(source_path) and not check_fn:
402 raise LinkError('_LinkChromiumPath can only be used to link to %s: '
403 'Tried to link to: %s' % (check_msg, source_path))
404
405 if not os.path.exists(source_path):
406 logging.debug('Silently ignoring missing source: %s. This is to avoid '
407 'errors on platform-specific dependencies.', source_path)
408 return []
409
410 actions = []
411
412 if os.path.exists(link_path) or os.path.islink(link_path):
413 if os.path.islink(link_path):
414 actions.append(Remove(link_path, dangerous=False))
415 elif os.path.isfile(link_path):
416 actions.append(Remove(link_path, dangerous=True))
417 elif os.path.isdir(link_path):
418 actions.append(Rmtree(link_path))
419 else:
420 raise LinkError('Don\'t know how to plan: %s' % link_path)
421
422 # Create parent directories to the target link if needed.
423 target_parent_dirs = os.path.dirname(link_path)
424 if (target_parent_dirs and
425 target_parent_dirs != link_path and
426 not os.path.exists(target_parent_dirs)):
427 actions.append(Makedirs(target_parent_dirs))
428
429 actions.append(Symlink(source_path, link_path))
430
431 return actions
432
433def _initialize_database(filename):
434 links_database = shelve.open(filename)
435
436 # Wipe the database if this version of the script ends up looking at a
437 # newer (future) version of the links db, just to be sure.
438 version = links_database.get('SCHEMA_VERSION')
439 if version and version != SCHEMA_VERSION:
440 logging.info('Found database with schema version %s while this script only '
441 'supports %s. Wiping previous database contents.', version,
442 SCHEMA_VERSION)
443 links_database.clear()
444 links_database['SCHEMA_VERSION'] = SCHEMA_VERSION
445 return links_database
446
447
448def main():
449 on_bot = os.environ.get('CHROME_HEADLESS') == '1'
450
451 parser = optparse.OptionParser()
452 parser.add_option('-d', '--dry-run', action='store_true', default=False,
453 help='Print what would be done, but don\'t perform any '
454 'operations. This will automatically set logging to '
455 'verbose.')
456 parser.add_option('-c', '--clean-only', action='store_true', default=False,
457 help='Only clean previously created links, don\'t create '
458 'new ones. This will automatically set logging to '
459 'verbose.')
460 parser.add_option('-f', '--force', action='store_true', default=on_bot,
461 help='Force link creation. CAUTION: This deletes existing '
462 'folders and files in the locations where links are '
463 'about to be created.')
464 parser.add_option('-n', '--no-prompt', action='store_false', dest='prompt',
465 default=(not on_bot),
466 help='Prompt if we\'re planning to do a dangerous action')
467 parser.add_option('-v', '--verbose', action='store_const',
468 const=logging.DEBUG, default=logging.INFO,
469 help='Print verbose output for debugging.')
470 options, _ = parser.parse_args()
471
472 if options.dry_run or options.force or options.clean_only:
473 options.verbose = logging.DEBUG
474 logging.basicConfig(format='%(message)s', level=options.verbose)
475
476 # Work from the root directory of the checkout.
477 script_dir = os.path.dirname(os.path.abspath(__file__))
478 os.chdir(script_dir)
479
480 if sys.platform.startswith('win'):
481 def is_admin():
482 try:
483 return os.getuid() == 0
484 except AttributeError:
485 return ctypes.windll.shell32.IsUserAnAdmin() != 0
486 if not is_admin():
487 logging.error('On Windows, you now need to have administrator '
488 'privileges for the shell running %s (or '
489 '`gclient sync|runhooks`).\nPlease start another command '
Henrik Kjellander57e5fd22015-05-25 12:55:39 +0200490 'prompt as Administrator and try again.', sys.argv[0])
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000491 return 1
492
493 if not os.path.exists(CHROMIUM_CHECKOUT):
494 logging.error('Cannot find a Chromium checkout at %s. Did you run "gclient '
495 'sync" before running this script?', CHROMIUM_CHECKOUT)
496 return 2
497
498 links_database = _initialize_database(LINKS_DB)
499 try:
500 symlink_creator = WebRTCLinkSetup(links_database, options.force,
501 options.dry_run, options.prompt)
502 symlink_creator.CleanupLinks()
503 if not options.clean_only:
504 symlink_creator.CreateLinks(on_bot)
505 except LinkError as e:
506 print >> sys.stderr, e.message
507 return 3
508 finally:
509 links_database.close()
510 return 0
511
512
513if __name__ == '__main__':
514 sys.exit(main())