You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
497 lines
16 KiB
497 lines
16 KiB
#!/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', |
|
'mojo', # TODO(kjellander): Remove, see webrtc:5629. |
|
'native_client', |
|
'net', |
|
'testing', |
|
'third_party/binutils', |
|
'third_party/drmemory', |
|
'third_party/instrumented_libraries', |
|
'third_party/libjpeg', |
|
'third_party/libjpeg_turbo', |
|
'third_party/llvm-build', |
|
'third_party/lss', |
|
'third_party/yasm', |
|
'third_party/WebKit', # TODO(kjellander): Remove, see webrtc:5629. |
|
'tools/clang', |
|
'tools/gn', |
|
'tools/gyp', |
|
'tools/memory', |
|
'tools/python', |
|
'tools/swarming_client', |
|
'tools/valgrind', |
|
'tools/vim', |
|
'tools/win', |
|
] |
|
|
|
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/libxml', |
|
'third_party/mockito', |
|
'third_party/modp_b64', |
|
'third_party/protobuf', |
|
'third_party/requests', |
|
'third_party/robolectric', |
|
'tools/android', |
|
'tools/grit', |
|
] |
|
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())
|
|
|