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