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