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