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