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