e94f83a191
The .gclient_entries file is written after a successful gclient sync operation and contains paths mapped to URLs for all DEPS entries that have been synced. This has been causing problems for users when switching from the legacy Subversion based checkouts to the new DEPS approach using a Chromium Git checkout combined with symlinks. Also it has been discovered that when entries have been removed from the Chromium DEPS file, subsequent gclient sync operations fail when it's trying to process those directories. This CL changes so that .gclient_entries is wiped for the WebRTC checkout when moving from the legacy SVN to Git. It also wipes the chromium/.gclient_entries file when a new Chromium revision is about to be synced, to avoid problems when DEPS entries have been removed. BUG=415219 R=agable@chromium.org Review URL: https://webrtc-codereview.appspot.com/28509004 git-svn-id: http://webrtc.googlecode.com/svn/trunk@7222 4adac7df-926f-26a2-2b94-8c16560cd09d
496 lines
16 KiB
Python
Executable File
496 lines
16 KiB
Python
Executable File
#!/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',
|
|
'google_apis', # Needed by build/common.gypi.
|
|
'net',
|
|
'testing',
|
|
'third_party/binutils',
|
|
'third_party/boringssl',
|
|
'third_party/colorama',
|
|
'third_party/drmemory',
|
|
'third_party/expat',
|
|
'third_party/icu',
|
|
'third_party/jsoncpp',
|
|
'third_party/libc++',
|
|
'third_party/libc++abi',
|
|
'third_party/libjpeg',
|
|
'third_party/libjpeg_turbo',
|
|
'third_party/libsrtp',
|
|
'third_party/libvpx',
|
|
'third_party/libyuv',
|
|
'third_party/llvm-build',
|
|
'third_party/nss',
|
|
'third_party/openmax_dl',
|
|
'third_party/opus',
|
|
'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/win',
|
|
]
|
|
|
|
from sync_chromium import get_target_os_list
|
|
if 'android' in get_target_os_list():
|
|
DIRECTORIES += [
|
|
'base',
|
|
'third_party/android_testrunner',
|
|
'third_party/android_tools',
|
|
'tools/android',
|
|
]
|
|
|
|
FILES = {
|
|
'.gn': None,
|
|
'tools/find_depot_tools.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, _links_db):
|
|
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, _links_db):
|
|
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, _links_db):
|
|
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():
|
|
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', 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())
|