blob: 72085c4c61575e3bc1a2adb0b8f8be2e9d810f80 [file] [log] [blame]
jamesrenff6e5aa2010-02-12 00:46:40 +00001# Please keep this code python 2.4 compatible and stand alone.
2
3import logging, os, shutil, sys, tempfile, time, urllib2
4import subprocess, re
beepsd9153b52013-01-23 20:52:46 -08005from autotest_lib.client.common_lib import revision_control, utils
jamesrenff6e5aa2010-02-12 00:46:40 +00006
7_READ_SIZE = 64*1024
8_MAX_PACKAGE_SIZE = 100*1024*1024
9
10
11class Error(Exception):
12 """Local exception to be raised by code in this file."""
13
14class FetchError(Error):
15 """Failed to fetch a package from any of its listed URLs."""
16
17
18def _checksum_file(full_path):
19 """@returns The hex checksum of a file given its pathname."""
20 inputfile = open(full_path, 'rb')
21 try:
22 hex_sum = utils.hash('sha1', inputfile.read()).hexdigest()
23 finally:
24 inputfile.close()
25 return hex_sum
26
27
28def system(commandline):
Dan Shi57631dc2013-02-22 09:54:14 -080029 """Same as os.system(commandline) but logs the command first.
30
31 @param commandline: commandline to be called.
32 """
jamesrenff6e5aa2010-02-12 00:46:40 +000033 logging.info(commandline)
34 return os.system(commandline)
35
36
37def find_top_of_autotest_tree():
38 """@returns The full path to the top of the autotest directory tree."""
39 dirname = os.path.dirname(__file__)
40 autotest_dir = os.path.abspath(os.path.join(dirname, '..'))
41 return autotest_dir
42
43
44class ExternalPackage(object):
45 """
46 Defines an external package with URLs to fetch its sources from and
47 a build_and_install() method to unpack it, build it and install it
48 beneath our own autotest/site-packages directory.
49
50 Base Class. Subclass this to define packages.
51
52 Attributes:
53 @attribute urls - A tuple of URLs to try fetching the package from.
54 @attribute local_filename - A local filename to use when saving the
55 fetched package.
56 @attribute hex_sum - The hex digest (currently SHA1) of this package
57 to be used to verify its contents.
58 @attribute module_name - The installed python module name to be used for
59 for a version check. Defaults to the lower case class name with
60 the word Package stripped off.
61 @attribute version - The desired minimum package version.
62 @attribute os_requirements - A dictionary mapping a file pathname on the
63 the OS distribution to a likely name of a package the user
64 needs to install on their system in order to get this file.
65 @attribute name - Read only, the printable name of the package.
66 @attribute subclasses - This class attribute holds a list of all defined
67 subclasses. It is constructed dynamically using the metaclass.
68 """
69 subclasses = []
70 urls = ()
71 local_filename = None
72 hex_sum = None
73 module_name = None
74 version = None
75 os_requirements = None
76
77
78 class __metaclass__(type):
79 """Any time a subclass is defined, add it to our list."""
80 def __init__(mcs, name, bases, dict):
beepsd9153b52013-01-23 20:52:46 -080081 if name != 'ExternalPackage' and not name.startswith('_'):
jamesrenff6e5aa2010-02-12 00:46:40 +000082 mcs.subclasses.append(mcs)
83
84
85 def __init__(self):
86 self.verified_package = ''
87 if not self.module_name:
88 self.module_name = self.name.lower()
89 self.installed_version = ''
90
91
92 @property
93 def name(self):
94 """Return the class name with any trailing 'Package' stripped off."""
95 class_name = self.__class__.__name__
96 if class_name.endswith('Package'):
97 return class_name[:-len('Package')]
98 return class_name
99
100
101 def is_needed(self, unused_install_dir):
Dan Shi57631dc2013-02-22 09:54:14 -0800102 """@returns True if self.module_name needs to be built and installed.
103
104 @param unused_install_dir: install directory, not used.
105 """
jamesrenff6e5aa2010-02-12 00:46:40 +0000106 if not self.module_name or not self.version:
107 logging.warning('version and module_name required for '
108 'is_needed() check to work.')
109 return True
110 try:
111 module = __import__(self.module_name)
112 except ImportError, e:
jamesrenca2a9002010-04-20 22:33:22 +0000113 logging.info("%s isn't present. Will install.", self.module_name)
jamesrenff6e5aa2010-02-12 00:46:40 +0000114 return True
115 self.installed_version = self._get_installed_version_from_module(module)
116 logging.info('imported %s version %s.', self.module_name,
117 self.installed_version)
Dale Curtis74a314b2011-06-23 14:55:46 -0700118 if hasattr(self, 'minimum_version'):
119 return self.minimum_version > self.installed_version
120 else:
121 return self.version > self.installed_version
jamesrenff6e5aa2010-02-12 00:46:40 +0000122
123
124 def _get_installed_version_from_module(self, module):
125 """Ask our module its version string and return it or '' if unknown."""
126 try:
127 return module.__version__
128 except AttributeError:
129 logging.error('could not get version from %s', module)
130 return ''
131
132
133 def _build_and_install(self, install_dir):
134 """Subclasses MUST provide their own implementation."""
135 raise NotImplementedError
136
137
138 def _build_and_install_current_dir(self, install_dir):
139 """
140 Subclasses that use _build_and_install_from_package() MUST provide
141 their own implementation of this method.
142 """
143 raise NotImplementedError
144
145
146 def build_and_install(self, install_dir):
147 """
148 Builds and installs the package. It must have been fetched already.
149
150 @param install_dir - The package installation directory. If it does
151 not exist it will be created.
152 """
153 if not self.verified_package:
154 raise Error('Must call fetch() first. - %s' % self.name)
155 self._check_os_requirements()
156 return self._build_and_install(install_dir)
157
158
159 def _check_os_requirements(self):
160 if not self.os_requirements:
161 return
162 failed = False
163 for file_name, package_name in self.os_requirements.iteritems():
164 if not os.path.exists(file_name):
165 failed = True
166 logging.error('File %s not found, %s needs it.',
167 file_name, self.name)
168 logging.error('Perhaps you need to install something similar '
169 'to the %s package for OS first.', package_name)
170 if failed:
171 raise Error('Missing OS requirements for %s. (see above)' %
172 self.name)
173
174
175 def _build_and_install_current_dir_setup_py(self, install_dir):
176 """For use as a _build_and_install_current_dir implementation."""
177 egg_path = self._build_egg_using_setup_py(setup_py='setup.py')
178 if not egg_path:
179 return False
180 return self._install_from_egg(install_dir, egg_path)
181
182
183 def _build_and_install_current_dir_setupegg_py(self, install_dir):
184 """For use as a _build_and_install_current_dir implementation."""
185 egg_path = self._build_egg_using_setup_py(setup_py='setupegg.py')
186 if not egg_path:
187 return False
188 return self._install_from_egg(install_dir, egg_path)
189
190
191 def _build_and_install_current_dir_noegg(self, install_dir):
192 if not self._build_using_setup_py():
193 return False
194 return self._install_using_setup_py_and_rsync(install_dir)
195
196
197 def _build_and_install_from_package(self, install_dir):
198 """
199 This method may be used as a _build_and_install() implementation
200 for subclasses if they implement _build_and_install_current_dir().
201
202 Extracts the .tar.gz file, chdirs into the extracted directory
203 (which is assumed to match the tar filename) and calls
204 _build_and_isntall_current_dir from there.
205
206 Afterwards the build (regardless of failure) extracted .tar.gz
207 directory is cleaned up.
208
209 @returns True on success, False otherwise.
210
211 @raises OSError If the expected extraction directory does not exist.
212 """
213 self._extract_compressed_package()
214 if self.verified_package.endswith('.tar.gz'):
215 extension = '.tar.gz'
216 elif self.verified_package.endswith('.tar.bz2'):
217 extension = '.tar.bz2'
218 elif self.verified_package.endswith('.zip'):
219 extension = '.zip'
220 else:
221 raise Error('Unexpected package file extension on %s' %
222 self.verified_package)
223 os.chdir(os.path.dirname(self.verified_package))
224 os.chdir(self.local_filename[:-len(extension)])
225 extracted_dir = os.getcwd()
226 try:
227 return self._build_and_install_current_dir(install_dir)
228 finally:
229 os.chdir(os.path.join(extracted_dir, '..'))
230 shutil.rmtree(extracted_dir)
231
232
233 def _extract_compressed_package(self):
234 """Extract the fetched compressed .tar or .zip within its directory."""
235 if not self.verified_package:
236 raise Error('Package must have been fetched first.')
237 os.chdir(os.path.dirname(self.verified_package))
238 if self.verified_package.endswith('gz'):
239 status = system("tar -xzf '%s'" % self.verified_package)
240 elif self.verified_package.endswith('bz2'):
241 status = system("tar -xjf '%s'" % self.verified_package)
242 elif self.verified_package.endswith('zip'):
243 status = system("unzip '%s'" % self.verified_package)
244 else:
245 raise Error('Unknown compression suffix on %s.' %
246 self.verified_package)
247 if status:
248 raise Error('tar failed with %s' % (status,))
249
250
251 def _build_using_setup_py(self, setup_py='setup.py'):
252 """
253 Assuming the cwd is the extracted python package, execute a simple
254 python setup.py build.
255
256 @param setup_py - The name of the setup.py file to execute.
257
258 @returns True on success, False otherwise.
259 """
260 if not os.path.exists(setup_py):
261 raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
262 status = system("'%s' %s build" % (sys.executable, setup_py))
263 if status:
Dan Shi57631dc2013-02-22 09:54:14 -0800264 logging.error('%s build failed.', self.name)
jamesrenff6e5aa2010-02-12 00:46:40 +0000265 return False
266 return True
267
268
269 def _build_egg_using_setup_py(self, setup_py='setup.py'):
270 """
271 Assuming the cwd is the extracted python package, execute a simple
272 python setup.py bdist_egg.
273
274 @param setup_py - The name of the setup.py file to execute.
275
276 @returns The relative path to the resulting egg file or '' on failure.
277 """
278 if not os.path.exists(setup_py):
279 raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
280 egg_subdir = 'dist'
281 if os.path.isdir(egg_subdir):
282 shutil.rmtree(egg_subdir)
283 status = system("'%s' %s bdist_egg" % (sys.executable, setup_py))
284 if status:
285 logging.error('bdist_egg of setuptools failed.')
286 return ''
287 # I've never seen a bdist_egg lay multiple .egg files.
288 for filename in os.listdir(egg_subdir):
289 if filename.endswith('.egg'):
290 return os.path.join(egg_subdir, filename)
291
292
293 def _install_from_egg(self, install_dir, egg_path):
294 """
295 Install a module from an egg file by unzipping the necessary parts
296 into install_dir.
297
298 @param install_dir - The installation directory.
299 @param egg_path - The pathname of the egg file.
300 """
301 status = system("unzip -q -o -d '%s' '%s'" % (install_dir, egg_path))
302 if status:
303 logging.error('unzip of %s failed', egg_path)
304 return False
305 egg_info = os.path.join(install_dir, 'EGG-INFO')
306 if os.path.isdir(egg_info):
307 shutil.rmtree(egg_info)
308 return True
309
310
311 def _get_temp_dir(self):
312 return tempfile.mkdtemp(dir='/var/tmp')
313
314
315 def _site_packages_path(self, temp_dir):
316 # This makes assumptions about what python setup.py install
317 # does when given a prefix. Is this always correct?
318 python_xy = 'python%s' % sys.version[:3]
319 return os.path.join(temp_dir, 'lib', python_xy, 'site-packages')
320
321
beepsd9153b52013-01-23 20:52:46 -0800322 def _rsync (self, temp_site_dir, install_dir):
323 """Rsync contents. """
324 status = system("rsync -r '%s/' '%s/'" %
325 (os.path.normpath(temp_site_dir),
326 os.path.normpath(install_dir)))
327 if status:
328 logging.error('%s rsync to install_dir failed.', self.name)
329 return False
330 return True
331
332
jamesrenff6e5aa2010-02-12 00:46:40 +0000333 def _install_using_setup_py_and_rsync(self, install_dir,
334 setup_py='setup.py',
335 temp_dir=None):
336 """
337 Assuming the cwd is the extracted python package, execute a simple:
338
339 python setup.py install --prefix=BLA
340
341 BLA will be a temporary directory that everything installed will
342 be picked out of and rsynced to the appropriate place under
343 install_dir afterwards.
344
345 Afterwards, it deconstructs the extra lib/pythonX.Y/site-packages/
346 directory tree that setuptools created and moves all installed
347 site-packages directly up into install_dir itself.
348
349 @param install_dir the directory for the install to happen under.
350 @param setup_py - The name of the setup.py file to execute.
351
352 @returns True on success, False otherwise.
353 """
354 if not os.path.exists(setup_py):
355 raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
356
357 if temp_dir is None:
358 temp_dir = self._get_temp_dir()
359
360 try:
361 status = system("'%s' %s install --no-compile --prefix='%s'"
362 % (sys.executable, setup_py, temp_dir))
363 if status:
Dan Shi57631dc2013-02-22 09:54:14 -0800364 logging.error('%s install failed.', self.name)
jamesrenff6e5aa2010-02-12 00:46:40 +0000365 return False
366
367 if os.path.isdir(os.path.join(temp_dir, 'lib')):
368 # NOTE: This ignores anything outside of the lib/ dir that
369 # was installed.
370 temp_site_dir = self._site_packages_path(temp_dir)
371 else:
372 temp_site_dir = temp_dir
373
beepsd9153b52013-01-23 20:52:46 -0800374 return self._rsync(temp_site_dir, install_dir)
jamesrenff6e5aa2010-02-12 00:46:40 +0000375 finally:
376 shutil.rmtree(temp_dir)
377
378
379
380 def _build_using_make(self, install_dir):
381 """Build the current package using configure/make.
382
383 @returns True on success, False otherwise.
384 """
385 install_prefix = os.path.join(install_dir, 'usr', 'local')
386 status = system('./configure --prefix=%s' % install_prefix)
387 if status:
388 logging.error('./configure failed for %s', self.name)
389 return False
390 status = system('make')
391 if status:
392 logging.error('make failed for %s', self.name)
393 return False
394 status = system('make check')
395 if status:
396 logging.error('make check failed for %s', self.name)
397 return False
398 return True
399
400
401 def _install_using_make(self):
402 """Install the current package using make install.
403
404 Assumes the install path was set up while running ./configure (in
405 _build_using_make()).
406
407 @returns True on success, False otherwise.
408 """
409 status = system('make install')
410 return status == 0
411
412
413 def fetch(self, dest_dir):
414 """
415 Fetch the package from one its URLs and save it in dest_dir.
416
417 If the the package already exists in dest_dir and the checksum
418 matches this code will not fetch it again.
419
420 Sets the 'verified_package' attribute with the destination pathname.
421
422 @param dest_dir - The destination directory to save the local file.
423 If it does not exist it will be created.
424
425 @returns A boolean indicating if we the package is now in dest_dir.
426 @raises FetchError - When something unexpected happens.
427 """
428 if not os.path.exists(dest_dir):
429 os.makedirs(dest_dir)
430 local_path = os.path.join(dest_dir, self.local_filename)
431
432 # If the package exists, verify its checksum and be happy if it is good.
433 if os.path.exists(local_path):
434 actual_hex_sum = _checksum_file(local_path)
435 if self.hex_sum == actual_hex_sum:
436 logging.info('Good checksum for existing %s package.',
437 self.name)
438 self.verified_package = local_path
439 return True
440 logging.warning('Bad checksum for existing %s package. '
441 'Re-downloading', self.name)
442 os.rename(local_path, local_path + '.wrong-checksum')
443
444 # Download the package from one of its urls, rejecting any if the
445 # checksum does not match.
446 for url in self.urls:
447 logging.info('Fetching %s', url)
448 try:
449 url_file = urllib2.urlopen(url)
450 except (urllib2.URLError, EnvironmentError):
451 logging.warning('Could not fetch %s package from %s.',
452 self.name, url)
453 continue
Dan Shi57631dc2013-02-22 09:54:14 -0800454
jamesrenff6e5aa2010-02-12 00:46:40 +0000455 data_length = int(url_file.info().get('Content-Length',
456 _MAX_PACKAGE_SIZE))
457 if data_length <= 0 or data_length > _MAX_PACKAGE_SIZE:
458 raise FetchError('%s from %s fails Content-Length %d '
459 'sanity check.' % (self.name, url,
460 data_length))
461 checksum = utils.hash('sha1')
462 total_read = 0
463 output = open(local_path, 'wb')
464 try:
465 while total_read < data_length:
466 data = url_file.read(_READ_SIZE)
467 if not data:
468 break
469 output.write(data)
470 checksum.update(data)
471 total_read += len(data)
472 finally:
473 output.close()
474 if self.hex_sum != checksum.hexdigest():
475 logging.warning('Bad checksum for %s fetched from %s.',
476 self.name, url)
477 logging.warning('Got %s', checksum.hexdigest())
478 logging.warning('Expected %s', self.hex_sum)
479 os.unlink(local_path)
480 continue
481 logging.info('Good checksum.')
482 self.verified_package = local_path
483 return True
484 else:
485 return False
486
487
488# NOTE: This class definition must come -before- all other ExternalPackage
489# classes that need to use this version of setuptools so that is is inserted
490# into the ExternalPackage.subclasses list before them.
491class SetuptoolsPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800492 """setuptools package"""
jamesrenff6e5aa2010-02-12 00:46:40 +0000493 # For all known setuptools releases a string compare works for the
494 # version string. Hopefully they never release a 0.10. (Their own
495 # version comparison code would break if they did.)
Dale Curtis74a314b2011-06-23 14:55:46 -0700496 # Any system with setuptools > 0.6 is fine. If none installed, then
497 # try to install the latest found on the upstream.
498 minimum_version = '0.6'
Eric Li6f27d4f2010-09-29 10:55:17 -0700499 version = '0.6c11'
jamesrenff6e5aa2010-02-12 00:46:40 +0000500 urls = ('http://pypi.python.org/packages/source/s/setuptools/'
501 'setuptools-%s.tar.gz' % (version,),)
502 local_filename = 'setuptools-%s.tar.gz' % version
Eric Li6f27d4f2010-09-29 10:55:17 -0700503 hex_sum = '8d1ad6384d358c547c50c60f1bfdb3362c6c4a7d'
jamesrenff6e5aa2010-02-12 00:46:40 +0000504
505 SUDO_SLEEP_DELAY = 15
506
507
508 def _build_and_install(self, install_dir):
509 """Install setuptools on the system."""
510 logging.info('NOTE: setuptools install does not use install_dir.')
511 return self._build_and_install_from_package(install_dir)
512
513
514 def _build_and_install_current_dir(self, install_dir):
515 egg_path = self._build_egg_using_setup_py()
516 if not egg_path:
517 return False
518
519 print '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
520 print 'About to run sudo to install setuptools', self.version
521 print 'on your system for use by', sys.executable, '\n'
522 print '!! ^C within', self.SUDO_SLEEP_DELAY, 'seconds to abort.\n'
523 time.sleep(self.SUDO_SLEEP_DELAY)
524
525 # Copy the egg to the local filesystem /var/tmp so that root can
526 # access it properly (avoid NFS squashroot issues).
527 temp_dir = self._get_temp_dir()
528 try:
529 shutil.copy(egg_path, temp_dir)
530 egg_name = os.path.split(egg_path)[1]
531 temp_egg = os.path.join(temp_dir, egg_name)
532 p = subprocess.Popen(['sudo', '/bin/sh', temp_egg],
533 stdout=subprocess.PIPE)
534 regex = re.compile('Copying (.*?) to (.*?)\n')
535 match = regex.search(p.communicate()[0])
536 status = p.wait()
537
538 if match:
539 compiled = os.path.join(match.group(2), match.group(1))
540 os.system("sudo chmod a+r '%s'" % compiled)
541 finally:
542 shutil.rmtree(temp_dir)
543
544 if status:
545 logging.error('install of setuptools from egg failed.')
546 return False
547 return True
548
549
550class MySQLdbPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800551 """mysql package, used in scheduler."""
jamesrenff6e5aa2010-02-12 00:46:40 +0000552 module_name = 'MySQLdb'
553 version = '1.2.2'
554 urls = ('http://downloads.sourceforge.net/project/mysql-python/'
555 'mysql-python/%(version)s/MySQL-python-%(version)s.tar.gz'
556 % dict(version=version),)
557 local_filename = 'MySQL-python-%s.tar.gz' % version
558 hex_sum = '945a04773f30091ad81743f9eb0329a3ee3de383'
559
560 _build_and_install_current_dir = (
561 ExternalPackage._build_and_install_current_dir_setup_py)
562
563
564 def _build_and_install(self, install_dir):
565 if not os.path.exists('/usr/bin/mysql_config'):
566 logging.error('You need to install /usr/bin/mysql_config')
567 logging.error('On Ubuntu or Debian based systems use this: '
568 'sudo apt-get install libmysqlclient15-dev')
569 return False
570 return self._build_and_install_from_package(install_dir)
571
572
573class DjangoPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800574 """django package."""
Dale Curtis74a314b2011-06-23 14:55:46 -0700575 version = '1.3'
jamesrenff6e5aa2010-02-12 00:46:40 +0000576 local_filename = 'Django-%s.tar.gz' % version
577 urls = ('http://www.djangoproject.com/download/%s/tarball/' % version,)
Dale Curtis74a314b2011-06-23 14:55:46 -0700578 hex_sum = 'f8814d5e1412bb932318db5130260da5bf053ff7'
jamesrenff6e5aa2010-02-12 00:46:40 +0000579
580 _build_and_install = ExternalPackage._build_and_install_from_package
581 _build_and_install_current_dir = (
582 ExternalPackage._build_and_install_current_dir_noegg)
583
584
585 def _get_installed_version_from_module(self, module):
586 try:
587 return module.get_version().split()[0]
588 except AttributeError:
589 return '0.9.6'
590
591
592
593class NumpyPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800594 """numpy package, required by matploglib."""
Scott Zawalskic2c9a012013-03-06 09:13:26 -0500595 version = '1.7.0'
jamesrenff6e5aa2010-02-12 00:46:40 +0000596 local_filename = 'numpy-%s.tar.gz' % version
597 urls = ('http://downloads.sourceforge.net/project/numpy/NumPy/%(version)s/'
598 'numpy-%(version)s.tar.gz' % dict(version=version),)
Scott Zawalskic2c9a012013-03-06 09:13:26 -0500599 hex_sum = 'ba328985f20390b0f969a5be2a6e1141d5752cf9'
jamesrenff6e5aa2010-02-12 00:46:40 +0000600
601 _build_and_install = ExternalPackage._build_and_install_from_package
602 _build_and_install_current_dir = (
603 ExternalPackage._build_and_install_current_dir_setupegg_py)
604
605
jamesrenff6e5aa2010-02-12 00:46:40 +0000606class MatplotlibPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800607 """
608 matplotlib package
609
610 This requires numpy so it must be declared after numpy to guarantee that
611 it is already installed.
612 """
jamesrenff6e5aa2010-02-12 00:46:40 +0000613 version = '0.98.5.3'
614 short_version = '0.98.5'
615 local_filename = 'matplotlib-%s.tar.gz' % version
616 urls = ('http://downloads.sourceforge.net/project/matplotlib/matplotlib/'
617 'matplotlib-%s/matplotlib-%s.tar.gz' % (short_version, version),)
618 hex_sum = '2f6c894cf407192b3b60351bcc6468c0385d47b6'
619 os_requirements = {'/usr/include/ft2build.h': 'libfreetype6-dev',
620 '/usr/include/png.h': 'libpng12-dev'}
621
622 _build_and_install = ExternalPackage._build_and_install_from_package
623 _build_and_install_current_dir = (
624 ExternalPackage._build_and_install_current_dir_setupegg_py)
625
626
627class AtForkPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800628 """atfork package"""
jamesrenff6e5aa2010-02-12 00:46:40 +0000629 version = '0.1.2'
630 local_filename = 'atfork-%s.zip' % version
631 urls = ('http://python-atfork.googlecode.com/files/' + local_filename,)
632 hex_sum = '5baa64c73e966b57fa797040585c760c502dc70b'
633
634 _build_and_install = ExternalPackage._build_and_install_from_package
635 _build_and_install_current_dir = (
636 ExternalPackage._build_and_install_current_dir_noegg)
637
638
639class ParamikoPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800640 """paramiko package"""
jamesrenff6e5aa2010-02-12 00:46:40 +0000641 version = '1.7.5'
642 local_filename = 'paramiko-%s.tar.gz' % version
Eric Li861b2d52011-02-04 14:50:35 -0800643 urls = ('http://www.lag.net/paramiko/download/' + local_filename,
644 'ftp://mirrors.kernel.org/gentoo/distfiles/' + local_filename,)
jamesrenff6e5aa2010-02-12 00:46:40 +0000645 hex_sum = '592be7a08290070b71da63a8e6f28a803399e5c5'
646
647
648 _build_and_install = ExternalPackage._build_and_install_from_package
649
650
651 def _check_for_pycrypto(self):
652 # NOTE(gps): Linux distros have better python-crypto packages than we
653 # can easily get today via a wget due to the library's age and staleness
654 # yet many security and behavior bugs are fixed by patches that distros
655 # already apply. PyCrypto has a new active maintainer in 2009. Once a
656 # new release is made (http://pycrypto.org/) we should add an installer.
657 try:
658 import Crypto
659 except ImportError:
660 logging.error('Please run "sudo apt-get install python-crypto" '
661 'or your Linux distro\'s equivalent.')
662 return False
663 return True
664
665
666 def _build_and_install_current_dir(self, install_dir):
667 if not self._check_for_pycrypto():
668 return False
669 # paramiko 1.7.4 doesn't require building, it is just a module directory
670 # that we can rsync into place directly.
671 if not os.path.isdir('paramiko'):
672 raise Error('no paramiko directory in %s.' % os.getcwd())
673 status = system("rsync -r 'paramiko' '%s/'" % install_dir)
674 if status:
Dan Shi57631dc2013-02-22 09:54:14 -0800675 logging.error('%s rsync to install_dir failed.', self.name)
jamesrenff6e5aa2010-02-12 00:46:40 +0000676 return False
677 return True
678
679
Chris Masonebafbbb02012-05-16 13:41:36 -0700680class RequestsPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800681 """requests package"""
Chris Masonebafbbb02012-05-16 13:41:36 -0700682 version = '0.11.2'
683 local_filename = 'requests-%s.tar.gz' % version
684 urls = ('http://pypi.python.org/packages/source/r/requests/' +
685 local_filename,)
686 hex_sum = '00a49e8bd6dd8955acf6f6269d1b85f50c70b712'
687
688 _build_and_install = ExternalPackage._build_and_install_from_package
689 _build_and_install_current_dir = (
690 ExternalPackage._build_and_install_current_dir_setup_py)
691
692
jamesrenff6e5aa2010-02-12 00:46:40 +0000693class SimplejsonPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800694 """simplejson package"""
jamesrenff6e5aa2010-02-12 00:46:40 +0000695 version = '2.0.9'
696 local_filename = 'simplejson-%s.tar.gz' % version
697 urls = ('http://pypi.python.org/packages/source/s/simplejson/' +
698 local_filename,)
699 hex_sum = 'b5b26059adbe677b06c299bed30557fcb0c7df8c'
700
701 _build_and_install = ExternalPackage._build_and_install_from_package
702 _build_and_install_current_dir = (
703 ExternalPackage._build_and_install_current_dir_setup_py)
704
705
706class Httplib2Package(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800707 """httplib2 package"""
jamesrenff6e5aa2010-02-12 00:46:40 +0000708 version = '0.6.0'
709 local_filename = 'httplib2-%s.tar.gz' % version
710 urls = ('http://httplib2.googlecode.com/files/' + local_filename,)
711 hex_sum = '995344b2704826cc0d61a266e995b328d92445a5'
712
713 def _get_installed_version_from_module(self, module):
714 # httplib2 doesn't contain a proper version
715 return self.version
716
717 _build_and_install = ExternalPackage._build_and_install_from_package
718 _build_and_install_current_dir = (
719 ExternalPackage._build_and_install_current_dir_noegg)
720
721
722class GwtPackage(ExternalPackage):
723 """Fetch and extract a local copy of GWT used to build the frontend."""
724
Dale Curtis74a314b2011-06-23 14:55:46 -0700725 version = '2.3.0'
jamesren012d0322010-04-30 20:21:29 +0000726 local_filename = 'gwt-%s.zip' % version
jamesrenff6e5aa2010-02-12 00:46:40 +0000727 urls = ('http://google-web-toolkit.googlecode.com/files/' + local_filename,)
Dale Curtis74a314b2011-06-23 14:55:46 -0700728 hex_sum = 'd51fce9166e6b31349659ffca89baf93e39bc84b'
jamesrenff6e5aa2010-02-12 00:46:40 +0000729 name = 'gwt'
730 about_filename = 'about.txt'
731 module_name = None # Not a Python module.
732
733
734 def is_needed(self, install_dir):
735 gwt_dir = os.path.join(install_dir, self.name)
736 about_file = os.path.join(install_dir, self.name, self.about_filename)
737
738 if not os.path.exists(gwt_dir) or not os.path.exists(about_file):
739 logging.info('gwt not installed for autotest')
740 return True
741
742 f = open(about_file, 'r')
743 version_line = f.readline()
744 f.close()
745
746 match = re.match(r'Google Web Toolkit (.*)', version_line)
747 if not match:
748 logging.info('did not find gwt version')
749 return True
750
751 logging.info('found gwt version %s', match.group(1))
752 return match.group(1) != self.version
753
754
jamesrenca2a9002010-04-20 22:33:22 +0000755 def _build_and_install(self, install_dir):
jamesrenff6e5aa2010-02-12 00:46:40 +0000756 os.chdir(install_dir)
757 self._extract_compressed_package()
jamesren012d0322010-04-30 20:21:29 +0000758 extracted_dir = self.local_filename[:-len('.zip')]
jamesrenff6e5aa2010-02-12 00:46:40 +0000759 target_dir = os.path.join(install_dir, self.name)
760 if os.path.exists(target_dir):
761 shutil.rmtree(target_dir)
762 os.rename(extracted_dir, target_dir)
763 return True
764
765
Mike Trutyddd44b22011-04-14 15:38:56 -0700766class GVizAPIPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800767 """gviz package"""
Alex Miller0e1217c2013-02-22 10:11:08 -0800768 module_name = 'gviz_api'
Mike Trutyddd44b22011-04-14 15:38:56 -0700769 version = '1.7.0'
770 url_filename = 'gviz_api_py-%s.tar.gz' % version
771 local_filename = 'google-visualization-python.tar.gz'
772 urls = ('http://google-visualization-python.googlecode.com/files/%s' % (
773 url_filename),)
774 hex_sum = 'cd9a0fb4ca5c4f86c0d85756f501fd54ccf492d2'
775
776 _build_and_install = ExternalPackage._build_and_install_from_package
777 _build_and_install_current_dir = (
778 ExternalPackage._build_and_install_current_dir_noegg)
beepsd9153b52013-01-23 20:52:46 -0800779
Alex Miller0e1217c2013-02-22 10:11:08 -0800780 def _get_installed_version_from_module(self, module):
781 # gviz doesn't contain a proper version
782 return self.version
783
beepsd9153b52013-01-23 20:52:46 -0800784
Scott Zawalski2a6acf82013-02-11 16:18:27 -0500785class StatsdPackage(ExternalPackage):
Dan Shi57631dc2013-02-22 09:54:14 -0800786 """python-statsd package"""
Alex Milleree10e932013-03-18 18:01:46 -0700787 version = '1.5.8'
Scott Zawalski2a6acf82013-02-11 16:18:27 -0500788 url_filename = 'python-statsd-%s.tar.gz' % version
789 local_filename = url_filename
790 urls = ('http://pypi.python.org/packages/source/p/python-statsd/%s' % (
791 url_filename),)
Alex Milleree10e932013-03-18 18:01:46 -0700792 hex_sum = '50eccab74ca88884297954497f85039e5a2e732c'
Scott Zawalski2a6acf82013-02-11 16:18:27 -0500793
794 _build_and_install = ExternalPackage._build_and_install_from_package
795 _build_and_install_current_dir = (
796 ExternalPackage._build_and_install_current_dir_setup_py)
797
798
beeps93bef482013-02-04 16:24:22 -0800799class GdataPackage(ExternalPackage):
800 """
801 Pulls the GData library, giving us an API to query tracker.
802 """
803
804 version = '2.0.14'
805 url_filename = 'gdata-%s.tar.gz' % version
806 local_filename = url_filename
807 urls = ('http://gdata-python-client.googlecode.com/files/%s' % (
808 url_filename),)
809 hex_sum = '5eed0e01ab931e3f706ec544fc8f06ecac384e91'
810
811 _build_and_install = ExternalPackage._build_and_install_from_package
812 _build_and_install_current_dir = (
813 ExternalPackage._build_and_install_current_dir_noegg)
814
Alex Miller0e1217c2013-02-22 10:11:08 -0800815 def _get_installed_version_from_module(self, module):
816 # gdata doesn't contain a proper version
817 return self.version
818
beeps93bef482013-02-04 16:24:22 -0800819
Alex Millere7b6e8b2013-02-13 17:34:04 -0800820class DnsPythonPackage(ExternalPackage):
821 """
822 dns module
823
824 Used in unittests.
825 """
826 module_name = 'dns'
827 version = '1.3.5'
828 url_filename = 'dnspython-%s.tar.gz' % version
829 local_filename = url_filename
830 urls = ('http://www.dnspython.org/kits/%s/%s' % (
Dan Shi57631dc2013-02-22 09:54:14 -0800831 version, url_filename),)
832
Alex Millere7b6e8b2013-02-13 17:34:04 -0800833 hex_sum = '06314dad339549613435470c6add992910e26e5d'
834
835 _build_and_install = ExternalPackage._build_and_install_from_package
836 _build_and_install_current_dir = (
837 ExternalPackage._build_and_install_current_dir_noegg)
838
839 def _get_installed_version_from_module(self, module):
840 """Ask our module its version string and return it or '' if unknown."""
841 try:
842 __import__(self.module_name + '.version')
843 return module.version.version
844 except AttributeError:
845 logging.error('could not get version from %s', module)
846 return ''
847
848
849class PyudevPackage(ExternalPackage):
850 """
851 pyudev module
852
853 Used in unittests.
854 """
855 version = '0.16.1'
856 url_filename = 'pyudev-%s.tar.gz' % version
857 local_filename = url_filename
858 urls = ('http://pypi.python.org/packages/source/p/pyudev/%s' % (
859 url_filename),)
860 hex_sum = 'b36bc5c553ce9b56d32a5e45063a2c88156771c0'
861
862 _build_and_install = ExternalPackage._build_and_install_from_package
863 _build_and_install_current_dir = (
864 ExternalPackage._build_and_install_current_dir_setup_py)
865
866
867class PyMoxPackage(ExternalPackage):
868 """
869 mox module
870
871 Used in unittests.
872 """
873 module_name = 'mox'
874 version = '0.5.3'
875 url_filename = 'mox-%s.tar.gz' % version
876 local_filename = url_filename
877 urls = ('http://pypi.python.org/packages/source/m/mox/%s' % (
878 url_filename),)
879 hex_sum = '1c502d2c0a8aefbba2c7f385a83d33e7d822452a'
880
881 _build_and_install = ExternalPackage._build_and_install_from_package
882 _build_and_install_current_dir = (
883 ExternalPackage._build_and_install_current_dir_noegg)
884
885 def _get_installed_version_from_module(self, module):
886 # mox doesn't contain a proper version
887 return self.version
888
889
beepsd9153b52013-01-23 20:52:46 -0800890class _ExternalGitRepo(ExternalPackage):
891 """
892 Parent class for any package which needs to pull a git repo.
893
894 This class inherits from ExternalPackage only so we can sync git
895 repos through the build_externals script. We do not reuse any of
896 ExternalPackage's other methods. Any package that needs a git repo
897 should subclass this and override build_and_install or fetch as
898 they see appropriate.
899 """
900
901 os_requirements = {'/usr/bin/git' : 'git-core'}
902
903 def is_needed(self, unused_install_dir):
904 """Tell build_externals that we need to fetch."""
905 # TODO(beeps): check if we're already upto date.
906 return True
907
908
909 def build_and_install(self, unused_install_dir):
910 """
911 Fall through method to install a package.
912
913 Overwritten in base classes to pull a git repo.
914 """
915 raise NotImplementedError
916
917
918 def fetch(self, unused_dest_dir):
919 """Fallthrough method to fetch a package."""
920 return True
921
922
923class HdctoolsRepo(_ExternalGitRepo):
924 """Clones or updates the hdctools repo."""
925
Alex Miller0e1217c2013-02-22 10:11:08 -0800926 module_name = 'servo'
beepsd9153b52013-01-23 20:52:46 -0800927 temp_hdctools_dir = tempfile.mktemp(suffix='hdctools')
928 _GIT_URL = ('https://git.chromium.org/git/'
929 'chromiumos/third_party/hdctools')
930
931 def fetch(self, unused_dest_dir):
932 """
933 Fetch repo to a temporary location.
934
935 We use an intermediate temp directory to stage our
936 installation because we only care about the servo package.
937 If we can't get at the top commit hash after fetching
938 something is wrong. This can happen when we've cloned/pulled
939 an empty repo. Not something we expect to do.
940
941 @parma unused_dest_dir: passed in because we inherit from
942 ExternalPackage.
943
944 @return: True if repo sync was successful.
945 """
946 git_repo = revision_control.GitRepo(
947 self.temp_hdctools_dir,
948 self._GIT_URL,
949 None,
950 self.temp_hdctools_dir)
951 git_repo.fetch_and_reset_or_clone()
952
953 if git_repo.get_latest_commit_hash():
954 return True
955 return False
956
957
958 def build_and_install(self, install_dir):
959 """Reach into the hdctools repo and rsync only the servo directory."""
960
961 servo_dir = os.path.join(self.temp_hdctools_dir, 'servo')
962 if not os.path.exists(servo_dir):
963 return False
964
965 rv = self._rsync(servo_dir, os.path.join(install_dir, 'servo'))
966 shutil.rmtree(self.temp_hdctools_dir)
967 return rv
968
969
970class ChromiteRepo(_ExternalGitRepo):
971 """Clones or updates the chromite repo."""
972
973 _GIT_URL = ('https://git.chromium.org/git/'
974 'chromiumos/chromite')
975
976 def build_and_install(self, install_dir):
977 """
978 Clone if the repo isn't initialized, pull clean bits if it is.
979
980 Unlike it's hdctools counterpart the chromite repo clones master
981 directly into site-packages. It doesn't use an intermediate temp
Chris Sosa1bf41c42013-02-27 11:34:23 -0800982 directory because it doesn't need installation.
beepsd9153b52013-01-23 20:52:46 -0800983
984 @param install_dir: destination directory for chromite installation.
985 """
986 local_chromite_dir = os.path.join(install_dir, 'chromite')
Chris Sosa1bf41c42013-02-27 11:34:23 -0800987 git_repo = revision_control.GitRepo(local_chromite_dir, self._GIT_URL,
988 abs_work_tree=local_chromite_dir)
989 git_repo.fetch_and_reset_or_clone()
990
991 if git_repo.get_latest_commit_hash():
992 return True
993 return False
994
995
996class DevServerRepo(_ExternalGitRepo):
997 """Clones or updates the chromite repo."""
998
999 _GIT_URL = ('https://git.chromium.org/git/'
1000 'chromiumos/platform/dev-util')
1001
1002 def build_and_install(self, install_dir):
1003 """
1004 Clone if the repo isn't initialized, pull clean bits if it is.
1005
1006 Unlike it's hdctools counterpart the dev-util repo clones master
1007 directly into site-packages. It doesn't use an intermediate temp
1008 directory because it doesn't need installation.
1009
1010 @param install_dir: destination directory for chromite installation.
1011 """
1012 local_devserver_dir = os.path.join(install_dir, 'devserver')
1013 git_repo = revision_control.GitRepo(local_devserver_dir, self._GIT_URL,
1014 abs_work_tree=local_devserver_dir)
beepsd9153b52013-01-23 20:52:46 -08001015 git_repo.fetch_and_reset_or_clone()
1016
1017 if git_repo.get_latest_commit_hash():
1018 return True
1019 return False