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