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