OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. |
| 5 |
| 6 import argparse |
| 7 import contextlib |
| 8 import glob |
| 9 import logging |
| 10 import os |
| 11 import re |
| 12 import shutil |
| 13 import subprocess |
| 14 import sys |
| 15 import tempfile |
| 16 |
| 17 from util import STORAGE_URL, OBJECT_URL, LOCAL_STORAGE_PATH, LOCAL_OBJECT_URL |
| 18 from util import read_deps, merge_deps, print_deps, platform_tag |
| 19 |
| 20 LOGGER = logging.getLogger(__name__) |
| 21 |
| 22 # /path/to/recipes-py |
| 23 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 24 |
| 25 PYTHON_BAT_WIN = '@%~dp0\\..\\Scripts\\python.exe %*' |
| 26 |
| 27 |
| 28 class NoWheelException(Exception): |
| 29 def __init__(self, name, version, build, source_sha): |
| 30 super(NoWheelException, self).__init__( |
| 31 'No matching wheel found for (%s==%s (build %s_%s))' % |
| 32 (name, version, build, source_sha)) |
| 33 |
| 34 |
| 35 def check_pydistutils(): |
| 36 if os.path.exists(os.path.expanduser('~/.pydistutils.cfg')): |
| 37 print >> sys.stderr, '\n'.join([ |
| 38 '', |
| 39 '', |
| 40 '=========== ERROR ===========', |
| 41 'You have a ~/.pydistutils.cfg file, which interferes with the ', |
| 42 'recipes-py virtualenv environment. Please move it to the side and ' |
| 43 'bootstrap again. ' |
| 44 'Once recipes-py has bootstrapped, you may move it back.', |
| 45 '', |
| 46 'Upstream bug: https://github.com/pypa/virtualenv/issues/88/', |
| 47 '' |
| 48 ]) |
| 49 sys.exit(1) |
| 50 |
| 51 |
| 52 def ls(prefix): |
| 53 from pip._vendor import requests # pylint: disable=E0611 |
| 54 data = requests.get(STORAGE_URL, params=dict( |
| 55 prefix=prefix, |
| 56 fields='items(name,md5Hash)' |
| 57 )).json() |
| 58 entries = data.get('items', []) |
| 59 for entry in entries: |
| 60 entry['md5Hash'] = entry['md5Hash'].decode('base64').encode('hex') |
| 61 entry['local'] = False |
| 62 # Also look in the local cache |
| 63 entries.extend([ |
| 64 {'name': fname, 'md5Hash': None, 'local': True} |
| 65 for fname in glob.glob(os.path.join(LOCAL_STORAGE_PATH, |
| 66 prefix.split('/')[-1] + '*'))]) |
| 67 return entries |
| 68 |
| 69 |
| 70 def sha_for(deps_entry): |
| 71 if 'rev' in deps_entry: |
| 72 return deps_entry['rev'] |
| 73 else: |
| 74 return deps_entry['gs'].split('.')[0] |
| 75 |
| 76 |
| 77 def get_links(deps): |
| 78 import pip.wheel # pylint: disable=E0611 |
| 79 plat_tag = platform_tag() |
| 80 |
| 81 links = [] |
| 82 |
| 83 for name, dep in deps.iteritems(): |
| 84 version, source_sha = dep['version'] , sha_for(dep) |
| 85 prefix = 'wheels/{}-{}-{}_{}'.format(name, version, dep['build'], |
| 86 source_sha) |
| 87 generic_link = None |
| 88 binary_link = None |
| 89 local_link = None |
| 90 |
| 91 for entry in ls(prefix): |
| 92 fname = entry['name'].split('/')[-1] |
| 93 md5hash = entry['md5Hash'] |
| 94 wheel_info = pip.wheel.Wheel.wheel_file_re.match(fname) |
| 95 if not wheel_info: |
| 96 LOGGER.warn('Skipping invalid wheel: %r', fname) |
| 97 continue |
| 98 |
| 99 if pip.wheel.Wheel(fname).supported(): |
| 100 if entry['local']: |
| 101 link = LOCAL_OBJECT_URL.format(entry['name']) |
| 102 local_link = link |
| 103 continue |
| 104 else: |
| 105 link = OBJECT_URL.format(entry['name'], md5hash) |
| 106 if fname.endswith('none-any.whl'): |
| 107 if generic_link: |
| 108 LOGGER.error( |
| 109 'Found more than one generic matching wheel for %r: %r', |
| 110 prefix, dep) |
| 111 continue |
| 112 generic_link = link |
| 113 elif plat_tag in fname: |
| 114 if binary_link: |
| 115 LOGGER.error( |
| 116 'Found more than one binary matching wheel for %r: %r', |
| 117 prefix, dep) |
| 118 continue |
| 119 binary_link = link |
| 120 |
| 121 if not binary_link and not generic_link and not local_link: |
| 122 raise NoWheelException(name, version, dep['build'], source_sha) |
| 123 |
| 124 links.append(local_link or binary_link or generic_link) |
| 125 |
| 126 return links |
| 127 |
| 128 |
| 129 @contextlib.contextmanager |
| 130 def html_index(links): |
| 131 tf = tempfile.mktemp('.html') |
| 132 try: |
| 133 with open(tf, 'w') as f: |
| 134 print >> f, '<html><body>' |
| 135 for link in links: |
| 136 print >> f, '<a href="%s">wat</a>' % link |
| 137 print >> f, '</body></html>' |
| 138 yield tf |
| 139 finally: |
| 140 os.unlink(tf) |
| 141 |
| 142 |
| 143 def install(deps): |
| 144 bin_dir = 'Scripts' if sys.platform.startswith('win') else 'bin' |
| 145 pip = os.path.join(sys.prefix, bin_dir, 'pip') |
| 146 |
| 147 links = get_links(deps) |
| 148 with html_index(links) as ipath: |
| 149 requirements = [] |
| 150 # TODO(iannucci): Do this as a requirements.txt |
| 151 for name, deps_entry in deps.iteritems(): |
| 152 if not deps_entry.get('implicit'): |
| 153 requirements.append('%s==%s' % (name, deps_entry['version'])) |
| 154 subprocess.check_call( |
| 155 [pip, 'install', '--no-index', '--download-cache', |
| 156 os.path.join(ROOT, '.wheelcache'), '-f', ipath] + requirements) |
| 157 |
| 158 |
| 159 def activate_env(env, deps, quiet=False): |
| 160 if hasattr(sys, 'real_prefix'): |
| 161 LOGGER.error('Already activated environment!') |
| 162 return |
| 163 |
| 164 if not quiet: |
| 165 print 'Activating environment: %r' % env |
| 166 assert isinstance(deps, dict) |
| 167 |
| 168 manifest_path = os.path.join(env, 'manifest.pyl') |
| 169 cur_deps = read_deps(manifest_path) |
| 170 if cur_deps != deps: |
| 171 if not quiet: |
| 172 print ' Removing old environment: %r' % cur_deps |
| 173 shutil.rmtree(env, ignore_errors=True) |
| 174 cur_deps = None |
| 175 |
| 176 if cur_deps is None: |
| 177 check_pydistutils() |
| 178 |
| 179 if not quiet: |
| 180 print ' Building new environment' |
| 181 # Add in bundled virtualenv lib |
| 182 sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'virtualenv')) |
| 183 import virtualenv # pylint: disable=F0401 |
| 184 virtualenv.create_environment( |
| 185 env, search_dirs=virtualenv.file_search_dirs()) |
| 186 |
| 187 # Hack: On windows orig-prefix.txt contains the hardcoded path |
| 188 # "E:\b\depot_tools\python276_bin", but some systems have depot_tools |
| 189 # installed on C:\ instead, so fiddle site.py to try loading it from there |
| 190 # as well. |
| 191 if sys.platform.startswith('win'): |
| 192 site_py_path = os.path.join(env, 'Lib\\site.py') |
| 193 with open(site_py_path) as fh: |
| 194 site_py = fh.read() |
| 195 |
| 196 m = re.search(r'( +)sys\.real_prefix = .*', site_py) |
| 197 replacement = ('%(indent)sif (sys.real_prefix.startswith("E:\\\\") and\n' |
| 198 '%(indent)s not os.path.exists(sys.real_prefix)):\n' |
| 199 '%(indent)s cand = "C:\\\\setup" + sys.real_prefix[4:]\n' |
| 200 '%(indent)s if os.path.exists(cand):\n' |
| 201 '%(indent)s sys.real_prefix = cand\n' |
| 202 '%(indent)s else:\n' |
| 203 '%(indent)s sys.real_prefix = "C" + sys.real_prefix' |
| 204 '[1:]\n' |
| 205 % {'indent': m.group(1)}) |
| 206 |
| 207 site_py = site_py[:m.end(0)] + '\n' + replacement + site_py[m.end(0):] |
| 208 with open(site_py_path, 'w') as fh: |
| 209 fh.write(site_py) |
| 210 |
| 211 if not quiet: |
| 212 print ' Activating environment' |
| 213 # Ensure hermeticity during activation. |
| 214 os.environ.pop('PYTHONPATH', None) |
| 215 bin_dir = 'Scripts' if sys.platform.startswith('win') else 'bin' |
| 216 activate_this = os.path.join(env, bin_dir, 'activate_this.py') |
| 217 execfile(activate_this, dict(__file__=activate_this)) |
| 218 |
| 219 if cur_deps is None: |
| 220 if not quiet: |
| 221 print ' Installing deps' |
| 222 print_deps(deps, indent=2, with_implicit=False) |
| 223 install(deps) |
| 224 virtualenv.make_environment_relocatable(env) |
| 225 with open(manifest_path, 'wb') as f: |
| 226 f.write(repr(deps) + '\n') |
| 227 |
| 228 # Create bin\python.bat on Windows to unify path where Python is found. |
| 229 if sys.platform.startswith('win'): |
| 230 bin_path = os.path.join(env, 'bin') |
| 231 if not os.path.isdir(bin_path): |
| 232 os.makedirs(bin_path) |
| 233 python_bat_path = os.path.join(bin_path, 'python.bat') |
| 234 if not os.path.isfile(python_bat_path): |
| 235 with open(python_bat_path, 'w') as python_bat_file: |
| 236 python_bat_file.write(PYTHON_BAT_WIN) |
| 237 |
| 238 if not quiet: |
| 239 print 'Done creating environment' |
| 240 |
| 241 |
| 242 def main(args): |
| 243 parser = argparse.ArgumentParser() |
| 244 parser.add_argument('--deps-file', '--deps_file', action='append', |
| 245 help='Path to deps.pyl file (may be used multiple times)') |
| 246 parser.add_argument('-q', '--quiet', action='store_true', default=False, |
| 247 help='Supress all output') |
| 248 parser.add_argument('env_path', |
| 249 help='Path to place environment (default: %(default)s)', |
| 250 default='ENV') |
| 251 opts = parser.parse_args(args) |
| 252 |
| 253 deps = merge_deps(opts.deps_file) |
| 254 activate_env(opts.env_path, deps, opts.quiet) |
| 255 |
| 256 |
| 257 if __name__ == '__main__': |
| 258 logging.basicConfig() |
| 259 LOGGER.setLevel(logging.DEBUG) |
| 260 sys.exit(main(sys.argv[1:])) |
OLD | NEW |