aboutsummaryrefslogtreecommitdiff
path: root/setup_links.py
blob: 492c38b70ed3dec5a010f39c29084d64f4397e05 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
#!/usr/bin/env python
# Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
#
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE file in the root of the source
# tree. An additional intellectual property rights grant can be found
# in the file PATENTS.  All contributing project authors may
# be found in the AUTHORS file in the root of the source tree.

"""Setup links to a Chromium checkout for WebRTC.

WebRTC standalone shares a lot of dependencies and build tools with Chromium.
To do this, many of the paths of a Chromium checkout is emulated by creating
symlinks to files and directories. This script handles the setup of symlinks to
achieve this.

It also handles cleanup of the legacy Subversion-based approach that was used
before Chrome switched over their master repo from Subversion to Git.
"""


import ctypes
import errno
import logging
import optparse
import os
import shelve
import shutil
import subprocess
import sys
import textwrap


DIRECTORIES = [
  'build',
  'buildtools',
  'testing',
  'third_party/binutils',
  'third_party/boringssl',
  'third_party/colorama',
  'third_party/drmemory',
  'third_party/expat',
  'third_party/ffmpeg',
  'third_party/instrumented_libraries',
  'third_party/jsoncpp',
  'third_party/libc++-static',
  'third_party/libjpeg',
  'third_party/libjpeg_turbo',
  'third_party/libsrtp',
  'third_party/libudev',
  'third_party/libvpx_new',
  'third_party/libyuv',
  'third_party/llvm-build',
  'third_party/lss',
  'third_party/nss',
  'third_party/ocmock',
  'third_party/openh264',
  'third_party/openmax_dl',
  'third_party/opus',
  'third_party/proguard',
  'third_party/protobuf',
  'third_party/sqlite',
  'third_party/syzygy',
  'third_party/usrsctp',
  'third_party/yasm',
  'third_party/zlib',
  'tools/clang',
  'tools/generate_library_loader',
  'tools/gn',
  'tools/gyp',
  'tools/memory',
  'tools/protoc_wrapper',
  'tools/python',
  'tools/swarming_client',
  'tools/valgrind',
  'tools/vim',
  'tools/win',
  'tools/xdisplaycheck',
]

from sync_chromium import get_target_os_list
target_os = get_target_os_list()
if 'android' in target_os:
  DIRECTORIES += [
    'base',
    'third_party/android_platform',
    'third_party/android_tools',
    'third_party/appurify-python',
    'third_party/ashmem',
    'third_party/catapult',
    'third_party/icu',
    'third_party/ijar',
    'third_party/jsr-305',
    'third_party/junit',
    'third_party/libevent',
    'third_party/libxml',
    'third_party/mockito',
    'third_party/modp_b64',
    'third_party/requests',
    'third_party/robolectric',
    'tools/android',
    'tools/grit',
    'tools/telemetry',
  ]
if 'ios' in target_os:
  DIRECTORIES.append('third_party/class-dump')

FILES = {
  'tools/isolate_driver.py': None,
  'third_party/BUILD.gn': None,
}

ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
CHROMIUM_CHECKOUT = os.path.join('chromium', 'src')
LINKS_DB = 'links'

# Version management to make future upgrades/downgrades easier to support.
SCHEMA_VERSION = 1


def query_yes_no(question, default=False):
  """Ask a yes/no question via raw_input() and return their answer.

  Modified from http://stackoverflow.com/a/3041990.
  """
  prompt = " [%s/%%s]: "
  prompt = prompt % ('Y' if default is True  else 'y')
  prompt = prompt % ('N' if default is False else 'n')

  if default is None:
    default = 'INVALID'

  while True:
    sys.stdout.write(question + prompt)
    choice = raw_input().lower()
    if choice == '' and default != 'INVALID':
      return default

    if 'yes'.startswith(choice):
      return True
    elif 'no'.startswith(choice):
      return False

    print "Please respond with 'yes' or 'no' (or 'y' or 'n')."


# Actions
class Action(object):
  def __init__(self, dangerous):
    self.dangerous = dangerous

  def announce(self, planning):
    """Log a description of this action.

    Args:
      planning - True iff we're in the planning stage, False if we're in the
                 doit stage.
    """
    pass

  def doit(self, links_db):
    """Execute the action, recording what we did to links_db, if necessary."""
    pass


class Remove(Action):
  def __init__(self, path, dangerous):
    super(Remove, self).__init__(dangerous)
    self._priority = 0
    self._path = path

  def announce(self, planning):
    log = logging.warn
    filesystem_type = 'file'
    if not self.dangerous:
      log = logging.info
      filesystem_type = 'link'
    if planning:
      log('Planning to remove %s: %s', filesystem_type, self._path)
    else:
      log('Removing %s: %s', filesystem_type, self._path)

  def doit(self, _):
    os.remove(self._path)


class Rmtree(Action):
  def __init__(self, path):
    super(Rmtree, self).__init__(dangerous=True)
    self._priority = 0
    self._path = path

  def announce(self, planning):
    if planning:
      logging.warn('Planning to remove directory: %s', self._path)
    else:
      logging.warn('Removing directory: %s', self._path)

  def doit(self, _):
    if sys.platform.startswith('win'):
      # shutil.rmtree() doesn't work on Windows if any of the directories are
      # read-only, which svn repositories are.
      subprocess.check_call(['rd', '/q', '/s', self._path], shell=True)
    else:
      shutil.rmtree(self._path)


class Makedirs(Action):
  def __init__(self, path):
    super(Makedirs, self).__init__(dangerous=False)
    self._priority = 1
    self._path = path

  def doit(self, _):
    try:
      os.makedirs(self._path)
    except OSError as e:
      if e.errno != errno.EEXIST:
        raise


class Symlink(Action):
  def __init__(self, source_path, link_path):
    super(Symlink, self).__init__(dangerous=False)
    self._priority = 2
    self._source_path = source_path
    self._link_path = link_path

  def announce(self, planning):
    if planning:
      logging.info(
          'Planning to create link from %s to %s', self._link_path,
          self._source_path)
    else:
      logging.debug(
          'Linking from %s to %s', self._link_path, self._source_path)

  def doit(self, links_db):
    # Files not in the root directory need relative path calculation.
    # On Windows, use absolute paths instead since NTFS doesn't seem to support
    # relative paths for symlinks.
    if sys.platform.startswith('win'):
      source_path = os.path.abspath(self._source_path)
    else:
      if os.path.dirname(self._link_path) != self._link_path:
        source_path = os.path.relpath(self._source_path,
                                      os.path.dirname(self._link_path))

    os.symlink(source_path, os.path.abspath(self._link_path))
    links_db[self._source_path] = self._link_path


class LinkError(IOError):
  """Failed to create a link."""
  pass


# Handles symlink creation on the different platforms.
if sys.platform.startswith('win'):
  def symlink(source_path, link_path):
    flag = 1 if os.path.isdir(source_path) else 0
    if not ctypes.windll.kernel32.CreateSymbolicLinkW(
        unicode(link_path), unicode(source_path), flag):
      raise OSError('Failed to create symlink to %s. Notice that only NTFS '
                    'version 5.0 and up has all the needed APIs for '
                    'creating symlinks.' % source_path)
  os.symlink = symlink


class WebRTCLinkSetup(object):
  def __init__(self, links_db, force=False, dry_run=False, prompt=False):
    self._force = force
    self._dry_run = dry_run
    self._prompt = prompt
    self._links_db = links_db

  def CreateLinks(self, on_bot):
    logging.debug('CreateLinks')
    # First, make a plan of action
    actions = []

    for source_path, link_path in FILES.iteritems():
      actions += self._ActionForPath(
          source_path, link_path, check_fn=os.path.isfile, check_msg='files')
    for source_dir in DIRECTORIES:
      actions += self._ActionForPath(
          source_dir, None, check_fn=os.path.isdir,
          check_msg='directories')

    if not on_bot and self._force:
      # When making the manual switch from legacy SVN checkouts to the new
      # Git-based Chromium DEPS, the .gclient_entries file that contains cached
      # URLs for all DEPS entries must be removed to avoid future sync problems.
      entries_file = os.path.join(os.path.dirname(ROOT_DIR), '.gclient_entries')
      if os.path.exists(entries_file):
        actions.append(Remove(entries_file, dangerous=True))

    actions.sort()

    if self._dry_run:
      for action in actions:
        action.announce(planning=True)
      logging.info('Not doing anything because dry-run was specified.')
      sys.exit(0)

    if any(a.dangerous for a in actions):
      logging.warn('Dangerous actions:')
      for action in (a for a in actions if a.dangerous):
        action.announce(planning=True)
      print

      if not self._force:
        logging.error(textwrap.dedent("""\
        @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
                              A C T I O N     R E Q I R E D
        @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

        Because chromium/src is transitioning to Git (from SVN), we needed to
        change the way that the WebRTC standalone checkout works. Instead of
        individually syncing subdirectories of Chromium in SVN, we're now
        syncing Chromium (and all of its DEPS, as defined by its own DEPS file),
        into the `chromium/src` directory.

        As such, all Chromium directories which are currently pulled by DEPS are
        now replaced with a symlink into the full Chromium checkout.

        To avoid disrupting developers, we've chosen to not delete your
        directories forcibly, in case you have some work in progress in one of
        them :).

        ACTION REQUIRED:
        Before running `gclient sync|runhooks` again, you must run:
        %s%s --force

        Which will replace all directories which now must be symlinks, after
        prompting with a summary of the work-to-be-done.
        """), 'python ' if sys.platform.startswith('win') else '', sys.argv[0])
        sys.exit(1)
      elif self._prompt:
        if not query_yes_no('Would you like to perform the above plan?'):
          sys.exit(1)

    for action in actions:
      action.announce(planning=False)
      action.doit(self._links_db)

    if not on_bot and self._force:
      logging.info('Completed!\n\nNow run `gclient sync|runhooks` again to '
                   'let the remaining hooks (that probably were interrupted) '
                   'execute.')

  def CleanupLinks(self):
    logging.debug('CleanupLinks')
    for source, link_path  in self._links_db.iteritems():
      if source == 'SCHEMA_VERSION':
        continue
      if os.path.islink(link_path) or sys.platform.startswith('win'):
        # os.path.islink() always returns false on Windows
        # See http://bugs.python.org/issue13143.
        logging.debug('Removing link to %s at %s', source, link_path)
        if not self._dry_run:
          if os.path.exists(link_path):
            if sys.platform.startswith('win') and os.path.isdir(link_path):
              subprocess.check_call(['rmdir', '/q', '/s', link_path],
                                    shell=True)
            else:
              os.remove(link_path)
          del self._links_db[source]

  @staticmethod
  def _ActionForPath(source_path, link_path=None, check_fn=None,
                     check_msg=None):
    """Create zero or more Actions to link to a file or directory.

    This will be a symlink on POSIX platforms. On Windows this requires
    that NTFS is version 5.0 or higher (Vista or newer).

    Args:
      source_path: Path relative to the Chromium checkout root.
        For readability, the path may contain slashes, which will
        automatically be converted to the right path delimiter on Windows.
      link_path: The location for the link to create. If omitted it will be the
        same path as source_path.
      check_fn: A function returning true if the type of filesystem object is
        correct for the attempted call. Otherwise an error message with
        check_msg will be printed.
      check_msg: String used to inform the user of an invalid attempt to create
        a file.
    Returns:
      A list of Action objects.
    """
    def fix_separators(path):
      if sys.platform.startswith('win'):
        return path.replace(os.altsep, os.sep)
      else:
        return path

    assert check_fn
    assert check_msg
    link_path = link_path or source_path
    link_path = fix_separators(link_path)

    source_path = fix_separators(source_path)
    source_path = os.path.join(CHROMIUM_CHECKOUT, source_path)
    if os.path.exists(source_path) and not check_fn:
      raise LinkError('_LinkChromiumPath can only be used to link to %s: '
                      'Tried to link to: %s' % (check_msg, source_path))

    if not os.path.exists(source_path):
      logging.debug('Silently ignoring missing source: %s. This is to avoid '
                    'errors on platform-specific dependencies.', source_path)
      return []

    actions = []

    if os.path.exists(link_path) or os.path.islink(link_path):
      if os.path.islink(link_path):
        actions.append(Remove(link_path, dangerous=False))
      elif os.path.isfile(link_path):
        actions.append(Remove(link_path, dangerous=True))
      elif os.path.isdir(link_path):
        actions.append(Rmtree(link_path))
      else:
        raise LinkError('Don\'t know how to plan: %s' % link_path)

    # Create parent directories to the target link if needed.
    target_parent_dirs = os.path.dirname(link_path)
    if (target_parent_dirs and
        target_parent_dirs != link_path and
        not os.path.exists(target_parent_dirs)):
      actions.append(Makedirs(target_parent_dirs))

    actions.append(Symlink(source_path, link_path))

    return actions

def _initialize_database(filename):
  links_database = shelve.open(filename)

  # Wipe the database if this version of the script ends up looking at a
  # newer (future) version of the links db, just to be sure.
  version = links_database.get('SCHEMA_VERSION')
  if version and version != SCHEMA_VERSION:
    logging.info('Found database with schema version %s while this script only '
                 'supports %s. Wiping previous database contents.', version,
                 SCHEMA_VERSION)
    links_database.clear()
  links_database['SCHEMA_VERSION'] = SCHEMA_VERSION
  return links_database


def main():
  on_bot = os.environ.get('CHROME_HEADLESS') == '1'

  parser = optparse.OptionParser()
  parser.add_option('-d', '--dry-run', action='store_true', default=False,
                    help='Print what would be done, but don\'t perform any '
                         'operations. This will automatically set logging to '
                         'verbose.')
  parser.add_option('-c', '--clean-only', action='store_true', default=False,
                    help='Only clean previously created links, don\'t create '
                         'new ones. This will automatically set logging to '
                         'verbose.')
  parser.add_option('-f', '--force', action='store_true', default=on_bot,
                    help='Force link creation. CAUTION: This deletes existing '
                         'folders and files in the locations where links are '
                         'about to be created.')
  parser.add_option('-n', '--no-prompt', action='store_false', dest='prompt',
                    default=(not on_bot),
                    help='Prompt if we\'re planning to do a dangerous action')
  parser.add_option('-v', '--verbose', action='store_const',
                    const=logging.DEBUG, default=logging.INFO,
                    help='Print verbose output for debugging.')
  options, _ = parser.parse_args()

  if options.dry_run or options.force or options.clean_only:
    options.verbose = logging.DEBUG
  logging.basicConfig(format='%(message)s', level=options.verbose)

  # Work from the root directory of the checkout.
  script_dir = os.path.dirname(os.path.abspath(__file__))
  os.chdir(script_dir)

  if sys.platform.startswith('win'):
    def is_admin():
      try:
        return os.getuid() == 0
      except AttributeError:
        return ctypes.windll.shell32.IsUserAnAdmin() != 0
    if not is_admin():
      logging.error('On Windows, you now need to have administrator '
                    'privileges for the shell running %s (or '
                    '`gclient sync|runhooks`).\nPlease start another command '
                    'prompt as Administrator and try again.', sys.argv[0])
      return 1

  if not os.path.exists(CHROMIUM_CHECKOUT):
    logging.error('Cannot find a Chromium checkout at %s. Did you run "gclient '
                  'sync" before running this script?', CHROMIUM_CHECKOUT)
    return 2

  links_database = _initialize_database(LINKS_DB)
  try:
    symlink_creator = WebRTCLinkSetup(links_database, options.force,
                                      options.dry_run, options.prompt)
    symlink_creator.CleanupLinks()
    if not options.clean_only:
      symlink_creator.CreateLinks(on_bot)
  except LinkError as e:
    print >> sys.stderr, e.message
    return 3
  finally:
    links_database.close()
  return 0


if __name__ == '__main__':
  sys.exit(main())