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