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