blob: bdca32b7b797d6dca76c44343e9a2bd99133cdfc [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import print_function
import argparse
import contextlib
import glob
import logging
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import time
from util import STORAGE_URL, OBJECT_URL, LOCAL_STORAGE_PATH, LOCAL_OBJECT_URL
from util import build_manifest, filter_deps, read_python_literal, \
merge_deps, print_deps, platform_tag
LOGGER = logging.getLogger(__name__)
# /path/to/infra
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PYTHON_BAT_WIN = '@%~dp0\\..\\Scripts\\python.exe %*'
class NoWheelException(Exception):
def __init__(self, name, version, build, source_sha):
super(NoWheelException, self).__init__(
'No matching wheel found for (%s==%s (build %s_%s))' %
(name, version, build, source_sha))
def check_pydistutils():
if os.path.exists(os.path.expanduser('~/.pydistutils.cfg')):
print(
'\n'.join([
'', '', '=========== ERROR ===========',
'You have a ~/.pydistutils.cfg file, which interferes with the ',
'infra virtualenv environment. Please move it to the side and ',
'bootstrap again. Once infra has bootstrapped, you may move it '
'back.', '',
'Upstream bug: https://github.com/pypa/virtualenv/issues/88/', ''
]),
file=sys.stderr)
sys.exit(1)
def ls(prefix):
from pip._vendor import requests # pylint: disable=E0611
for retry in range(4):
try:
r = requests.get(STORAGE_URL, params=dict(
prefix=prefix,
fields='items(name,md5Hash)'
))
r.raise_for_status()
data = r.json()
break
except (requests.exceptions.SSLError, requests.exceptions.HTTPError) as ex:
delay = 4 ** (retry-1)
print(
'caught an error: %s: retrying in %f sec' % (ex, delay),
file=sys.stderr)
time.sleep(delay)
continue
else:
raise Exception("exceeded allowed retries!")
entries = data.get('items', [])
for entry in entries:
entry['md5Hash'] = entry['md5Hash'].decode('base64').encode('hex')
entry['local'] = False
entry['link'] = OBJECT_URL.format(entry['name'], entry['md5Hash'])
# Also look in the local cache
entries.extend([
{
'name': fname,
'md5Hash': None,
'local': True,
'link': LOCAL_OBJECT_URL.format(fname),
}
for fname in glob.glob(os.path.join(LOCAL_STORAGE_PATH,
prefix.split('/')[-1] + '*'))])
return entries
def sha_for(deps_entry):
if 'rev' in deps_entry:
return deps_entry['rev']
else:
return deps_entry['gs'].split('.')[0]
def get_links(deps):
import pip._internal.utils.compatibility_tags as compatibility_tags
import pip._internal.models.wheel as pip_wheel
plat_tag = platform_tag() # this is something like '_Ubuntu_14.04' or ''
pep425_tags = compatibility_tags.get_supported()
links = []
for name, dep in deps.iteritems():
version, source_sha = dep['version'] , sha_for(dep)
prefix = '{}-{}-{}_{}'.format(name, version, dep['build'], source_sha)
generic_wheels = []
platform_wheels = []
local_wheels = []
for entry in ls('wheels/'+prefix):
fname = entry['name'].split('/')[-1]
wheel_info = pip_wheel.Wheel.wheel_file_re.match(fname)
if not wheel_info:
LOGGER.warning('Skipping invalid wheel: %r', fname)
continue
# This check skips all obviously unsupported wheels (like Linux wheels on
# Windows).
if not pip_wheel.Wheel(fname).supported(pep425_tags):
continue
if entry['local']:
# A locally built wheel?
local_wheels.append(entry)
elif plat_tag and fname.startswith(prefix + plat_tag):
# A wheel targeting our very specific platform (if any)? This is hit on
# different versions of Ubuntu for example.
platform_wheels.append(entry)
else:
# Some more generic wheel (e.g. 'linux1many' or source wheel).
generic_wheels.append(entry)
# Prefer local wheels if have them, then per-platform, then generic.
wheel_set = local_wheels or platform_wheels or generic_wheels
if not wheel_set:
raise NoWheelException(name, version, dep['build'], source_sha)
if len(wheel_set) != 1:
LOGGER.warning('Letting pip choose a wheel for "%s":', name)
for entry in wheel_set:
LOGGER.warning(' * %s', entry['name'])
links.extend(entry['link'] for entry in wheel_set)
return links
@contextlib.contextmanager
def html_index(links):
tf = tempfile.mktemp('.html')
try:
with open(tf, 'w') as f:
print('<html><body>', file=f)
for link in links:
print('<a href="%s">wat</a>' % link, file=f)
print('</body></html>', file=f)
yield tf
finally:
os.unlink(tf)
def install(deps):
if sys.platform.startswith('win'):
# On Windows, "pip" is installed as a standalone binary called "pip.exe".
pip = [os.path.join(sys.prefix, 'Scripts', 'pip')]
else:
# On Linux, "pip" is a "#!/...python"-bootstrapped wrapper. Because of
# shebang length limitations, we will manually run this through the
# Python interpreter rather than relying on shebang interpretation.
pip = [
os.path.join(sys.prefix, 'bin', 'python'),
os.path.join(sys.prefix, 'bin', 'pip'),
]
links = get_links(deps)
with html_index(links) as ipath:
requirements = []
# TODO(iannucci): Do this as a requirements.txt
for name, deps_entry in deps.iteritems():
if not deps_entry.get('implicit'):
requirements.append('%s==%s' % (name, deps_entry['version']))
subprocess.check_call(
pip + ['install', '--no-index', '-f', ipath] + requirements)
def activate_env(env, manifest, quiet=False):
if not quiet:
print('Activating environment: %r' % env)
assert isinstance(manifest, dict)
manifest_path = os.path.join(env, 'manifest.pyl')
cur_manifest = read_python_literal(manifest_path)
if cur_manifest != manifest:
if not quiet:
print(' Removing old environment: %r' % cur_manifest)
shutil.rmtree(env, ignore_errors=True)
cur_manifest = None
virtualenv_dir = os.path.join(os.path.dirname(__file__), 'virtualenv-ext')
os.environ.pop('PYTHONPATH', None)
virtualenv_py = os.path.join(virtualenv_dir, 'virtualenv.py')
# This script itself is run in a virtualenv, and weird behavior can
# result if our version of virtualenv is of a different version than
# virtualenv-ext/. To avoid this, execute virtualenv.py under the real
# Python interpreter.
real_python = sys.executable
if hasattr(sys, 'real_prefix'):
real_python = sys.real_prefix
if not real_python.endswith('bin'):
real_python = os.path.join(real_python, 'bin')
real_python = os.path.join(real_python, 'python')
if sys.platform.startswith('win'):
real_python += '.exe'
if cur_manifest is None:
check_pydistutils()
if not quiet:
print(' Building new environment')
subprocess.check_call([real_python, virtualenv_py, env, '--no-download'])
if not quiet:
print(' Activating environment')
bin_dir = 'Scripts' if sys.platform.startswith('win') else 'bin'
activate_this = os.path.join(env, bin_dir, 'activate_this.py')
execfile(activate_this, dict(__file__=activate_this))
if cur_manifest is None:
deps = manifest.get('deps', {})
if not quiet:
print(' Installing deps')
print_deps(deps, indent=2, with_implicit=False)
install(deps)
subprocess.check_call([real_python, virtualenv_py, env, '--relocatable'])
# Write the original deps (including metadata) as manifest.
with open(manifest_path, 'wb') as f:
f.write(repr(manifest) + '\n')
# Create bin\python.bat on Windows to unify path where Python is found.
if sys.platform.startswith('win'):
bin_path = os.path.join(env, 'bin')
if not os.path.isdir(bin_path):
os.makedirs(bin_path)
python_bat_path = os.path.join(bin_path, 'python.bat')
if not os.path.isfile(python_bat_path):
with open(python_bat_path, 'w') as python_bat_file:
python_bat_file.write(PYTHON_BAT_WIN)
if not quiet:
print('Done creating environment')
def main(args):
parser = argparse.ArgumentParser()
parser.add_argument('--deps-file', '--deps_file', action='append',
help='Path to deps.pyl file (may be used multiple times, '
'default: bootstrap/deps.pyl)')
parser.add_argument('-q', '--quiet', action='store_true', default=False,
help='Supress all output')
parser.add_argument('env_path',
help='Path to place environment (default: %(default)s)',
default='ENV')
opts = parser.parse_args(args)
opts.deps_file = opts.deps_file or [os.path.join(ROOT, 'bootstrap/deps.pyl')]
# Skip deps not available for this flavor of Python interpreter.
#
# Possible platform names:
# macosx_x86_64
# linux_i686
# linux_x86_64
# windows_i686
# windows_x86_64
if sys.platform.startswith('linux'):
osname = 'linux'
elif sys.platform == 'darwin':
osname = 'macosx'
elif sys.platform == 'win32':
osname = 'windows'
else:
osname = sys.platform
if sys.maxsize == (2 ** 31) - 1:
bitness = 'i686'
else:
bitness = 'x86_64'
machine = platform.machine()
arch = 'arm' if ('arm' in machine or 'aarch' in machine) else 'intel'
if arch == 'arm':
print('---------------------------')
print('WARNING! WARNING! WARNING! ')
print('---------------------------')
print('The infra python environment is not available on ARM.')
print('If you need to develop infra tools for ARM, use Go instead.')
return
plat = '%s_%s' % (osname, bitness)
deps, kicked = filter_deps(merge_deps(opts.deps_file), plat)
manifest = build_manifest(deps)
activate_env(opts.env_path, manifest, opts.quiet)
if not opts.quiet and kicked:
print('---------------------------')
print('WARNING! WARNING! WARNING! ')
print('---------------------------')
print('The following deps were skipped, they are not available on %s' %
plat)
for pkg, dep in sorted(kicked.iteritems()):
print(' * %s (%s)' % (pkg, dep['version']))
if __name__ == '__main__':
logging.basicConfig()
LOGGER.setLevel(logging.DEBUG)
sys.exit(main(sys.argv[1:]))