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