OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Package Management | |
7 | |
8 This module is used to bring in external Python dependencies via eggs from the | |
9 PyPi repository. It is intended to work with any version of Python from 2.4 | |
M-A Ruel
2012/01/09 02:34:15
Don't claim to support 2.4 unless you plan to supp
chrisha
2012/01/31 16:35:40
I tested it on 2.4, assuming that depot_tools stil
| |
10 through 2.6 and beyond, as well as on any OS and hardware. | |
11 | |
12 The approach is to create a site directory in depot_tools root which will | |
13 contain those packages that are listed as dependencies in the module variable | |
14 PACKAGES. Since we can't guarantee that setuptools is available in all | |
15 distributions this module also contains the ability to bootstrap the site | |
16 directory by manually downloading and installing setuptools. Once setuptools is | |
17 available it uses that to install the other packages in the traditional | |
18 manner. | |
19 | |
20 Use is simple: | |
21 | |
22 import package_management | |
23 | |
24 # Before any imports from the site directory, call this. This only needs | |
25 # to be called in one place near the beginning of the program. | |
26 package_management.SetupSiteDirectory() | |
27 | |
28 # If 'SetupSiteDirectory' fails it will complain with an error message but | |
29 # continue happily. Expect ImportErrors when trying to import any third | |
30 # party modules from the site directory. | |
31 | |
32 import some_third_party_module | |
33 | |
34 ... etc ... | |
35 """ | |
36 | |
37 import cStringIO | |
38 import os | |
39 import re | |
40 import shutil | |
41 import site | |
42 import subprocess | |
43 import sys | |
44 import tempfile | |
45 import urllib | |
46 | |
47 | |
48 # This is the version of setuptools that we will download if the local | |
49 # python distribution does not include one. | |
50 SETUPTOOLS = ('setuptools', '0.6c11') | |
51 | |
52 # These are the packages that are to be installed in the site directory. | |
53 # easy_install makes it so that the most recently installed version of a | |
54 # package is the one that takes precedence, even if a newer version exists | |
55 # in the site directory. This allows us to blindly install these one on top | |
56 # of the other without worrying about whats already installed. | |
57 # | |
58 # NOTE: If we are often rolling these dependencies then users' site | |
59 # directories will grow monotonically. We could be purging any orphaned | |
60 # packages using the tools provided by pkg_resources. | |
61 PACKAGES = (('logilab-common', '0.57.1'), | |
62 ('logilab-astng', '0.23.1'), | |
63 ('pylint', '0.25.1')) | |
64 | |
65 | |
66 # The Python version suffix used in generating the site directory and in | |
67 # requesting packages from PyPi. | |
68 VERSION_SUFFIX = "%d.%d" % sys.version_info[0:2] | |
69 | |
70 # This is the root directory of the depot_tools installation. | |
71 ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) | |
72 | |
73 # This is the path of the site directory we will use. We make this | |
74 # python version specific so that this will continue to work even if the | |
75 # python version is rolled. | |
76 SITE_DIR = os.path.join(ROOT_DIR, 'site-packages-py%s' % VERSION_SUFFIX) | |
77 | |
78 # This status file is created the last time PACKAGES were rolled successfully. | |
79 # It is used to determine if packages need to be rolled by comparing against | |
80 # the age of __file__. | |
81 LAST_ROLLED = os.path.join(SITE_DIR, 'last_rolled.txt') | |
82 | |
83 | |
84 class Error(Exception): | |
85 """The base class for all module errors.""" | |
86 pass | |
87 | |
88 | |
89 class InstallError(Error): | |
90 """Thrown if an installation is unable to complete.""" | |
91 pass | |
92 | |
93 | |
94 class Package(object): | |
95 """A package represents a release of a project. | |
96 | |
97 We use this as a lightweight version of pkg_resources, allowing us to | |
98 perform an end-run around setuptools for the purpose of bootstrapping. Its | |
99 functionality is very limited. | |
100 | |
101 Attributes: | |
102 name: the name of the package. | |
103 version: the version of the package. | |
104 safe_name: the safe name of the package. | |
105 safe_version: the safe version string of the package. | |
106 file_name: the filename-safe name of the package. | |
107 file_version: the filename-safe version string of the package. | |
108 """ | |
109 | |
110 def __init__(self, name, version): | |
111 """Initialize this package. | |
112 | |
113 Args: | |
114 name: the name of the package. | |
115 version: the version of the package. | |
116 """ | |
117 self.name = name | |
118 self.version = version | |
119 self.safe_name = Package._MakeSafeName(self.name) | |
120 self.safe_version = Package._MakeSafeVersion(self.version) | |
121 self.file_name = Package._MakeSafeForFilename(self.safe_name) | |
122 self.file_version = Package._MakeSafeForFilename(self.safe_version) | |
123 | |
124 @staticmethod | |
125 def _MakeSafeName(name): | |
126 """Makes a safe package name, as per pkg_resources.""" | |
127 return re.sub('[^A-Za-z0-9]+', '-', name) | |
128 | |
129 @staticmethod | |
130 def _MakeSafeVersion(version): | |
131 """Makes a safe package version string, as per pkg_resources.""" | |
132 version = re.sub('\s+', '.', version) | |
133 return re.sub('[^A-Za-z0-9\.]+', '-', version) | |
134 | |
135 @staticmethod | |
136 def _MakeSafeForFilename(safe_name_or_version): | |
137 """Makes a safe name or safe version safe to use in a file name. | |
138 |safe_name_or_version| must be a safe name or version string as returned | |
139 by GetSafeName or GetSafeVersion. | |
140 """ | |
141 return re.sub('-', '_', safe_name_or_version) | |
142 | |
143 def GetAsRequirementString(self): | |
144 """Builds an easy_install requirements string representing this package.""" | |
145 return '%s==%s' % (self.name, self.version) | |
146 | |
147 def GetFilename(self, extension=None): | |
148 """Builds a filename for this package using the setuptools convention. | |
149 If |extensions| is provided it will be appended to the generated filename, | |
150 otherwise only the basename is returned. | |
151 | |
152 The following url discusses the filename format: | |
153 | |
154 http://svn.python.org/projects/sandbox/trunk/setuptools/doc/formats.txt | |
155 """ | |
156 filename = '%s-%s-py%s' % (self.file_name, self.file_version, | |
157 VERSION_SUFFIX) | |
158 | |
159 if extension: | |
160 if extension[0] != '.': | |
M-A Ruel
2012/01/09 02:34:15
if not extension.startswith('.'):
| |
161 filename += '.' | |
162 filename += extension | |
163 | |
164 return filename | |
165 | |
166 def GetPyPiUrl(self, extension): | |
167 """Returns the URL where this package is hosted on PyPi.""" | |
168 return 'http://pypi.python.org/packages/2.6/%c/%s/%s' % ( | |
169 self.file_name[0], self.file_name, self.GetFilename(extension)) | |
170 | |
171 def DownloadEgg(self, dest_dir, overwrite=False): | |
172 """Downloads the EGG for this URL. | |
173 | |
174 Args: | |
175 dest_dir: The directory where the EGG should be written. If the EGG | |
176 has already been downloaded and cached the returned path may not | |
177 be in this directory. | |
178 overwite: If True the destination path will be overwritten even if | |
179 it already exists. Defaults to False. | |
180 | |
181 Returns: | |
182 The path to the written EGG. | |
183 | |
184 Raises: | |
185 Error: if dest_dir doesn't exist, the EGG is unable to be written, | |
186 the URL doesn't exist, or the server returned an error, or the | |
187 transmission was interrupted. | |
188 """ | |
189 if not os.path.exists(dest_dir): | |
190 raise Error('Path does not exist: %s' % dest_dir) | |
191 | |
192 if not os.path.isdir(dest_dir): | |
193 raise Error('Path is not a directory: %s' % dest_dir) | |
194 | |
195 filename = os.path.abspath(os.path.join(dest_dir, self.GetFilename('egg'))) | |
196 if os.path.exists(filename): | |
197 if os.path.isdir(filename): | |
198 raise Error('Path is a directory: %s' % filename) | |
199 if not overwrite: | |
200 return filename | |
201 | |
202 url = self.GetPyPiUrl('egg') | |
203 | |
204 try: | |
205 (path, headers) = urllib.urlretrieve(url, filename) | |
206 return path | |
207 except (IOError, urllib.ContentTooShortError): | |
208 # Reraise with a new error type, keeping the original message and | |
209 # traceback. | |
210 raise Error, sys.exc_info()[1], sys.exc_info()[2] | |
211 | |
212 | |
213 def AddToPythonPath(path): | |
214 """Adds the provided path to the head of PYTHONPATH and sys.path.""" | |
M-A Ruel
2012/01/09 02:34:15
path = os.path.abspath(path)
| |
215 if path not in sys.path: | |
216 sys.path.insert(0, path) | |
217 | |
218 paths = os.environ.get('PYTHONPATH', '').split(os.pathsep) | |
219 if path not in paths: | |
220 paths.insert(0, path) | |
221 os.environ['PYTHONPATH'] = os.pathsep.join(paths) | |
222 | |
223 | |
224 def AddSiteDirectory(path): | |
225 """Adds the provided path to the runtime as a site directory. | |
226 | |
227 Any modules that are in the site directory will be available for importing | |
228 after this returns. If modules are added or deleted this must be called | |
229 again for the changes to be reflected in the runtime. | |
230 | |
231 This calls both AddToPythonPath and site.addsitedir. Both are needed to | |
232 convince easy_install to treat |path| as a site directory. | |
233 """ | |
234 AddToPythonPath(path) | |
235 site.addsitedir(path) | |
236 | |
237 | |
238 def EnsureSiteDirectory(path): | |
239 """Creates and/or adds the provided path to the runtime as a site directory. | |
240 | |
241 This works like AddSiteDirectory but it will create the directory if it | |
242 does not yet exist. | |
243 | |
244 Raise: | |
245 Error: if the site directory is unable to be created, or if it exists and | |
246 is not a directory. | |
247 """ | |
248 if os.path.exists(path): | |
249 if not os.path.isdir(path): | |
250 raise Error('Path is not a directory: %s' % path) | |
251 else: | |
252 try: | |
253 os.mkdir(path) | |
254 except IOError: | |
255 raise Error('Unable to create directory: %s' % path) | |
256 | |
257 AddSiteDirectory(path) | |
258 | |
259 | |
260 def ModuleIsFromPackage(module, package_path): | |
261 """Determines if a module has been imported from a given package. | |
262 | |
263 Args: | |
264 module: the module to test. | |
265 package_path: the path to the package to test. | |
266 | |
267 Returns: | |
268 True if |module| has been imported from |package_path|, False otherwise. | |
269 """ | |
270 try: | |
271 m = os.path.abspath(module.__file__) | |
272 p = os.path.abspath(package_path) | |
273 if len(m) <= len(p): | |
274 return False | |
275 if m[0:len(p)] != p: | |
276 return False | |
277 return m[len(p)] == os.sep | |
278 except AttributeError: | |
279 return False | |
280 | |
281 | |
282 def _CaptureStdStreams(function, *args, **kwargs): | |
283 """Captures stdout and stderr while running the provided function. | |
284 | |
285 This only works if |function| only accesses sys.stdout and sys.stderr. If | |
286 we need more than this we'll have to use subprocess.Popen. | |
287 | |
288 Args: | |
289 function: the function to be called. | |
290 args: the arguments to pass to |function|. | |
291 kwargs: the keyword arguments to pass to |function|. | |
292 """ | |
293 orig_stdout = sys.stdout | |
294 orig_stderr = sys.stderr | |
295 sys.stdout = cStringIO.StringIO() | |
296 sys.stderr = cStringIO.StringIO() | |
297 try: | |
298 return function(*args, **kwargs) | |
299 finally: | |
300 sys.stdout = orig_stdout | |
301 sys.stderr = orig_stderr | |
302 | |
303 | |
304 def InstallPackage(url_or_req, site_dir): | |
305 """Installs a package to a site directory. | |
306 | |
307 |site_dir| must exist and already be an active site directory. setuptools | |
308 must in the path. Uses easy_install which may involve a download from | |
309 pypi.python.org, so this also requires network access. | |
310 | |
311 Args: | |
312 url_or_req: the package to install, expressed as an URL (may be local), | |
313 or a requirement string. | |
314 site_dir: the site directory in which to install it. | |
315 | |
316 Raises: | |
317 InstallError: if installation fails for any reason. | |
318 """ | |
319 args = ['--quiet', '--install-dir', site_dir, '--exclude-scripts', | |
320 '--always-unzip', '--no-deps', url_or_req] | |
321 | |
322 # The easy_install script only calls SystemExit if something goes wrong. | |
323 # Otherwise, it falls through returning None. | |
324 try: | |
325 import setuptools.command.easy_install | |
326 _CaptureStdStreams(setuptools.command.easy_install.main, args) | |
327 except (ImportError, SystemExit), e: | |
328 # Re-raise the error, preserving the stack trace and message. | |
329 raise InstallError, sys.exc_info()[1], sys.exc_info()[2] | |
330 | |
331 | |
332 def _RunInSubprocess(pycode): | |
333 """Launches a python subprocess with the provided code. | |
334 | |
335 The subprocess will be launched with the same stdout and stderr. The | |
336 subprocess will use the same instance of python as is currently running, | |
337 passing |pycode| as arguments to this script. |pycode| will be interpreted | |
338 as python code in the context of this module. | |
339 | |
340 Returns: | |
341 True if the subprocess returned 0, False if it returned an error. | |
342 """ | |
343 return not subprocess.call([sys.executable, __file__, pycode]) | |
344 | |
345 | |
346 def _LoadSetupToolsFromEggAndInstall(egg_path): | |
347 """Loads setuptools from the provided egg |egg_path|, and installs it to | |
348 SITE_DIR. | |
349 | |
350 This is intended to be run from a subprocess as it pollutes the running | |
351 instance of Python by importing a module and then forcibly deleting its | |
352 source. | |
353 | |
354 Returns: | |
355 True on success, False on failure. | |
356 """ | |
357 AddToPythonPath(egg_path) | |
358 | |
359 try: | |
360 # Import setuptools and ensure it comes from the EGG. | |
361 import setuptools | |
362 if not ModuleIsFromPackage(setuptools, egg_path): | |
363 raise ImportError() | |
364 except ImportError: | |
365 print ' Unable to import downloaded package!' | |
366 return False | |
367 | |
368 try: | |
369 print ' Using setuptools to install itself ...' | |
370 InstallPackage(egg_path, SITE_DIR) | |
371 except InstallError: | |
372 print ' Unable to install setuptools!' | |
373 return False | |
374 | |
375 return True | |
376 | |
377 | |
378 def BootstrapSetupTools(): | |
379 """Bootstraps the runtime with setuptools. | |
380 | |
381 Will try to import setuptools directly. If not found it will attempt to | |
382 download it and load it from there. If the download is successful it will | |
383 then use setuptools to install itself in the site directory. | |
384 | |
385 This is meant to be run from a child process as it modifies the running | |
386 instance of Python by importing modules and then physically deleting them | |
387 from disk. | |
388 | |
389 Returns: | |
390 Returns True if 'import setuptools' will succeed, False otherwise. | |
391 """ | |
392 AddSiteDirectory(SITE_DIR) | |
393 | |
394 # Check if setuptools is already available. If so, we're done. | |
395 try: | |
396 import setuptools | |
397 return True | |
398 except ImportError: | |
399 pass | |
400 | |
401 print 'Bootstrapping setuptools ...' | |
402 | |
403 EnsureSiteDirectory(SITE_DIR) | |
404 | |
405 # Download the egg to a temp directory. | |
406 dest_dir = tempfile.mkdtemp('depot_tools') | |
407 path = None | |
408 try: | |
409 package = Package(*SETUPTOOLS) | |
410 print ' Downloading %s ...' % package.GetFilename() | |
411 path = package.DownloadEgg(dest_dir) | |
412 except Error: | |
413 print ' Download failed!' | |
414 shutil.rmtree(dest_dir) | |
415 return False | |
416 except: | |
M-A Ruel
2012/01/09 02:34:15
Not a fan. (IOError, OSError) would probably be fi
chrisha
2012/01/31 16:35:40
Not strictly needed. Removed.
| |
417 shutil.rmtree(dest_dir) | |
418 | |
419 try: | |
420 # Load the downloaded egg, and install it to the site directory. Do this | |
421 # in a subprocess so as not to pollute this runtime. | |
422 pycode = '_LoadSetupToolsFromEggAndInstall(%s)' % repr(path) | |
423 if not _RunInSubprocess(pycode): | |
424 raise Error | |
M-A Ruel
2012/01/09 02:34:15
raise Error()
| |
425 | |
426 # Reload our site directory, which should now contain setuptools. | |
427 AddSiteDirectory(SITE_DIR) | |
428 | |
429 # Try and import setupttols | |
430 import setuptools | |
431 except ImportError: | |
432 print ' Unable to import setuptools!' | |
433 return False | |
434 except Error: | |
435 # This happens if RunInSubProcess fails, and the appropriate error has | |
436 # already been written. | |
437 return False | |
438 finally: | |
439 # Delete the temp directory. | |
440 shutil.rmtree(dest_dir) | |
441 | |
442 return True | |
443 | |
444 | |
445 def _GetModTime(path): | |
446 """Gets the last modification time associated with |path| in seconds since | |
447 epoch, returning 0 if |path| does not exist. | |
448 """ | |
449 try: | |
450 return os.stat(path).st_mtime | |
451 except: | |
452 # This error is different depending on the OS. | |
453 return 0 | |
454 | |
455 | |
456 def _SiteDirectoryIsUpToDate(): | |
457 return _GetModTime(LAST_ROLLED) > _GetModTime(__file__) | |
458 | |
459 | |
460 def UpdateSiteDirectory(): | |
461 """Installs the packages from PACKAGES if they are not already installed. | |
462 At this point we must have setuptools in the site directory. | |
463 | |
464 This is intended to be run in a subprocess *prior* to the site directory | |
465 having been added to the parent process as it may cause packages to be | |
466 added and/or removed. | |
467 | |
468 Returns: | |
469 True on success, False otherwise. | |
470 """ | |
471 if _SiteDirectoryIsUpToDate(): | |
472 return True | |
473 | |
474 try: | |
475 AddSiteDirectory(SITE_DIR) | |
476 import pkg_resources | |
477 | |
478 # Determine if any packages actually need installing. | |
479 missing_packages = [] | |
480 for package in [SETUPTOOLS] + list(PACKAGES): | |
481 pkg = Package(*package) | |
482 req = pkg.GetAsRequirementString() | |
483 | |
484 # It may be that this package is already available in the site | |
485 # directory. If so, we can skip past it without trying to install it. | |
486 pkg_req = pkg_resources.Requirement.parse(req) | |
487 try: | |
488 dist = pkg_resources.working_set.find(pkg_req) | |
489 if dist: | |
490 continue | |
491 except pkg_resources.VersionConflict: | |
492 # This happens if another version of the package is already | |
493 # installed in another site directory (ie: the system site directory). | |
494 pass | |
495 | |
496 missing_packages.append(pkg) | |
497 | |
498 # Install the missing packages. | |
499 if missing_packages: | |
500 print 'Updating python packages ...' | |
501 for pkg in missing_packages: | |
502 print ' Installing %s ...' % pkg.GetFilename() | |
503 InstallPackage(pkg.GetAsRequirementString(), SITE_DIR) | |
504 | |
505 # Touch the status file so we know that we're up to date next time. | |
506 open(LAST_ROLLED, 'wb') | |
507 except InstallError, e: | |
508 print ' Installation failed: %s' % str(e) | |
509 return False | |
510 | |
511 return True | |
512 | |
513 | |
514 def SetupSiteDirectory(): | |
515 """Sets up the site directory, bootstrapping setuptools if necessary. | |
516 | |
517 If this finishes successfully then SITE_DIR will exist and will contain | |
518 the appropriate version of setuptools and all of the packages listed in | |
519 PACKAGES. | |
520 | |
521 This is the main workhorse of this module. Calling this will do everything | |
522 necessary to ensure that you have the desired packages installed in the | |
523 site directory, and the site directory enabled in this process. | |
524 | |
525 Returns: | |
526 True on success, False on failure. | |
527 """ | |
528 if _SiteDirectoryIsUpToDate(): | |
529 AddSiteDirectory(SITE_DIR) | |
530 return True | |
531 | |
532 if not _RunInSubprocess('BootstrapSetupTools()'): | |
533 return False | |
534 | |
535 if not _RunInSubprocess('UpdateSiteDirectory()'): | |
536 return False | |
537 | |
538 # Process the site directory so that the packages within it are available | |
539 # for import. | |
540 AddSiteDirectory(SITE_DIR) | |
541 | |
542 return True | |
543 | |
544 | |
545 def CanImportFromSiteDirectory(package_name): | |
546 """Determines if the given package can be imported from the site directory. | |
547 | |
548 Args: | |
549 package_name: the name of the package to import. | |
550 | |
551 Returns: | |
552 True if 'import package_name' will succeed and return a module from the | |
553 site directory, False otherwise. | |
554 """ | |
555 try: | |
556 return ModuleIsFromPackage(__import__(package_name), SITE_DIR) | |
557 except ImportError: | |
558 return False | |
559 | |
560 | |
561 def Test(): | |
562 """Runs SetupSiteDirectory and then tries to load pylint, ensuring that it | |
563 comes from the site directory just created. This is an end-to-end unittest | |
564 and allows for simple testing from the command-line by running | |
565 | |
566 ./package_management.py 'Test()' | |
567 """ | |
568 print 'Testing package_management.' | |
569 if not SetupSiteDirectory(): | |
570 print 'SetupSiteDirectory failed.' | |
571 return False | |
572 if not CanImportFromSiteDirectory('pylint'): | |
573 print 'CanImportFromSiteDirectory failed.' | |
574 return False | |
575 print 'Success!' | |
576 return True | |
577 | |
578 | |
579 def Main(): | |
580 """The main entry for the package management script. | |
581 | |
582 If no arguments are provided simply runs SetupSiteDirectory. If arguments | |
583 have been passed we execute the first argument as python code in the | |
584 context of this module. This mechanism is used during the bootstrap | |
585 process so that the main instance of Python does not have its runtime | |
586 polluted by various intermediate packages and imports. | |
587 | |
588 Returns: | |
589 0 on success, 1 otherwise. | |
590 """ | |
591 if len(sys.argv) == 2: | |
592 result = False | |
593 exec('result = %s' % sys.argv[1]) | |
594 | |
595 # Translate the success state to a return code. | |
596 return not result | |
597 else: | |
598 return not SetupSiteDirectory() | |
599 | |
600 | |
601 if __name__ == '__main__': | |
602 sys.exit(Main()) | |
OLD | NEW |