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