blob: ed8b923db1e685f138b8df96b2753f3f9043bbb2 [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 = {
kjellander@webrtc.org89256622014-08-20 12:10:11 +000096 'tools/find_depot_tools.py': None,
97 'third_party/BUILD.gn': None,
kjellander@webrtc.org89256622014-08-20 12:10:11 +000098}
99
kjellander@webrtc.orge94f83a2014-09-18 13:47:23 +0000100ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000101CHROMIUM_CHECKOUT = os.path.join('chromium', 'src')
102LINKS_DB = 'links'
103
104# Version management to make future upgrades/downgrades easier to support.
105SCHEMA_VERSION = 1
106
107
108def query_yes_no(question, default=False):
109 """Ask a yes/no question via raw_input() and return their answer.
110
111 Modified from http://stackoverflow.com/a/3041990.
112 """
113 prompt = " [%s/%%s]: "
114 prompt = prompt % ('Y' if default is True else 'y')
115 prompt = prompt % ('N' if default is False else 'n')
116
117 if default is None:
118 default = 'INVALID'
119
120 while True:
121 sys.stdout.write(question + prompt)
122 choice = raw_input().lower()
123 if choice == '' and default != 'INVALID':
124 return default
125
126 if 'yes'.startswith(choice):
127 return True
128 elif 'no'.startswith(choice):
129 return False
130
131 print "Please respond with 'yes' or 'no' (or 'y' or 'n')."
132
133
134# Actions
135class Action(object):
136 def __init__(self, dangerous):
137 self.dangerous = dangerous
138
139 def announce(self, planning):
140 """Log a description of this action.
141
142 Args:
143 planning - True iff we're in the planning stage, False if we're in the
144 doit stage.
145 """
146 pass
147
148 def doit(self, links_db):
149 """Execute the action, recording what we did to links_db, if necessary."""
150 pass
151
152
153class Remove(Action):
154 def __init__(self, path, dangerous):
155 super(Remove, self).__init__(dangerous)
156 self._priority = 0
157 self._path = path
158
159 def announce(self, planning):
160 log = logging.warn
161 filesystem_type = 'file'
162 if not self.dangerous:
163 log = logging.info
164 filesystem_type = 'link'
165 if planning:
166 log('Planning to remove %s: %s', filesystem_type, self._path)
167 else:
168 log('Removing %s: %s', filesystem_type, self._path)
169
170 def doit(self, _links_db):
171 os.remove(self._path)
172
173
174class Rmtree(Action):
175 def __init__(self, path):
176 super(Rmtree, self).__init__(dangerous=True)
177 self._priority = 0
178 self._path = path
179
180 def announce(self, planning):
181 if planning:
182 logging.warn('Planning to remove directory: %s', self._path)
183 else:
184 logging.warn('Removing directory: %s', self._path)
185
186 def doit(self, _links_db):
187 if sys.platform.startswith('win'):
188 # shutil.rmtree() doesn't work on Windows if any of the directories are
189 # read-only, which svn repositories are.
190 subprocess.check_call(['rd', '/q', '/s', self._path], shell=True)
191 else:
192 shutil.rmtree(self._path)
193
194
195class Makedirs(Action):
196 def __init__(self, path):
197 super(Makedirs, self).__init__(dangerous=False)
198 self._priority = 1
199 self._path = path
200
201 def doit(self, _links_db):
202 try:
203 os.makedirs(self._path)
204 except OSError as e:
205 if e.errno != errno.EEXIST:
206 raise
207
208
209class Symlink(Action):
210 def __init__(self, source_path, link_path):
211 super(Symlink, self).__init__(dangerous=False)
212 self._priority = 2
213 self._source_path = source_path
214 self._link_path = link_path
215
216 def announce(self, planning):
217 if planning:
218 logging.info(
219 'Planning to create link from %s to %s', self._link_path,
220 self._source_path)
221 else:
222 logging.debug(
223 'Linking from %s to %s', self._link_path, self._source_path)
224
225 def doit(self, links_db):
226 # Files not in the root directory need relative path calculation.
227 # On Windows, use absolute paths instead since NTFS doesn't seem to support
228 # relative paths for symlinks.
229 if sys.platform.startswith('win'):
230 source_path = os.path.abspath(self._source_path)
231 else:
232 if os.path.dirname(self._link_path) != self._link_path:
233 source_path = os.path.relpath(self._source_path,
234 os.path.dirname(self._link_path))
235
236 os.symlink(source_path, os.path.abspath(self._link_path))
237 links_db[self._source_path] = self._link_path
238
239
240class LinkError(IOError):
241 """Failed to create a link."""
242 pass
243
244
245# Handles symlink creation on the different platforms.
246if sys.platform.startswith('win'):
247 def symlink(source_path, link_path):
248 flag = 1 if os.path.isdir(source_path) else 0
249 if not ctypes.windll.kernel32.CreateSymbolicLinkW(
250 unicode(link_path), unicode(source_path), flag):
251 raise OSError('Failed to create symlink to %s. Notice that only NTFS '
252 'version 5.0 and up has all the needed APIs for '
253 'creating symlinks.' % source_path)
254 os.symlink = symlink
255
256
257class WebRTCLinkSetup():
258 def __init__(self, links_db, force=False, dry_run=False, prompt=False):
259 self._force = force
260 self._dry_run = dry_run
261 self._prompt = prompt
262 self._links_db = links_db
263
264 def CreateLinks(self, on_bot):
265 logging.debug('CreateLinks')
266 # First, make a plan of action
267 actions = []
268
269 for source_path, link_path in FILES.iteritems():
270 actions += self._ActionForPath(
271 source_path, link_path, check_fn=os.path.isfile, check_msg='files')
272 for source_dir in DIRECTORIES:
273 actions += self._ActionForPath(
274 source_dir, None, check_fn=os.path.isdir,
275 check_msg='directories')
276
kjellander@webrtc.orge94f83a2014-09-18 13:47:23 +0000277 if not on_bot and self._force:
278 # When making the manual switch from legacy SVN checkouts to the new
279 # Git-based Chromium DEPS, the .gclient_entries file that contains cached
280 # URLs for all DEPS entries must be removed to avoid future sync problems.
281 entries_file = os.path.join(os.path.dirname(ROOT_DIR), '.gclient_entries')
282 if os.path.exists(entries_file):
283 actions.append(Remove(entries_file, dangerous=True))
284
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000285 actions.sort()
286
287 if self._dry_run:
288 for action in actions:
289 action.announce(planning=True)
290 logging.info('Not doing anything because dry-run was specified.')
291 sys.exit(0)
292
293 if any(a.dangerous for a in actions):
294 logging.warn('Dangerous actions:')
295 for action in (a for a in actions if a.dangerous):
296 action.announce(planning=True)
297 print
298
299 if not self._force:
300 logging.error(textwrap.dedent("""\
301 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
302 A C T I O N R E Q I R E D
303 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
304
305 Because chromium/src is transitioning to Git (from SVN), we needed to
306 change the way that the WebRTC standalone checkout works. Instead of
307 individually syncing subdirectories of Chromium in SVN, we're now
308 syncing Chromium (and all of its DEPS, as defined by its own DEPS file),
309 into the `chromium/src` directory.
310
311 As such, all Chromium directories which are currently pulled by DEPS are
312 now replaced with a symlink into the full Chromium checkout.
313
314 To avoid disrupting developers, we've chosen to not delete your
315 directories forcibly, in case you have some work in progress in one of
316 them :).
317
318 ACTION REQUIRED:
319 Before running `gclient sync|runhooks` again, you must run:
320 %s%s --force
321
322 Which will replace all directories which now must be symlinks, after
323 prompting with a summary of the work-to-be-done.
324 """), 'python ' if sys.platform.startswith('win') else '', sys.argv[0])
325 sys.exit(1)
326 elif self._prompt:
327 if not query_yes_no('Would you like to perform the above plan?'):
328 sys.exit(1)
329
330 for action in actions:
331 action.announce(planning=False)
332 action.doit(self._links_db)
333
334 if not on_bot and self._force:
335 logging.info('Completed!\n\nNow run `gclient sync|runhooks` again to '
336 'let the remaining hooks (that probably were interrupted) '
337 'execute.')
338
339 def CleanupLinks(self):
340 logging.debug('CleanupLinks')
341 for source, link_path in self._links_db.iteritems():
342 if source == 'SCHEMA_VERSION':
343 continue
344 if os.path.islink(link_path) or sys.platform.startswith('win'):
345 # os.path.islink() always returns false on Windows
346 # See http://bugs.python.org/issue13143.
347 logging.debug('Removing link to %s at %s', source, link_path)
348 if not self._dry_run:
349 if os.path.exists(link_path):
350 if sys.platform.startswith('win') and os.path.isdir(link_path):
Henrik Kjellanderc444de62015-04-29 11:27:22 +0200351 subprocess.check_call(['rmdir', '/q', '/s', link_path],
352 shell=True)
kjellander@webrtc.org89256622014-08-20 12:10:11 +0000353 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())