blob: 360e23ecad45e845e778238b2f10c4c6ed1de8fd [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):
29 """Same as os.system(commandline) but logs the command first."""
30 logging.info(commandline)
31 return os.system(commandline)
32
33
34def find_top_of_autotest_tree():
35 """@returns The full path to the top of the autotest directory tree."""
36 dirname = os.path.dirname(__file__)
37 autotest_dir = os.path.abspath(os.path.join(dirname, '..'))
38 return autotest_dir
39
40
41class ExternalPackage(object):
42 """
43 Defines an external package with URLs to fetch its sources from and
44 a build_and_install() method to unpack it, build it and install it
45 beneath our own autotest/site-packages directory.
46
47 Base Class. Subclass this to define packages.
48
49 Attributes:
50 @attribute urls - A tuple of URLs to try fetching the package from.
51 @attribute local_filename - A local filename to use when saving the
52 fetched package.
53 @attribute hex_sum - The hex digest (currently SHA1) of this package
54 to be used to verify its contents.
55 @attribute module_name - The installed python module name to be used for
56 for a version check. Defaults to the lower case class name with
57 the word Package stripped off.
58 @attribute version - The desired minimum package version.
59 @attribute os_requirements - A dictionary mapping a file pathname on the
60 the OS distribution to a likely name of a package the user
61 needs to install on their system in order to get this file.
62 @attribute name - Read only, the printable name of the package.
63 @attribute subclasses - This class attribute holds a list of all defined
64 subclasses. It is constructed dynamically using the metaclass.
65 """
66 subclasses = []
67 urls = ()
68 local_filename = None
69 hex_sum = None
70 module_name = None
71 version = None
72 os_requirements = None
73
74
75 class __metaclass__(type):
76 """Any time a subclass is defined, add it to our list."""
77 def __init__(mcs, name, bases, dict):
beepsd9153b52013-01-23 20:52:46 -080078 if name != 'ExternalPackage' and not name.startswith('_'):
jamesrenff6e5aa2010-02-12 00:46:40 +000079 mcs.subclasses.append(mcs)
80
81
82 def __init__(self):
83 self.verified_package = ''
84 if not self.module_name:
85 self.module_name = self.name.lower()
86 self.installed_version = ''
87
88
89 @property
90 def name(self):
91 """Return the class name with any trailing 'Package' stripped off."""
92 class_name = self.__class__.__name__
93 if class_name.endswith('Package'):
94 return class_name[:-len('Package')]
95 return class_name
96
97
98 def is_needed(self, unused_install_dir):
99 """@returns True if self.module_name needs to be built and installed."""
100 if not self.module_name or not self.version:
101 logging.warning('version and module_name required for '
102 'is_needed() check to work.')
103 return True
104 try:
105 module = __import__(self.module_name)
106 except ImportError, e:
jamesrenca2a9002010-04-20 22:33:22 +0000107 logging.info("%s isn't present. Will install.", self.module_name)
jamesrenff6e5aa2010-02-12 00:46:40 +0000108 return True
109 self.installed_version = self._get_installed_version_from_module(module)
110 logging.info('imported %s version %s.', self.module_name,
111 self.installed_version)
Dale Curtis74a314b2011-06-23 14:55:46 -0700112 if hasattr(self, 'minimum_version'):
113 return self.minimum_version > self.installed_version
114 else:
115 return self.version > self.installed_version
jamesrenff6e5aa2010-02-12 00:46:40 +0000116
117
118 def _get_installed_version_from_module(self, module):
119 """Ask our module its version string and return it or '' if unknown."""
120 try:
121 return module.__version__
122 except AttributeError:
123 logging.error('could not get version from %s', module)
124 return ''
125
126
127 def _build_and_install(self, install_dir):
128 """Subclasses MUST provide their own implementation."""
129 raise NotImplementedError
130
131
132 def _build_and_install_current_dir(self, install_dir):
133 """
134 Subclasses that use _build_and_install_from_package() MUST provide
135 their own implementation of this method.
136 """
137 raise NotImplementedError
138
139
140 def build_and_install(self, install_dir):
141 """
142 Builds and installs the package. It must have been fetched already.
143
144 @param install_dir - The package installation directory. If it does
145 not exist it will be created.
146 """
147 if not self.verified_package:
148 raise Error('Must call fetch() first. - %s' % self.name)
149 self._check_os_requirements()
150 return self._build_and_install(install_dir)
151
152
153 def _check_os_requirements(self):
154 if not self.os_requirements:
155 return
156 failed = False
157 for file_name, package_name in self.os_requirements.iteritems():
158 if not os.path.exists(file_name):
159 failed = True
160 logging.error('File %s not found, %s needs it.',
161 file_name, self.name)
162 logging.error('Perhaps you need to install something similar '
163 'to the %s package for OS first.', package_name)
164 if failed:
165 raise Error('Missing OS requirements for %s. (see above)' %
166 self.name)
167
168
169 def _build_and_install_current_dir_setup_py(self, install_dir):
170 """For use as a _build_and_install_current_dir implementation."""
171 egg_path = self._build_egg_using_setup_py(setup_py='setup.py')
172 if not egg_path:
173 return False
174 return self._install_from_egg(install_dir, egg_path)
175
176
177 def _build_and_install_current_dir_setupegg_py(self, install_dir):
178 """For use as a _build_and_install_current_dir implementation."""
179 egg_path = self._build_egg_using_setup_py(setup_py='setupegg.py')
180 if not egg_path:
181 return False
182 return self._install_from_egg(install_dir, egg_path)
183
184
185 def _build_and_install_current_dir_noegg(self, install_dir):
186 if not self._build_using_setup_py():
187 return False
188 return self._install_using_setup_py_and_rsync(install_dir)
189
190
191 def _build_and_install_from_package(self, install_dir):
192 """
193 This method may be used as a _build_and_install() implementation
194 for subclasses if they implement _build_and_install_current_dir().
195
196 Extracts the .tar.gz file, chdirs into the extracted directory
197 (which is assumed to match the tar filename) and calls
198 _build_and_isntall_current_dir from there.
199
200 Afterwards the build (regardless of failure) extracted .tar.gz
201 directory is cleaned up.
202
203 @returns True on success, False otherwise.
204
205 @raises OSError If the expected extraction directory does not exist.
206 """
207 self._extract_compressed_package()
208 if self.verified_package.endswith('.tar.gz'):
209 extension = '.tar.gz'
210 elif self.verified_package.endswith('.tar.bz2'):
211 extension = '.tar.bz2'
212 elif self.verified_package.endswith('.zip'):
213 extension = '.zip'
214 else:
215 raise Error('Unexpected package file extension on %s' %
216 self.verified_package)
217 os.chdir(os.path.dirname(self.verified_package))
218 os.chdir(self.local_filename[:-len(extension)])
219 extracted_dir = os.getcwd()
220 try:
221 return self._build_and_install_current_dir(install_dir)
222 finally:
223 os.chdir(os.path.join(extracted_dir, '..'))
224 shutil.rmtree(extracted_dir)
225
226
227 def _extract_compressed_package(self):
228 """Extract the fetched compressed .tar or .zip within its directory."""
229 if not self.verified_package:
230 raise Error('Package must have been fetched first.')
231 os.chdir(os.path.dirname(self.verified_package))
232 if self.verified_package.endswith('gz'):
233 status = system("tar -xzf '%s'" % self.verified_package)
234 elif self.verified_package.endswith('bz2'):
235 status = system("tar -xjf '%s'" % self.verified_package)
236 elif self.verified_package.endswith('zip'):
237 status = system("unzip '%s'" % self.verified_package)
238 else:
239 raise Error('Unknown compression suffix on %s.' %
240 self.verified_package)
241 if status:
242 raise Error('tar failed with %s' % (status,))
243
244
245 def _build_using_setup_py(self, setup_py='setup.py'):
246 """
247 Assuming the cwd is the extracted python package, execute a simple
248 python setup.py build.
249
250 @param setup_py - The name of the setup.py file to execute.
251
252 @returns True on success, False otherwise.
253 """
254 if not os.path.exists(setup_py):
255 raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
256 status = system("'%s' %s build" % (sys.executable, setup_py))
257 if status:
258 logging.error('%s build failed.' % self.name)
259 return False
260 return True
261
262
263 def _build_egg_using_setup_py(self, setup_py='setup.py'):
264 """
265 Assuming the cwd is the extracted python package, execute a simple
266 python setup.py bdist_egg.
267
268 @param setup_py - The name of the setup.py file to execute.
269
270 @returns The relative path to the resulting egg file or '' on failure.
271 """
272 if not os.path.exists(setup_py):
273 raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
274 egg_subdir = 'dist'
275 if os.path.isdir(egg_subdir):
276 shutil.rmtree(egg_subdir)
277 status = system("'%s' %s bdist_egg" % (sys.executable, setup_py))
278 if status:
279 logging.error('bdist_egg of setuptools failed.')
280 return ''
281 # I've never seen a bdist_egg lay multiple .egg files.
282 for filename in os.listdir(egg_subdir):
283 if filename.endswith('.egg'):
284 return os.path.join(egg_subdir, filename)
285
286
287 def _install_from_egg(self, install_dir, egg_path):
288 """
289 Install a module from an egg file by unzipping the necessary parts
290 into install_dir.
291
292 @param install_dir - The installation directory.
293 @param egg_path - The pathname of the egg file.
294 """
295 status = system("unzip -q -o -d '%s' '%s'" % (install_dir, egg_path))
296 if status:
297 logging.error('unzip of %s failed', egg_path)
298 return False
299 egg_info = os.path.join(install_dir, 'EGG-INFO')
300 if os.path.isdir(egg_info):
301 shutil.rmtree(egg_info)
302 return True
303
304
305 def _get_temp_dir(self):
306 return tempfile.mkdtemp(dir='/var/tmp')
307
308
309 def _site_packages_path(self, temp_dir):
310 # This makes assumptions about what python setup.py install
311 # does when given a prefix. Is this always correct?
312 python_xy = 'python%s' % sys.version[:3]
313 return os.path.join(temp_dir, 'lib', python_xy, 'site-packages')
314
315
beepsd9153b52013-01-23 20:52:46 -0800316 def _rsync (self, temp_site_dir, install_dir):
317 """Rsync contents. """
318 status = system("rsync -r '%s/' '%s/'" %
319 (os.path.normpath(temp_site_dir),
320 os.path.normpath(install_dir)))
321 if status:
322 logging.error('%s rsync to install_dir failed.', self.name)
323 return False
324 return True
325
326
jamesrenff6e5aa2010-02-12 00:46:40 +0000327 def _install_using_setup_py_and_rsync(self, install_dir,
328 setup_py='setup.py',
329 temp_dir=None):
330 """
331 Assuming the cwd is the extracted python package, execute a simple:
332
333 python setup.py install --prefix=BLA
334
335 BLA will be a temporary directory that everything installed will
336 be picked out of and rsynced to the appropriate place under
337 install_dir afterwards.
338
339 Afterwards, it deconstructs the extra lib/pythonX.Y/site-packages/
340 directory tree that setuptools created and moves all installed
341 site-packages directly up into install_dir itself.
342
343 @param install_dir the directory for the install to happen under.
344 @param setup_py - The name of the setup.py file to execute.
345
346 @returns True on success, False otherwise.
347 """
348 if not os.path.exists(setup_py):
349 raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
350
351 if temp_dir is None:
352 temp_dir = self._get_temp_dir()
353
354 try:
355 status = system("'%s' %s install --no-compile --prefix='%s'"
356 % (sys.executable, setup_py, temp_dir))
357 if status:
358 logging.error('%s install failed.' % self.name)
359 return False
360
361 if os.path.isdir(os.path.join(temp_dir, 'lib')):
362 # NOTE: This ignores anything outside of the lib/ dir that
363 # was installed.
364 temp_site_dir = self._site_packages_path(temp_dir)
365 else:
366 temp_site_dir = temp_dir
367
beepsd9153b52013-01-23 20:52:46 -0800368 return self._rsync(temp_site_dir, install_dir)
jamesrenff6e5aa2010-02-12 00:46:40 +0000369 finally:
370 shutil.rmtree(temp_dir)
371
372
373
374 def _build_using_make(self, install_dir):
375 """Build the current package using configure/make.
376
377 @returns True on success, False otherwise.
378 """
379 install_prefix = os.path.join(install_dir, 'usr', 'local')
380 status = system('./configure --prefix=%s' % install_prefix)
381 if status:
382 logging.error('./configure failed for %s', self.name)
383 return False
384 status = system('make')
385 if status:
386 logging.error('make failed for %s', self.name)
387 return False
388 status = system('make check')
389 if status:
390 logging.error('make check failed for %s', self.name)
391 return False
392 return True
393
394
395 def _install_using_make(self):
396 """Install the current package using make install.
397
398 Assumes the install path was set up while running ./configure (in
399 _build_using_make()).
400
401 @returns True on success, False otherwise.
402 """
403 status = system('make install')
404 return status == 0
405
406
407 def fetch(self, dest_dir):
408 """
409 Fetch the package from one its URLs and save it in dest_dir.
410
411 If the the package already exists in dest_dir and the checksum
412 matches this code will not fetch it again.
413
414 Sets the 'verified_package' attribute with the destination pathname.
415
416 @param dest_dir - The destination directory to save the local file.
417 If it does not exist it will be created.
418
419 @returns A boolean indicating if we the package is now in dest_dir.
420 @raises FetchError - When something unexpected happens.
421 """
422 if not os.path.exists(dest_dir):
423 os.makedirs(dest_dir)
424 local_path = os.path.join(dest_dir, self.local_filename)
425
426 # If the package exists, verify its checksum and be happy if it is good.
427 if os.path.exists(local_path):
428 actual_hex_sum = _checksum_file(local_path)
429 if self.hex_sum == actual_hex_sum:
430 logging.info('Good checksum for existing %s package.',
431 self.name)
432 self.verified_package = local_path
433 return True
434 logging.warning('Bad checksum for existing %s package. '
435 'Re-downloading', self.name)
436 os.rename(local_path, local_path + '.wrong-checksum')
437
438 # Download the package from one of its urls, rejecting any if the
439 # checksum does not match.
440 for url in self.urls:
441 logging.info('Fetching %s', url)
442 try:
443 url_file = urllib2.urlopen(url)
444 except (urllib2.URLError, EnvironmentError):
445 logging.warning('Could not fetch %s package from %s.',
446 self.name, url)
447 continue
448 data_length = int(url_file.info().get('Content-Length',
449 _MAX_PACKAGE_SIZE))
450 if data_length <= 0 or data_length > _MAX_PACKAGE_SIZE:
451 raise FetchError('%s from %s fails Content-Length %d '
452 'sanity check.' % (self.name, url,
453 data_length))
454 checksum = utils.hash('sha1')
455 total_read = 0
456 output = open(local_path, 'wb')
457 try:
458 while total_read < data_length:
459 data = url_file.read(_READ_SIZE)
460 if not data:
461 break
462 output.write(data)
463 checksum.update(data)
464 total_read += len(data)
465 finally:
466 output.close()
467 if self.hex_sum != checksum.hexdigest():
468 logging.warning('Bad checksum for %s fetched from %s.',
469 self.name, url)
470 logging.warning('Got %s', checksum.hexdigest())
471 logging.warning('Expected %s', self.hex_sum)
472 os.unlink(local_path)
473 continue
474 logging.info('Good checksum.')
475 self.verified_package = local_path
476 return True
477 else:
478 return False
479
480
481# NOTE: This class definition must come -before- all other ExternalPackage
482# classes that need to use this version of setuptools so that is is inserted
483# into the ExternalPackage.subclasses list before them.
484class SetuptoolsPackage(ExternalPackage):
485 # For all known setuptools releases a string compare works for the
486 # version string. Hopefully they never release a 0.10. (Their own
487 # version comparison code would break if they did.)
Dale Curtis74a314b2011-06-23 14:55:46 -0700488 # Any system with setuptools > 0.6 is fine. If none installed, then
489 # try to install the latest found on the upstream.
490 minimum_version = '0.6'
Eric Li6f27d4f2010-09-29 10:55:17 -0700491 version = '0.6c11'
jamesrenff6e5aa2010-02-12 00:46:40 +0000492 urls = ('http://pypi.python.org/packages/source/s/setuptools/'
493 'setuptools-%s.tar.gz' % (version,),)
494 local_filename = 'setuptools-%s.tar.gz' % version
Eric Li6f27d4f2010-09-29 10:55:17 -0700495 hex_sum = '8d1ad6384d358c547c50c60f1bfdb3362c6c4a7d'
jamesrenff6e5aa2010-02-12 00:46:40 +0000496
497 SUDO_SLEEP_DELAY = 15
498
499
500 def _build_and_install(self, install_dir):
501 """Install setuptools on the system."""
502 logging.info('NOTE: setuptools install does not use install_dir.')
503 return self._build_and_install_from_package(install_dir)
504
505
506 def _build_and_install_current_dir(self, install_dir):
507 egg_path = self._build_egg_using_setup_py()
508 if not egg_path:
509 return False
510
511 print '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
512 print 'About to run sudo to install setuptools', self.version
513 print 'on your system for use by', sys.executable, '\n'
514 print '!! ^C within', self.SUDO_SLEEP_DELAY, 'seconds to abort.\n'
515 time.sleep(self.SUDO_SLEEP_DELAY)
516
517 # Copy the egg to the local filesystem /var/tmp so that root can
518 # access it properly (avoid NFS squashroot issues).
519 temp_dir = self._get_temp_dir()
520 try:
521 shutil.copy(egg_path, temp_dir)
522 egg_name = os.path.split(egg_path)[1]
523 temp_egg = os.path.join(temp_dir, egg_name)
524 p = subprocess.Popen(['sudo', '/bin/sh', temp_egg],
525 stdout=subprocess.PIPE)
526 regex = re.compile('Copying (.*?) to (.*?)\n')
527 match = regex.search(p.communicate()[0])
528 status = p.wait()
529
530 if match:
531 compiled = os.path.join(match.group(2), match.group(1))
532 os.system("sudo chmod a+r '%s'" % compiled)
533 finally:
534 shutil.rmtree(temp_dir)
535
536 if status:
537 logging.error('install of setuptools from egg failed.')
538 return False
539 return True
540
541
542class MySQLdbPackage(ExternalPackage):
543 module_name = 'MySQLdb'
544 version = '1.2.2'
545 urls = ('http://downloads.sourceforge.net/project/mysql-python/'
546 'mysql-python/%(version)s/MySQL-python-%(version)s.tar.gz'
547 % dict(version=version),)
548 local_filename = 'MySQL-python-%s.tar.gz' % version
549 hex_sum = '945a04773f30091ad81743f9eb0329a3ee3de383'
550
551 _build_and_install_current_dir = (
552 ExternalPackage._build_and_install_current_dir_setup_py)
553
554
555 def _build_and_install(self, install_dir):
556 if not os.path.exists('/usr/bin/mysql_config'):
557 logging.error('You need to install /usr/bin/mysql_config')
558 logging.error('On Ubuntu or Debian based systems use this: '
559 'sudo apt-get install libmysqlclient15-dev')
560 return False
561 return self._build_and_install_from_package(install_dir)
562
563
564class DjangoPackage(ExternalPackage):
Dale Curtis74a314b2011-06-23 14:55:46 -0700565 version = '1.3'
jamesrenff6e5aa2010-02-12 00:46:40 +0000566 local_filename = 'Django-%s.tar.gz' % version
567 urls = ('http://www.djangoproject.com/download/%s/tarball/' % version,)
Dale Curtis74a314b2011-06-23 14:55:46 -0700568 hex_sum = 'f8814d5e1412bb932318db5130260da5bf053ff7'
jamesrenff6e5aa2010-02-12 00:46:40 +0000569
570 _build_and_install = ExternalPackage._build_and_install_from_package
571 _build_and_install_current_dir = (
572 ExternalPackage._build_and_install_current_dir_noegg)
573
574
575 def _get_installed_version_from_module(self, module):
576 try:
577 return module.get_version().split()[0]
578 except AttributeError:
579 return '0.9.6'
580
581
582
583class NumpyPackage(ExternalPackage):
584 version = '1.2.1'
585 local_filename = 'numpy-%s.tar.gz' % version
586 urls = ('http://downloads.sourceforge.net/project/numpy/NumPy/%(version)s/'
587 'numpy-%(version)s.tar.gz' % dict(version=version),)
588 hex_sum = '1aa706e733aea18eaffa70d93c0105718acb66c5'
589
590 _build_and_install = ExternalPackage._build_and_install_from_package
591 _build_and_install_current_dir = (
592 ExternalPackage._build_and_install_current_dir_setupegg_py)
593
594
595# This requires numpy so it must be declared after numpy to guarantee that it
596# is already installed.
597class MatplotlibPackage(ExternalPackage):
598 version = '0.98.5.3'
599 short_version = '0.98.5'
600 local_filename = 'matplotlib-%s.tar.gz' % version
601 urls = ('http://downloads.sourceforge.net/project/matplotlib/matplotlib/'
602 'matplotlib-%s/matplotlib-%s.tar.gz' % (short_version, version),)
603 hex_sum = '2f6c894cf407192b3b60351bcc6468c0385d47b6'
604 os_requirements = {'/usr/include/ft2build.h': 'libfreetype6-dev',
605 '/usr/include/png.h': 'libpng12-dev'}
606
607 _build_and_install = ExternalPackage._build_and_install_from_package
608 _build_and_install_current_dir = (
609 ExternalPackage._build_and_install_current_dir_setupegg_py)
610
611
612class AtForkPackage(ExternalPackage):
613 version = '0.1.2'
614 local_filename = 'atfork-%s.zip' % version
615 urls = ('http://python-atfork.googlecode.com/files/' + local_filename,)
616 hex_sum = '5baa64c73e966b57fa797040585c760c502dc70b'
617
618 _build_and_install = ExternalPackage._build_and_install_from_package
619 _build_and_install_current_dir = (
620 ExternalPackage._build_and_install_current_dir_noegg)
621
622
623class ParamikoPackage(ExternalPackage):
624 version = '1.7.5'
625 local_filename = 'paramiko-%s.tar.gz' % version
Eric Li861b2d52011-02-04 14:50:35 -0800626 urls = ('http://www.lag.net/paramiko/download/' + local_filename,
627 'ftp://mirrors.kernel.org/gentoo/distfiles/' + local_filename,)
jamesrenff6e5aa2010-02-12 00:46:40 +0000628 hex_sum = '592be7a08290070b71da63a8e6f28a803399e5c5'
629
630
631 _build_and_install = ExternalPackage._build_and_install_from_package
632
633
634 def _check_for_pycrypto(self):
635 # NOTE(gps): Linux distros have better python-crypto packages than we
636 # can easily get today via a wget due to the library's age and staleness
637 # yet many security and behavior bugs are fixed by patches that distros
638 # already apply. PyCrypto has a new active maintainer in 2009. Once a
639 # new release is made (http://pycrypto.org/) we should add an installer.
640 try:
641 import Crypto
642 except ImportError:
643 logging.error('Please run "sudo apt-get install python-crypto" '
644 'or your Linux distro\'s equivalent.')
645 return False
646 return True
647
648
649 def _build_and_install_current_dir(self, install_dir):
650 if not self._check_for_pycrypto():
651 return False
652 # paramiko 1.7.4 doesn't require building, it is just a module directory
653 # that we can rsync into place directly.
654 if not os.path.isdir('paramiko'):
655 raise Error('no paramiko directory in %s.' % os.getcwd())
656 status = system("rsync -r 'paramiko' '%s/'" % install_dir)
657 if status:
658 logging.error('%s rsync to install_dir failed.' % self.name)
659 return False
660 return True
661
662
Chris Masonebafbbb02012-05-16 13:41:36 -0700663class RequestsPackage(ExternalPackage):
664 version = '0.11.2'
665 local_filename = 'requests-%s.tar.gz' % version
666 urls = ('http://pypi.python.org/packages/source/r/requests/' +
667 local_filename,)
668 hex_sum = '00a49e8bd6dd8955acf6f6269d1b85f50c70b712'
669
670 _build_and_install = ExternalPackage._build_and_install_from_package
671 _build_and_install_current_dir = (
672 ExternalPackage._build_and_install_current_dir_setup_py)
673
674
jamesrenff6e5aa2010-02-12 00:46:40 +0000675class SimplejsonPackage(ExternalPackage):
676 version = '2.0.9'
677 local_filename = 'simplejson-%s.tar.gz' % version
678 urls = ('http://pypi.python.org/packages/source/s/simplejson/' +
679 local_filename,)
680 hex_sum = 'b5b26059adbe677b06c299bed30557fcb0c7df8c'
681
682 _build_and_install = ExternalPackage._build_and_install_from_package
683 _build_and_install_current_dir = (
684 ExternalPackage._build_and_install_current_dir_setup_py)
685
686
687class Httplib2Package(ExternalPackage):
688 version = '0.6.0'
689 local_filename = 'httplib2-%s.tar.gz' % version
690 urls = ('http://httplib2.googlecode.com/files/' + local_filename,)
691 hex_sum = '995344b2704826cc0d61a266e995b328d92445a5'
692
693 def _get_installed_version_from_module(self, module):
694 # httplib2 doesn't contain a proper version
695 return self.version
696
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
702class GwtPackage(ExternalPackage):
703 """Fetch and extract a local copy of GWT used to build the frontend."""
704
Dale Curtis74a314b2011-06-23 14:55:46 -0700705 version = '2.3.0'
jamesren012d0322010-04-30 20:21:29 +0000706 local_filename = 'gwt-%s.zip' % version
jamesrenff6e5aa2010-02-12 00:46:40 +0000707 urls = ('http://google-web-toolkit.googlecode.com/files/' + local_filename,)
Dale Curtis74a314b2011-06-23 14:55:46 -0700708 hex_sum = 'd51fce9166e6b31349659ffca89baf93e39bc84b'
jamesrenff6e5aa2010-02-12 00:46:40 +0000709 name = 'gwt'
710 about_filename = 'about.txt'
711 module_name = None # Not a Python module.
712
713
714 def is_needed(self, install_dir):
715 gwt_dir = os.path.join(install_dir, self.name)
716 about_file = os.path.join(install_dir, self.name, self.about_filename)
717
718 if not os.path.exists(gwt_dir) or not os.path.exists(about_file):
719 logging.info('gwt not installed for autotest')
720 return True
721
722 f = open(about_file, 'r')
723 version_line = f.readline()
724 f.close()
725
726 match = re.match(r'Google Web Toolkit (.*)', version_line)
727 if not match:
728 logging.info('did not find gwt version')
729 return True
730
731 logging.info('found gwt version %s', match.group(1))
732 return match.group(1) != self.version
733
734
jamesrenca2a9002010-04-20 22:33:22 +0000735 def _build_and_install(self, install_dir):
jamesrenff6e5aa2010-02-12 00:46:40 +0000736 os.chdir(install_dir)
737 self._extract_compressed_package()
jamesren012d0322010-04-30 20:21:29 +0000738 extracted_dir = self.local_filename[:-len('.zip')]
jamesrenff6e5aa2010-02-12 00:46:40 +0000739 target_dir = os.path.join(install_dir, self.name)
740 if os.path.exists(target_dir):
741 shutil.rmtree(target_dir)
742 os.rename(extracted_dir, target_dir)
743 return True
744
745
Mike Trutyddd44b22011-04-14 15:38:56 -0700746class GVizAPIPackage(ExternalPackage):
747 version = '1.7.0'
748 url_filename = 'gviz_api_py-%s.tar.gz' % version
749 local_filename = 'google-visualization-python.tar.gz'
750 urls = ('http://google-visualization-python.googlecode.com/files/%s' % (
751 url_filename),)
752 hex_sum = 'cd9a0fb4ca5c4f86c0d85756f501fd54ccf492d2'
753
754 _build_and_install = ExternalPackage._build_and_install_from_package
755 _build_and_install_current_dir = (
756 ExternalPackage._build_and_install_current_dir_noegg)
beepsd9153b52013-01-23 20:52:46 -0800757
758
Scott Zawalski2a6acf82013-02-11 16:18:27 -0500759class StatsdPackage(ExternalPackage):
760 version = '1.5.7'
761 url_filename = 'python-statsd-%s.tar.gz' % version
762 local_filename = url_filename
763 urls = ('http://pypi.python.org/packages/source/p/python-statsd/%s' % (
764 url_filename),)
765 hex_sum = '7490370b7e24aca4fd193838ac4e0107bb8185e1'
766
767 _build_and_install = ExternalPackage._build_and_install_from_package
768 _build_and_install_current_dir = (
769 ExternalPackage._build_and_install_current_dir_setup_py)
770
771
beeps93bef482013-02-04 16:24:22 -0800772class GdataPackage(ExternalPackage):
773 """
774 Pulls the GData library, giving us an API to query tracker.
775 """
776
777 version = '2.0.14'
778 url_filename = 'gdata-%s.tar.gz' % version
779 local_filename = url_filename
780 urls = ('http://gdata-python-client.googlecode.com/files/%s' % (
781 url_filename),)
782 hex_sum = '5eed0e01ab931e3f706ec544fc8f06ecac384e91'
783
784 _build_and_install = ExternalPackage._build_and_install_from_package
785 _build_and_install_current_dir = (
786 ExternalPackage._build_and_install_current_dir_noegg)
787
788
beepsd9153b52013-01-23 20:52:46 -0800789class _ExternalGitRepo(ExternalPackage):
790 """
791 Parent class for any package which needs to pull a git repo.
792
793 This class inherits from ExternalPackage only so we can sync git
794 repos through the build_externals script. We do not reuse any of
795 ExternalPackage's other methods. Any package that needs a git repo
796 should subclass this and override build_and_install or fetch as
797 they see appropriate.
798 """
799
800 os_requirements = {'/usr/bin/git' : 'git-core'}
801
802 def is_needed(self, unused_install_dir):
803 """Tell build_externals that we need to fetch."""
804 # TODO(beeps): check if we're already upto date.
805 return True
806
807
808 def build_and_install(self, unused_install_dir):
809 """
810 Fall through method to install a package.
811
812 Overwritten in base classes to pull a git repo.
813 """
814 raise NotImplementedError
815
816
817 def fetch(self, unused_dest_dir):
818 """Fallthrough method to fetch a package."""
819 return True
820
821
822class HdctoolsRepo(_ExternalGitRepo):
823 """Clones or updates the hdctools repo."""
824
825 temp_hdctools_dir = tempfile.mktemp(suffix='hdctools')
826 _GIT_URL = ('https://git.chromium.org/git/'
827 'chromiumos/third_party/hdctools')
828
829 def fetch(self, unused_dest_dir):
830 """
831 Fetch repo to a temporary location.
832
833 We use an intermediate temp directory to stage our
834 installation because we only care about the servo package.
835 If we can't get at the top commit hash after fetching
836 something is wrong. This can happen when we've cloned/pulled
837 an empty repo. Not something we expect to do.
838
839 @parma unused_dest_dir: passed in because we inherit from
840 ExternalPackage.
841
842 @return: True if repo sync was successful.
843 """
844 git_repo = revision_control.GitRepo(
845 self.temp_hdctools_dir,
846 self._GIT_URL,
847 None,
848 self.temp_hdctools_dir)
849 git_repo.fetch_and_reset_or_clone()
850
851 if git_repo.get_latest_commit_hash():
852 return True
853 return False
854
855
856 def build_and_install(self, install_dir):
857 """Reach into the hdctools repo and rsync only the servo directory."""
858
859 servo_dir = os.path.join(self.temp_hdctools_dir, 'servo')
860 if not os.path.exists(servo_dir):
861 return False
862
863 rv = self._rsync(servo_dir, os.path.join(install_dir, 'servo'))
864 shutil.rmtree(self.temp_hdctools_dir)
865 return rv
866
867
868class ChromiteRepo(_ExternalGitRepo):
869 """Clones or updates the chromite repo."""
870
871 _GIT_URL = ('https://git.chromium.org/git/'
872 'chromiumos/chromite')
873
874 def build_and_install(self, install_dir):
875 """
876 Clone if the repo isn't initialized, pull clean bits if it is.
877
878 Unlike it's hdctools counterpart the chromite repo clones master
879 directly into site-packages. It doesn't use an intermediate temp
880 directory because it doesn't need installastion.
881
882 @param install_dir: destination directory for chromite installation.
883 """
884 local_chromite_dir = os.path.join(install_dir, 'chromite')
885 git_repo = revision_control.GitRepo(local_chromite_dir, self._GIT_URL)
886 git_repo.fetch_and_reset_or_clone()
887
888 if git_repo.get_latest_commit_hash():
889 return True
890 return False