blob: aea887916d5398b60fe517b5f61856c826796439 [file] [log] [blame]
mblighfa29d662009-02-05 00:44:26 +00001#!/usr/bin/python
2#
3# Please keep this code python 2.4 compatible and stand alone.
4
5"""
6Fetch, build and install external Python library dependancies.
7
8This fetches external python libraries, builds them using your host's
9python and installs them under our own autotest/site-packages/ directory.
10
11Usage? Just run it.
12 utils/build_externals.py
13"""
14
lmr6f80e7a2010-02-04 03:18:28 +000015import compileall, logging, os, shutil, sys, tempfile, time, urllib2
mbligh489e0e02009-06-08 16:46:11 +000016import subprocess, re
mblighe5699e22009-09-18 16:44:12 +000017import common
18from autotest_lib.client.common_lib import logging_config, logging_manager
lmr6f80e7a2010-02-04 03:18:28 +000019from autotest_lib.client.common_lib import utils
mblighfa29d662009-02-05 00:44:26 +000020
21# Where package source be fetched to relative to the top of the autotest tree.
22PACKAGE_DIR = 'ExternalSource'
23
24# Where packages will be installed to relative to the top of the autotest tree.
25INSTALL_DIR = 'site-packages'
26
mbligh489e0e02009-06-08 16:46:11 +000027# Installs all packages, even if the system already has the version required
28INSTALL_ALL = False
29
mblighfa29d662009-02-05 00:44:26 +000030
31# Want to add more packages to fetch, build and install? See the class
32# definitions at the end of this file for examples of how to do it.
33
34
35_READ_SIZE = 64*1024
36_MAX_PACKAGE_SIZE = 100*1024*1024
37
38
mblighe5699e22009-09-18 16:44:12 +000039class BuildExternalsLoggingConfig(logging_config.LoggingConfig):
40 def configure_logging(self, results_dir=None, verbose=False):
41 super(BuildExternalsLoggingConfig, self).configure_logging(
42 use_console=True,
43 verbose=verbose)
44
mblighfa29d662009-02-05 00:44:26 +000045class Error(Exception):
46 """Local exception to be raised by code in this file."""
47
48class FetchError(Error):
49 """Failed to fetch a package from any of its listed URLs."""
50
51
52def main():
53 """
54 Find all ExternalPackage classes defined in this file and ask them to
55 fetch, build and install themselves.
56 """
mblighe5699e22009-09-18 16:44:12 +000057 logging_manager.configure_logging(BuildExternalsLoggingConfig(),
58 verbose=True)
mblighb155d0c2009-07-02 19:04:55 +000059 os.umask(022)
mblighfa29d662009-02-05 00:44:26 +000060
61 top_of_tree = _find_top_of_autotest_tree()
62 package_dir = os.path.join(top_of_tree, PACKAGE_DIR)
63 install_dir = os.path.join(top_of_tree, INSTALL_DIR)
64
lmr7ebba662009-10-23 12:55:09 +000065 # Make sure the install_dir is in our python module search path
66 # as well as the PYTHONPATH being used by all our setup.py
67 # install subprocesses.
mbligh623c5182009-02-07 01:31:31 +000068 if install_dir not in sys.path:
mbligh623c5182009-02-07 01:31:31 +000069 sys.path.insert(0, install_dir)
lmr7ebba662009-10-23 12:55:09 +000070 env_python_path_varname = 'PYTHONPATH'
71 env_python_path = os.environ.get(env_python_path_varname, '')
72 if install_dir+':' not in env_python_path:
73 os.environ[env_python_path_varname] = ':'.join([
74 install_dir, env_python_path])
mbligh623c5182009-02-07 01:31:31 +000075
mblighb155d0c2009-07-02 19:04:55 +000076 fetched_packages, fetch_errors = fetch_necessary_packages(package_dir,
77 install_dir)
mblighfa29d662009-02-05 00:44:26 +000078 install_errors = build_and_install_packages(fetched_packages, install_dir)
79
80 # Byte compile the code after it has been installed in its final
81 # location as .pyc files contain the path passed to compile_dir().
82 # When printing exception tracebacks, python uses that path first to look
83 # for the source code before checking the directory of the .pyc file.
84 # Don't leave references to our temporary build dir in the files.
mbligh623c5182009-02-07 01:31:31 +000085 logging.info('compiling .py files in %s to .pyc', install_dir)
86 compileall.compile_dir(install_dir, quiet=True)
87
88 # Some things install with whacky permissions, fix that.
89 system("chmod -R a+rX '%s'" % install_dir)
mblighfa29d662009-02-05 00:44:26 +000090
91 errors = fetch_errors + install_errors
92 for error_msg in errors:
93 print >>sys.stderr, error_msg
94
95 return len(errors)
96
97
mblighb155d0c2009-07-02 19:04:55 +000098def fetch_necessary_packages(dest_dir, install_dir):
mblighfa29d662009-02-05 00:44:26 +000099 """
100 Fetches all ExternalPackages into dest_dir.
101
mblighb155d0c2009-07-02 19:04:55 +0000102 @param dest_dir: Directory the packages should be fetched into.
103 @param install_dir: Directory where packages will later installed.
mblighfa29d662009-02-05 00:44:26 +0000104
105 @returns A tuple containing two lists:
106 * A list of ExternalPackage instances that were fetched and
107 need to be installed.
108 * A list of error messages for any failed fetches.
109 """
mbligh623c5182009-02-07 01:31:31 +0000110 names_to_check = sys.argv[1:]
mblighfa29d662009-02-05 00:44:26 +0000111 errors = []
112 fetched_packages = []
113 for package_class in ExternalPackage.subclasses:
114 package = package_class()
mbligh623c5182009-02-07 01:31:31 +0000115 if names_to_check and package.name.lower() not in names_to_check:
116 continue
mblighb155d0c2009-07-02 19:04:55 +0000117 if not package.is_needed(install_dir):
mblighfa29d662009-02-05 00:44:26 +0000118 logging.info('A new %s is not needed on this system.',
119 package.name)
mbligh489e0e02009-06-08 16:46:11 +0000120 if INSTALL_ALL:
121 logging.info('Installing anyways...')
122 else:
123 continue
mblighfa29d662009-02-05 00:44:26 +0000124 if not package.fetch(dest_dir):
125 msg = '!!! Unable to download %s' % package.name
126 print msg
127 errors.append(msg)
128 else:
129 fetched_packages.append(package)
130
131 return fetched_packages, errors
132
133
134def build_and_install_packages(packages, install_dir):
135 """
136 Builds and installs all packages into install_dir.
137
138 @param packages - A list of already fetched ExternalPackage instances.
139 @param install_dir - Directory the packages should be installed into.
140
141 @returns A list of error messages for any installs that failed.
142 """
143 errors = []
144 for package in packages:
145 if not package.build_and_install(install_dir):
146 msg = '!!! Unable to build and install %s' % package.name
147 print msg
148 errors.append(msg)
149 return errors
150
151
152def _checksum_file(full_path):
153 """@returns The hex checksum of a file given its pathname."""
154 inputfile = open(full_path, 'rb')
155 try:
lmr6f80e7a2010-02-04 03:18:28 +0000156 hex_sum = utils.hash('sha1', inputfile.read()).hexdigest()
mblighfa29d662009-02-05 00:44:26 +0000157 finally:
158 inputfile.close()
159 return hex_sum
160
161
162def _find_top_of_autotest_tree():
163 """@returns The full path to the top of the autotest directory tree."""
164 dirname = os.path.dirname(__file__)
165 autotest_dir = os.path.abspath(os.path.join(dirname, '..'))
166 return autotest_dir
167
168
169def system(commandline):
170 """Same as os.system(commandline) but logs the command first."""
171 logging.info(commandline)
172 return os.system(commandline)
173
174
175class ExternalPackage(object):
176 """
177 Defines an external package with URLs to fetch its sources from and
178 a build_and_install() method to unpack it, build it and install it
179 beneath our own autotest/site-packages directory.
180
181 Base Class. Subclass this to define packages.
182
183 Attributes:
184 @attribute urls - A tuple of URLs to try fetching the package from.
185 @attribute local_filename - A local filename to use when saving the
186 fetched package.
187 @attribute hex_sum - The hex digest (currently SHA1) of this package
188 to be used to verify its contents.
mbligh623c5182009-02-07 01:31:31 +0000189 @attribute module_name - The installed python module name to be used for
190 for a version check. Defaults to the lower case class name with
191 the word Package stripped off.
mblighf341fd12009-08-11 19:12:32 +0000192 @attribute version - The desired minimum package version.
193 @attribute os_requirements - A dictionary mapping a file pathname on the
194 the OS distribution to a likely name of a package the user
195 needs to install on their system in order to get this file.
mblighfa29d662009-02-05 00:44:26 +0000196 @attribute name - Read only, the printable name of the package.
197 @attribute subclasses - This class attribute holds a list of all defined
198 subclasses. It is constructed dynamically using the metaclass.
199 """
200 subclasses = []
201 urls = ()
202 local_filename = None
203 hex_sum = None
mbligh623c5182009-02-07 01:31:31 +0000204 module_name = None
205 version = None
mblighf341fd12009-08-11 19:12:32 +0000206 os_requirements = None
mblighfa29d662009-02-05 00:44:26 +0000207
208
209 class __metaclass__(type):
210 """Any time a subclass is defined, add it to our list."""
mbligh623c5182009-02-07 01:31:31 +0000211 def __init__(mcs, name, bases, dict):
mblighfa29d662009-02-05 00:44:26 +0000212 if name != 'ExternalPackage':
mbligh623c5182009-02-07 01:31:31 +0000213 mcs.subclasses.append(mcs)
mblighfa29d662009-02-05 00:44:26 +0000214
215
216 def __init__(self):
217 self.verified_package = ''
mbligh623c5182009-02-07 01:31:31 +0000218 if not self.module_name:
219 self.module_name = self.name.lower()
220 self.installed_version = ''
mblighfa29d662009-02-05 00:44:26 +0000221
222
223 @property
224 def name(self):
225 """Return the class name with any trailing 'Package' stripped off."""
226 class_name = self.__class__.__name__
227 if class_name.endswith('Package'):
228 return class_name[:-len('Package')]
229 return class_name
230
231
mblighb155d0c2009-07-02 19:04:55 +0000232 def is_needed(self, unused_install_dir):
mbligh623c5182009-02-07 01:31:31 +0000233 """@returns True if self.module_name needs to be built and installed."""
234 if not self.module_name or not self.version:
235 logging.warning('version and module_name required for '
236 'is_needed() check to work.')
237 return True
238 try:
239 module = __import__(self.module_name)
240 except ImportError, e:
241 logging.info('Could not import %s.', self.module_name)
242 return True
243 self.installed_version = self._get_installed_version_from_module(module)
244 logging.info('imported %s version %s.', self.module_name,
245 self.installed_version)
246 return self.version > self.installed_version
247
248
249 def _get_installed_version_from_module(self, module):
250 """Ask our module its version string and return it or '' if unknown."""
251 try:
252 return module.__version__
253 except AttributeError:
254 logging.error('could not get version from %s', module)
255 return ''
256
257
mblighfa29d662009-02-05 00:44:26 +0000258 def _build_and_install(self, install_dir):
259 """Subclasses MUST provide their own implementation."""
260 raise NotImplementedError
261
262
263 def _build_and_install_current_dir(self, install_dir):
264 """
mblighdb530cc2009-08-24 22:19:13 +0000265 Subclasses that use _build_and_install_from_package() MUST provide
mblighfa29d662009-02-05 00:44:26 +0000266 their own implementation of this method.
267 """
268 raise NotImplementedError
269
270
271 def build_and_install(self, install_dir):
272 """
273 Builds and installs the package. It must have been fetched already.
274
275 @param install_dir - The package installation directory. If it does
276 not exist it will be created.
277 """
278 if not self.verified_package:
279 raise Error('Must call fetch() first. - %s' % self.name)
mblighf341fd12009-08-11 19:12:32 +0000280 self._check_os_requirements()
mblighfa29d662009-02-05 00:44:26 +0000281 return self._build_and_install(install_dir)
282
283
mblighf341fd12009-08-11 19:12:32 +0000284 def _check_os_requirements(self):
285 if not self.os_requirements:
286 return
287 failed = False
288 for file_name, package_name in self.os_requirements.iteritems():
289 if not os.path.exists(file_name):
290 failed = True
291 logging.error('File %s not found, %s needs it.',
292 file_name, self.name)
293 logging.error('Perhaps you need to install something similar '
294 'to the %s package for OS first.', package_name)
295 if failed:
296 raise Error('Missing OS requirements for %s. (see above)' %
297 self.name)
298
299
mbligh623c5182009-02-07 01:31:31 +0000300 def _build_and_install_current_dir_setup_py(self, install_dir):
301 """For use as a _build_and_install_current_dir implementation."""
302 egg_path = self._build_egg_using_setup_py(setup_py='setup.py')
303 if not egg_path:
304 return False
305 return self._install_from_egg(install_dir, egg_path)
306
307
308 def _build_and_install_current_dir_setupegg_py(self, install_dir):
309 """For use as a _build_and_install_current_dir implementation."""
310 egg_path = self._build_egg_using_setup_py(setup_py='setupegg.py')
311 if not egg_path:
312 return False
313 return self._install_from_egg(install_dir, egg_path)
314
315
mbligh36cec882009-08-24 22:02:13 +0000316 def _build_and_install_current_dir_noegg(self, install_dir):
317 if not self._build_using_setup_py():
318 return False
319 return self._install_using_setup_py_and_rsync(install_dir)
320
321
mblighdb530cc2009-08-24 22:19:13 +0000322 def _build_and_install_from_package(self, install_dir):
mblighfa29d662009-02-05 00:44:26 +0000323 """
324 This method may be used as a _build_and_install() implementation
325 for subclasses if they implement _build_and_install_current_dir().
326
327 Extracts the .tar.gz file, chdirs into the extracted directory
328 (which is assumed to match the tar filename) and calls
329 _build_and_isntall_current_dir from there.
330
331 Afterwards the build (regardless of failure) extracted .tar.gz
332 directory is cleaned up.
333
334 @returns True on success, False otherwise.
335
336 @raises OSError If the expected extraction directory does not exist.
337 """
mblighdb530cc2009-08-24 22:19:13 +0000338 self._extract_compressed_package()
339 if self.verified_package.endswith('.tar.gz'):
340 extension = '.tar.gz'
341 elif self.verified_package.endswith('.tar.bz2'):
342 extension = '.tar.bz2'
343 elif self.verified_package.endswith('.zip'):
344 extension = '.zip'
345 else:
346 raise Error('Unexpected package file extension on %s' %
347 self.verified_package)
mblighfa29d662009-02-05 00:44:26 +0000348 os.chdir(os.path.dirname(self.verified_package))
mblighdb530cc2009-08-24 22:19:13 +0000349 os.chdir(self.local_filename[:-len(extension)])
mblighfa29d662009-02-05 00:44:26 +0000350 extracted_dir = os.getcwd()
351 try:
352 return self._build_and_install_current_dir(install_dir)
353 finally:
354 os.chdir(os.path.join(extracted_dir, '..'))
355 shutil.rmtree(extracted_dir)
356
357
mblighdb530cc2009-08-24 22:19:13 +0000358 def _extract_compressed_package(self):
359 """Extract the fetched compressed .tar or .zip within its directory."""
mblighfa29d662009-02-05 00:44:26 +0000360 if not self.verified_package:
361 raise Error('Package must have been fetched first.')
362 os.chdir(os.path.dirname(self.verified_package))
mblighb155d0c2009-07-02 19:04:55 +0000363 if self.verified_package.endswith('gz'):
364 status = system("tar -xzf '%s'" % self.verified_package)
365 elif self.verified_package.endswith('bz2'):
366 status = system("tar -xjf '%s'" % self.verified_package)
mblighdb530cc2009-08-24 22:19:13 +0000367 elif self.verified_package.endswith('zip'):
368 status = system("unzip '%s'" % self.verified_package)
mblighb155d0c2009-07-02 19:04:55 +0000369 else:
370 raise Error('Unknown compression suffix on %s.' %
371 self.verified_package)
mblighfa29d662009-02-05 00:44:26 +0000372 if status:
373 raise Error('tar failed with %s' % (status,))
374
375
mbligh623c5182009-02-07 01:31:31 +0000376 def _build_using_setup_py(self, setup_py='setup.py'):
mblighfa29d662009-02-05 00:44:26 +0000377 """
378 Assuming the cwd is the extracted python package, execute a simple
379 python setup.py build.
380
mbligh623c5182009-02-07 01:31:31 +0000381 @param setup_py - The name of the setup.py file to execute.
382
mblighfa29d662009-02-05 00:44:26 +0000383 @returns True on success, False otherwise.
384 """
mbligh623c5182009-02-07 01:31:31 +0000385 if not os.path.exists(setup_py):
386 raise Error('%sdoes not exist in %s' % (setup_py, os.getcwd()))
387 status = system("'%s' %s build" % (sys.executable, setup_py))
mblighfa29d662009-02-05 00:44:26 +0000388 if status:
389 logging.error('%s build failed.' % self.name)
390 return False
391 return True
392
393
mbligh623c5182009-02-07 01:31:31 +0000394 def _build_egg_using_setup_py(self, setup_py='setup.py'):
mblighfa29d662009-02-05 00:44:26 +0000395 """
396 Assuming the cwd is the extracted python package, execute a simple
397 python setup.py bdist_egg.
398
mbligh623c5182009-02-07 01:31:31 +0000399 @param setup_py - The name of the setup.py file to execute.
400
mblighfa29d662009-02-05 00:44:26 +0000401 @returns The relative path to the resulting egg file or '' on failure.
402 """
mbligh623c5182009-02-07 01:31:31 +0000403 if not os.path.exists(setup_py):
404 raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
mblighfa29d662009-02-05 00:44:26 +0000405 egg_subdir = 'dist'
406 if os.path.isdir(egg_subdir):
407 shutil.rmtree(egg_subdir)
mbligh623c5182009-02-07 01:31:31 +0000408 status = system("'%s' %s bdist_egg" % (sys.executable, setup_py))
mblighfa29d662009-02-05 00:44:26 +0000409 if status:
410 logging.error('bdist_egg of setuptools failed.')
411 return ''
412 # I've never seen a bdist_egg lay multiple .egg files.
413 for filename in os.listdir(egg_subdir):
414 if filename.endswith('.egg'):
415 return os.path.join(egg_subdir, filename)
416
417
418 def _install_from_egg(self, install_dir, egg_path):
419 """
420 Install a module from an egg file by unzipping the necessary parts
421 into install_dir.
422
423 @param install_dir - The installation directory.
424 @param egg_path - The pathname of the egg file.
425 """
426 status = system("unzip -q -o -d '%s' '%s'" % (install_dir, egg_path))
427 if status:
428 logging.error('unzip of %s failed', egg_path)
429 return False
430 egg_info = os.path.join(install_dir, 'EGG-INFO')
431 if os.path.isdir(egg_info):
432 shutil.rmtree(egg_info)
433 return True
434
435
mbligh623c5182009-02-07 01:31:31 +0000436 def _install_using_setup_py_and_rsync(self, install_dir,
437 setup_py='setup.py'):
mblighfa29d662009-02-05 00:44:26 +0000438 """
439 Assuming the cwd is the extracted python package, execute a simple:
440
441 python setup.py install --prefix=BLA
442
443 BLA will be a temporary directory that everything installed will
444 be picked out of and rsynced to the appropriate place under
445 install_dir afterwards.
446
447 Afterwards, it deconstructs the extra lib/pythonX.Y/site-packages/
448 directory tree that setuptools created and moves all installed
449 site-packages directly up into install_dir itself.
450
451 @param install_dir the directory for the install to happen under.
mbligh623c5182009-02-07 01:31:31 +0000452 @param setup_py - The name of the setup.py file to execute.
mblighfa29d662009-02-05 00:44:26 +0000453
454 @returns True on success, False otherwise.
455 """
mbligh623c5182009-02-07 01:31:31 +0000456 if not os.path.exists(setup_py):
457 raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
mblighfa29d662009-02-05 00:44:26 +0000458
459 temp_dir = tempfile.mkdtemp(dir='/var/tmp')
460 try:
mbligh623c5182009-02-07 01:31:31 +0000461 status = system("'%s' %s install --no-compile --prefix='%s'"
462 % (sys.executable, setup_py, temp_dir))
mblighfa29d662009-02-05 00:44:26 +0000463 if status:
mbligh623c5182009-02-07 01:31:31 +0000464 logging.error('%s install failed.' % self.name)
mblighfa29d662009-02-05 00:44:26 +0000465 return False
466
mblighfa29d662009-02-05 00:44:26 +0000467 # This makes assumptions about what python setup.py install
468 # does when given a prefix. Is this always correct?
469 python_xy = 'python%s' % sys.version[:3]
470 if os.path.isdir(os.path.join(temp_dir, 'lib')):
471 # NOTE: This ignores anything outside of the lib/ dir that
472 # was installed.
473 temp_site_dir = os.path.join(
474 temp_dir, 'lib', python_xy, 'site-packages')
475 else:
476 temp_site_dir = temp_dir
477
478 status = system("rsync -r '%s/' '%s/'" %
479 (temp_site_dir, install_dir))
480 if status:
481 logging.error('%s rsync to install_dir failed.' % self.name)
482 return False
483 return True
484 finally:
485 shutil.rmtree(temp_dir)
486
487
488
489 def fetch(self, dest_dir):
490 """
491 Fetch the package from one its URLs and save it in dest_dir.
492
493 If the the package already exists in dest_dir and the checksum
494 matches this code will not fetch it again.
495
496 Sets the 'verified_package' attribute with the destination pathname.
497
498 @param dest_dir - The destination directory to save the local file.
499 If it does not exist it will be created.
500
501 @returns A boolean indicating if we the package is now in dest_dir.
502 @raises FetchError - When something unexpected happens.
503 """
504 if not os.path.exists(dest_dir):
505 os.makedirs(dest_dir)
506 local_path = os.path.join(dest_dir, self.local_filename)
507
508 # If the package exists, verify its checksum and be happy if it is good.
509 if os.path.exists(local_path):
510 actual_hex_sum = _checksum_file(local_path)
511 if self.hex_sum == actual_hex_sum:
512 logging.info('Good checksum for existing %s package.',
513 self.name)
514 self.verified_package = local_path
515 return True
516 logging.warning('Bad checksum for existing %s package. '
517 'Re-downloading', self.name)
518 os.rename(local_path, local_path + '.wrong-checksum')
519
520 # Download the package from one of its urls, rejecting any if the
521 # checksum does not match.
522 for url in self.urls:
523 logging.info('Fetching %s', url)
524 try:
525 url_file = urllib2.urlopen(url)
mblighb155d0c2009-07-02 19:04:55 +0000526 except (urllib2.URLError, EnvironmentError):
mblighfa29d662009-02-05 00:44:26 +0000527 logging.warning('Could not fetch %s package from %s.',
528 self.name, url)
529 continue
530 data_length = int(url_file.info().get('Content-Length',
531 _MAX_PACKAGE_SIZE))
532 if data_length <= 0 or data_length > _MAX_PACKAGE_SIZE:
533 raise FetchError('%s from %s fails Content-Length %d '
534 'sanity check.' % (self.name, url,
535 data_length))
lmr6f80e7a2010-02-04 03:18:28 +0000536 checksum = utils.hash('sha1')
mblighfa29d662009-02-05 00:44:26 +0000537 total_read = 0
538 output = open(local_path, 'wb')
539 try:
540 while total_read < data_length:
541 data = url_file.read(_READ_SIZE)
542 if not data:
543 break
544 output.write(data)
545 checksum.update(data)
546 total_read += len(data)
547 finally:
548 output.close()
549 if self.hex_sum != checksum.hexdigest():
550 logging.warning('Bad checksum for %s fetched from %s.',
551 self.name, url)
showard78f5b012009-12-23 00:05:59 +0000552 logging.warning('Got %s', checksum.hexdigest())
553 logging.warning('Expected %s', self.hex_sum)
mblighfa29d662009-02-05 00:44:26 +0000554 os.unlink(local_path)
555 continue
556 logging.info('Good checksum.')
557 self.verified_package = local_path
558 return True
559 else:
560 return False
561
562
mbligh623c5182009-02-07 01:31:31 +0000563# NOTE: This class definition must come -before- all other ExternalPackage
mblighfa29d662009-02-05 00:44:26 +0000564# classes that need to use this version of setuptools so that is is inserted
565# into the ExternalPackage.subclasses list before them.
566class SetuptoolsPackage(ExternalPackage):
mbligh623c5182009-02-07 01:31:31 +0000567 # For all known setuptools releases a string compare works for the
568 # version string. Hopefully they never release a 0.10. (Their own
569 # version comparison code would break if they did.)
mblighfa29d662009-02-05 00:44:26 +0000570 version = '0.6c9'
571 urls = ('http://pypi.python.org/packages/source/s/setuptools/'
572 'setuptools-%s.tar.gz' % (version,),)
573 local_filename = 'setuptools-%s.tar.gz' % version
574 hex_sum = '79086433b341f0c1df45e10d586a7d3cc25089f1'
575
576 SUDO_SLEEP_DELAY = 15
577
578
mblighfa29d662009-02-05 00:44:26 +0000579 def _build_and_install(self, install_dir):
580 """Install setuptools on the system."""
581 logging.info('NOTE: setuptools install does not use install_dir.')
mblighdb530cc2009-08-24 22:19:13 +0000582 return self._build_and_install_from_package(install_dir)
mblighfa29d662009-02-05 00:44:26 +0000583
584
585 def _build_and_install_current_dir(self, install_dir):
586 egg_path = self._build_egg_using_setup_py()
587 if not egg_path:
588 return False
589
590 print '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
591 print 'About to run sudo to install setuptools', self.version
592 print 'on your system for use by', sys.executable, '\n'
593 print '!! ^C within', self.SUDO_SLEEP_DELAY, 'seconds to abort.\n'
594 time.sleep(self.SUDO_SLEEP_DELAY)
595
596 # Copy the egg to the local filesystem /var/tmp so that root can
597 # access it properly (avoid NFS squashroot issues).
598 temp_dir = tempfile.mkdtemp(dir='/var/tmp')
599 try:
600 shutil.copy(egg_path, temp_dir)
601 egg_name = os.path.split(egg_path)[1]
602 temp_egg = os.path.join(temp_dir, egg_name)
mbligh489e0e02009-06-08 16:46:11 +0000603 p = subprocess.Popen(['sudo', '/bin/sh', temp_egg],
604 stdout=subprocess.PIPE)
605 regex = re.compile('Copying (.*?) to (.*?)\n')
606 match = regex.search(p.communicate()[0])
607 status = p.wait()
608
609 if match:
610 compiled = os.path.join(match.group(2), match.group(1))
611 os.system("sudo chmod a+r '%s'" % compiled)
mblighfa29d662009-02-05 00:44:26 +0000612 finally:
613 shutil.rmtree(temp_dir)
614
615 if status:
616 logging.error('install of setuptools from egg failed.')
617 return False
618 return True
619
620
621class MySQLdbPackage(ExternalPackage):
mbligh623c5182009-02-07 01:31:31 +0000622 module_name = 'MySQLdb'
mblighfa29d662009-02-05 00:44:26 +0000623 version = '1.2.2'
mbligh279e5332009-07-28 23:32:11 +0000624 urls = ('http://downloads.sourceforge.net/project/mysql-python/'
625 'mysql-python/%(version)s/MySQL-python-%(version)s.tar.gz'
626 % dict(version=version),)
mblighfa29d662009-02-05 00:44:26 +0000627 local_filename = 'MySQL-python-%s.tar.gz' % version
628 hex_sum = '945a04773f30091ad81743f9eb0329a3ee3de383'
629
mbligh623c5182009-02-07 01:31:31 +0000630 _build_and_install_current_dir = (
631 ExternalPackage._build_and_install_current_dir_setup_py)
mblighfa29d662009-02-05 00:44:26 +0000632
633
mbligh3916b262009-06-08 16:36:35 +0000634 def _build_and_install(self, install_dir):
635 if not os.path.exists('/usr/bin/mysql_config'):
636 logging.error('You need to install /usr/bin/mysql_config')
637 logging.error('On Ubuntu or Debian based systems use this: '
mblighfd387072009-09-03 20:46:23 +0000638 'sudo apt-get install libmysqlclient15-dev')
mbligh3916b262009-06-08 16:36:35 +0000639 return False
mblighdb530cc2009-08-24 22:19:13 +0000640 return self._build_and_install_from_package(install_dir)
mbligh3916b262009-06-08 16:36:35 +0000641
642
mblighfa29d662009-02-05 00:44:26 +0000643class DjangoPackage(ExternalPackage):
showard78f5b012009-12-23 00:05:59 +0000644 version = '1.1.1'
showardb2649f92009-10-28 19:53:10 +0000645 local_filename = 'Django-%s.tar.gz' % version
646 urls = ('http://www.djangoproject.com/download/%s/tarball/' % version,)
showard78f5b012009-12-23 00:05:59 +0000647 hex_sum = '441c54f0e90730bf4a55432b64519169b1e6ef20'
mblighfa29d662009-02-05 00:44:26 +0000648
mblighdb530cc2009-08-24 22:19:13 +0000649 _build_and_install = ExternalPackage._build_and_install_from_package
mbligh36cec882009-08-24 22:02:13 +0000650 _build_and_install_current_dir = (
651 ExternalPackage._build_and_install_current_dir_noegg)
mblighfa29d662009-02-05 00:44:26 +0000652
653
mbligh623c5182009-02-07 01:31:31 +0000654 def _get_installed_version_from_module(self, module):
mblighb155d0c2009-07-02 19:04:55 +0000655 try:
656 return module.get_version().split()[0]
657 except AttributeError:
658 return '0.9.6'
mbligh623c5182009-02-07 01:31:31 +0000659
660
mblighfa29d662009-02-05 00:44:26 +0000661
mbligh623c5182009-02-07 01:31:31 +0000662class NumpyPackage(ExternalPackage):
663 version = '1.2.1'
664 local_filename = 'numpy-%s.tar.gz' % version
mbligh279e5332009-07-28 23:32:11 +0000665 urls = ('http://downloads.sourceforge.net/project/numpy/NumPy/%(version)s/'
666 'numpy-%(version)s.tar.gz' % dict(version=version),)
mbligh623c5182009-02-07 01:31:31 +0000667 hex_sum = '1aa706e733aea18eaffa70d93c0105718acb66c5'
668
mblighdb530cc2009-08-24 22:19:13 +0000669 _build_and_install = ExternalPackage._build_and_install_from_package
mbligh623c5182009-02-07 01:31:31 +0000670 _build_and_install_current_dir = (
671 ExternalPackage._build_and_install_current_dir_setupegg_py)
672
673
674# This requires numpy so it must be declared after numpy to guarantee that it
675# is already installed.
676class MatplotlibPackage(ExternalPackage):
mbligh279e5332009-07-28 23:32:11 +0000677 version = '0.98.5.3'
678 short_version = '0.98.5'
mbligh623c5182009-02-07 01:31:31 +0000679 local_filename = 'matplotlib-%s.tar.gz' % version
mbligh279e5332009-07-28 23:32:11 +0000680 urls = ('http://downloads.sourceforge.net/project/matplotlib/matplotlib/'
681 'matplotlib-%s/matplotlib-%s.tar.gz' % (short_version, version),)
682 hex_sum = '2f6c894cf407192b3b60351bcc6468c0385d47b6'
mblighf341fd12009-08-11 19:12:32 +0000683 os_requirements = {'/usr/include/ft2build.h': 'libfreetype6-dev',
684 '/usr/include/png.h': 'libpng12-dev'}
mbligh623c5182009-02-07 01:31:31 +0000685
mblighdb530cc2009-08-24 22:19:13 +0000686 _build_and_install = ExternalPackage._build_and_install_from_package
mbligh623c5182009-02-07 01:31:31 +0000687 _build_and_install_current_dir = (
688 ExternalPackage._build_and_install_current_dir_setupegg_py)
689
690
mblighdb530cc2009-08-24 22:19:13 +0000691class AtForkPackage(ExternalPackage):
mblighbeb687d2009-09-03 20:18:41 +0000692 version = '0.1.2'
mblighdb530cc2009-08-24 22:19:13 +0000693 local_filename = 'atfork-%s.zip' % version
694 urls = ('http://python-atfork.googlecode.com/files/' + local_filename,)
mblighbeb687d2009-09-03 20:18:41 +0000695 hex_sum = '5baa64c73e966b57fa797040585c760c502dc70b'
mblighdb530cc2009-08-24 22:19:13 +0000696
697 _build_and_install = ExternalPackage._build_and_install_from_package
698 _build_and_install_current_dir = (
699 ExternalPackage._build_and_install_current_dir_noegg)
700
701
mblighb155d0c2009-07-02 19:04:55 +0000702class ParamikoPackage(ExternalPackage):
mblighf51b2892009-08-24 22:03:09 +0000703 version = '1.7.5'
mblighb155d0c2009-07-02 19:04:55 +0000704 local_filename = 'paramiko-%s.tar.gz' % version
705 urls = ('http://www.lag.net/paramiko/download/' + local_filename,)
mblighf51b2892009-08-24 22:03:09 +0000706 hex_sum = '592be7a08290070b71da63a8e6f28a803399e5c5'
mblighb155d0c2009-07-02 19:04:55 +0000707
708
mblighdb530cc2009-08-24 22:19:13 +0000709 _build_and_install = ExternalPackage._build_and_install_from_package
mblighb155d0c2009-07-02 19:04:55 +0000710
711
712 def _check_for_pycrypto(self):
713 # NOTE(gps): Linux distros have better python-crypto packages than we
714 # can easily get today via a wget due to the library's age and staleness
715 # yet many security and behavior bugs are fixed by patches that distros
716 # already apply. PyCrypto has a new active maintainer in 2009. Once a
717 # new release is made (http://pycrypto.org/) we should add an installer.
718 try:
719 import Crypto
720 except ImportError:
721 logging.error('Please run "sudo apt-get install python-crypto" '
722 'or your Linux distro\'s equivalent.')
723 return False
724 return True
725
726
727 def _build_and_install_current_dir(self, install_dir):
728 if not self._check_for_pycrypto():
729 return False
730 # paramiko 1.7.4 doesn't require building, it is just a module directory
731 # that we can rsync into place directly.
732 if not os.path.isdir('paramiko'):
733 raise Error('no paramiko directory in %s.' % os.getcwd())
734 status = system("rsync -r 'paramiko' '%s/'" % install_dir)
735 if status:
736 logging.error('%s rsync to install_dir failed.' % self.name)
737 return False
738 return True
739
740
mbligh19841972010-01-11 19:15:56 +0000741class SimplejsonPackage(ExternalPackage):
742 version = '2.0.9'
743 local_filename = 'simplejson-%s.tar.gz' % version
744 urls = ('http://pypi.python.org/packages/source/s/simplejson/' +
745 local_filename,)
746 hex_sum = 'b5b26059adbe677b06c299bed30557fcb0c7df8c'
747
748 _build_and_install = ExternalPackage._build_and_install_from_package
749 _build_and_install_current_dir = (
750 ExternalPackage._build_and_install_current_dir_setup_py)
751
752
showard9adeb3c2010-02-03 20:29:36 +0000753class Httplib2Package(ExternalPackage):
754 version = '0.6.0'
755 local_filename = 'httplib2-%s.tar.gz' % version
756 urls = ('http://httplib2.googlecode.com/files/' + local_filename,)
757 hex_sum = '995344b2704826cc0d61a266e995b328d92445a5'
758
759 _build_and_install = ExternalPackage._build_and_install_from_package
760 _build_and_install_current_dir = (
761 ExternalPackage._build_and_install_current_dir_noegg)
762
763
mblighb155d0c2009-07-02 19:04:55 +0000764class GwtPackage(ExternalPackage):
765 """Fetch and extract a local copy of GWT used to build the frontend."""
766
showard61364422009-09-09 15:31:30 +0000767 version = '1.7.0'
mblighb155d0c2009-07-02 19:04:55 +0000768 local_filename = 'gwt-linux-%s.tar.bz2' % version
769 urls = ('http://google-web-toolkit.googlecode.com/files/' + local_filename,)
showard61364422009-09-09 15:31:30 +0000770 hex_sum = 'accb39506e1fa719ba166cf54451c91dafd9d456'
mblighb155d0c2009-07-02 19:04:55 +0000771 name = 'gwt'
showard61364422009-09-09 15:31:30 +0000772 about_filename = 'about.txt'
mblighb155d0c2009-07-02 19:04:55 +0000773 module_name = None # Not a Python module.
774
775
776 def is_needed(self, install_dir):
showard61364422009-09-09 15:31:30 +0000777 gwt_dir = os.path.join(install_dir, self.name)
778 about_file = os.path.join(install_dir, self.name, self.about_filename)
779
780 if not os.path.exists(gwt_dir) or not os.path.exists(about_file):
781 logging.info('gwt not installed for autotest')
782 return True
783
784 f = open(about_file, 'r')
785 version_line = f.readline()
786 f.close()
787
788 match = re.match(r'Google Web Toolkit (.*)', version_line)
789 if not match:
790 logging.info('did not find gwt version')
791 return True
792
793 logging.info('found gwt version %s', match.group(1))
794 return match.group(1) != self.version
mblighb155d0c2009-07-02 19:04:55 +0000795
796
797 def build_and_install(self, install_dir):
798 os.chdir(install_dir)
mblighdb530cc2009-08-24 22:19:13 +0000799 self._extract_compressed_package()
mblighb155d0c2009-07-02 19:04:55 +0000800 extracted_dir = self.local_filename[:-len('.tar.bz2')]
801 target_dir = os.path.join(install_dir, self.name)
showard61364422009-09-09 15:31:30 +0000802 if os.path.exists(target_dir):
803 shutil.rmtree(target_dir)
mblighb155d0c2009-07-02 19:04:55 +0000804 os.rename(extracted_dir, target_dir)
805 return True
806
807
mblighfa29d662009-02-05 00:44:26 +0000808if __name__ == '__main__':
809 sys.exit(main())