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