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