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