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