Merged changes from master
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob5a724d8dcfbbd9e00b4e46a5f9a1b4aaf78e2c75
1 """
2 Integration with native distribution package managers.
3 @since: 0.28
4 """
6 # Copyright (C) 2009, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
9 from zeroinstall import _, logger
10 import os, platform, re, subprocess, sys
11 from zeroinstall.injector import namespaces, model, arch, qdom
12 from zeroinstall.support import basedir, portable_rename, intern
14 _dotted_ints = '[0-9]+(?:\.[0-9]+)*'
16 # This matches a version number that would be a valid Zero Install version without modification
17 _zeroinstall_regexp = '(?:%s)(?:-(?:pre|rc|post|)(?:%s))*' % (_dotted_ints, _dotted_ints)
19 # This matches the interesting bits of distribution version numbers
20 # (first matching group is for Java-style 6b17 syntax, or "major")
21 _version_regexp = '(?:[a-z])?({ints}b)?({zero})(-r{ints})?'.format(zero = _zeroinstall_regexp, ints = _dotted_ints)
23 # We try to do updates atomically without locking, but we don't worry too much about
24 # duplicate entries or being a little out of sync with the on-disk copy.
25 class Cache(object):
26 def __init__(self, cache_leaf, source, format):
27 """Maintain a cache file (e.g. ~/.cache/0install.net/injector/$name).
28 If the size or mtime of $source has changed, or the cache
29 format version if different, reset the cache first."""
30 self.cache_leaf = cache_leaf
31 self.source = source
32 self.format = format
33 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
34 namespaces.config_prog)
35 self.cached_for = {} # Attributes of source when cache was created
36 try:
37 self._load_cache()
38 except Exception as ex:
39 logger.info(_("Failed to load cache (%s). Flushing..."), ex)
40 self.flush()
42 def flush(self):
43 # Wipe the cache
44 try:
45 info = os.stat(self.source)
46 mtime = int(info.st_mtime)
47 size = info.st_size
48 except Exception as ex:
49 logger.warn("Failed to stat %s: %s", self.source, ex)
50 mtime = size = 0
51 self.cache = {}
52 import tempfile
53 tmp = tempfile.NamedTemporaryFile(mode = 'wt', dir = self.cache_dir, delete = False)
54 tmp.write("mtime=%d\nsize=%d\nformat=%d\n\n" % (mtime, size, self.format))
55 tmp.close()
56 portable_rename(tmp.name, os.path.join(self.cache_dir, self.cache_leaf))
58 self._load_cache()
60 # Populate self.cache from our saved cache file.
61 # Throws an exception if the cache doesn't exist or has the wrong format.
62 def _load_cache(self):
63 self.cache = cache = {}
64 with open(os.path.join(self.cache_dir, self.cache_leaf)) as stream:
65 for line in stream:
66 line = line.strip()
67 if not line:
68 break
69 key, value = line.split('=', 1)
70 if key in ('mtime', 'size', 'format'):
71 self.cached_for[key] = int(value)
73 self._check_valid()
75 for line in stream:
76 key, value = line.split('=', 1)
77 cache[key] = value[:-1]
79 # Check the source file hasn't changed since we created the cache
80 def _check_valid(self):
81 info = os.stat(self.source)
82 if self.cached_for['mtime'] != int(info.st_mtime):
83 raise Exception("Modification time of %s has changed" % self.source)
84 if self.cached_for['size'] != info.st_size:
85 raise Exception("Size of %s has changed" % self.source)
86 if self.cached_for.get('format', None) != self.format:
87 raise Exception("Format of cache has changed")
89 def get(self, key):
90 try:
91 self._check_valid()
92 except Exception as ex:
93 logger.info(_("Cache needs to be refreshed: %s"), ex)
94 self.flush()
95 return None
96 else:
97 return self.cache.get(key, None)
99 def put(self, key, value):
100 cache_path = os.path.join(self.cache_dir, self.cache_leaf)
101 self.cache[key] = value
102 try:
103 with open(cache_path, 'a') as stream:
104 stream.write('%s=%s\n' % (key, value))
105 except Exception as ex:
106 logger.warn("Failed to write to cache %s: %s=%s: %s", cache_path, key, value, ex)
108 def try_cleanup_distro_version(version):
109 """Try to turn a distribution version string into one readable by Zero Install.
110 We do this by stripping off anything we can't parse.
111 @return: the part we understood, or None if we couldn't parse anything
112 @rtype: str"""
113 if ':' in version:
114 version = version.split(':')[1] # Skip 'epoch'
115 version = version.replace('_', '-')
116 if '~' in version:
117 version, suffix = version.split('~', 1)
118 if suffix.startswith('pre'):
119 suffix = suffix[3:]
120 suffix = '-pre' + (try_cleanup_distro_version(suffix) or '')
121 else:
122 suffix = ''
123 match = re.match(_version_regexp, version)
124 if match:
125 major, version, revision = match.groups()
126 if major is not None:
127 version = major[:-1] + '.' + version
128 if revision is not None:
129 version = '%s-%s' % (version, revision[2:])
130 return version + suffix
131 return None
133 class Distribution(object):
134 """Represents a distribution with which we can integrate.
135 Sub-classes should specialise this to integrate with the package managers of
136 particular distributions. This base class ignores the native package manager.
137 @since: 0.28
139 _packagekit = None
141 def get_package_info(self, package, factory):
142 """Get information about the given package.
143 Add zero or more implementations using the factory (typically at most two
144 will be added; the currently installed version and the latest available).
145 @param package: package name (e.g. "gimp")
146 @type package: str
147 @param factory: function for creating new DistributionImplementation objects from IDs
148 @type factory: str -> L{model.DistributionImplementation}
150 return
152 def get_score(self, distribution):
153 """Indicate how closely the host distribution matches this one.
154 The <package-implementation> with the highest score is passed
155 to L{Distribution.get_package_info}. If several elements get
156 the same score, get_package_info is called for all of them.
157 @param distribution: a distribution name
158 @type distribution: str
159 @return: an integer, or -1 if there is no match at all
160 @rtype: int
162 return 0
164 def get_feed(self, master_feed):
165 """Generate a feed containing information about distribution packages.
166 This should immediately return a feed containing an implementation for the
167 package if it's already installed. Information about versions that could be
168 installed using the distribution's package manager can be added asynchronously
169 later (see L{fetch_candidates}).
170 @param master_feed: feed containing the <package-implementation> elements
171 @type master_feed: L{model.ZeroInstallFeed}
172 @rtype: L{model.ZeroInstallFeed}"""
174 feed = model.ZeroInstallFeed(None)
175 feed.url = 'distribution:' + master_feed.url
177 for item, item_attrs in master_feed.get_package_impls(self):
178 package = item_attrs.get('package', None)
179 if package is None:
180 raise model.InvalidInterface(_("Missing 'package' attribute on %s") % item)
182 new_impls = []
184 def factory(id, only_if_missing = False, installed = True):
185 assert id.startswith('package:')
186 if id in feed.implementations:
187 if only_if_missing:
188 return None
189 logger.warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
190 impl = model.DistributionImplementation(feed, id, self, item)
191 feed.implementations[id] = impl
192 new_impls.append(impl)
194 impl.installed = installed
195 impl.metadata = item_attrs
197 if 'run' not in impl.commands:
198 item_main = item_attrs.get('main', None)
199 if item_main:
200 if item_main.startswith('/'):
201 impl.main = item_main
202 else:
203 raise model.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
204 item_main)
205 impl.upstream_stability = model.packaged
207 return impl
209 self.get_package_info(package, factory)
211 for impl in new_impls:
212 self.fixup(package, impl)
213 if impl.installed:
214 self.installed_fixup(impl)
215 return feed
217 def fetch_candidates(self, master_feed):
218 """Collect information about versions we could install using
219 the distribution's package manager. On success, the distribution
220 feed in iface_cache is updated.
221 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
222 if self.packagekit.available:
223 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
224 return self.packagekit.fetch_candidates(package_names)
226 @property
227 def packagekit(self):
228 """For use by subclasses.
229 @rtype: L{packagekit.PackageKit}"""
230 if not self._packagekit:
231 from zeroinstall.injector import packagekit
232 self._packagekit = packagekit.PackageKit()
233 return self._packagekit
235 def fixup(self, package, impl):
236 """Some packages require special handling (e.g. Java). This is called for each
237 package that was added by L{get_package_info} after it returns. The default
238 method does nothing.
239 @param package: the name of the package
240 @param impl: the constructed implementation"""
241 pass
243 def installed_fixup(self, impl):
244 """Called when an installed package is added (after L{fixup}), or when installation
245 completes. This is useful to fix up the main value.
246 @type impl: L{DistributionImplementation}
247 @since: 1.11"""
248 pass
250 class WindowsDistribution(Distribution):
251 def get_package_info(self, package, factory):
252 def _is_64bit_windows():
253 p = sys.platform
254 from win32process import IsWow64Process
255 if p == 'win64' or (p == 'win32' and IsWow64Process()): return True
256 elif p == 'win32': return False
257 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
259 def _read_hklm_reg(key_name, value_name):
260 from win32api import RegOpenKeyEx, RegQueryValueEx, RegCloseKey
261 from win32con import HKEY_LOCAL_MACHINE, KEY_READ
262 KEY_WOW64_64KEY = 0x0100
263 KEY_WOW64_32KEY = 0x0200
264 if _is_64bit_windows():
265 try:
266 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_32KEY)
267 (value32, _) = RegQueryValueEx(key32, value_name)
268 RegCloseKey(key32)
269 except:
270 value32 = ''
271 try:
272 key64 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_64KEY)
273 (value64, _) = RegQueryValueEx(key64, value_name)
274 RegCloseKey(key64)
275 except:
276 value64 = ''
277 else:
278 try:
279 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ)
280 (value32, _) = RegQueryValueEx(key32, value_name)
281 RegCloseKey(key32)
282 except:
283 value32 = ''
284 value64 = ''
285 return (value32, value64)
287 def find_java(part, win_version, zero_version):
288 reg_path = r"SOFTWARE\JavaSoft\{part}\{win_version}".format(part = part, win_version = win_version)
289 (java32_home, java64_home) = _read_hklm_reg(reg_path, "JavaHome")
291 for (home, arch) in [(java32_home, 'i486'),
292 (java64_home, 'x86_64')]:
293 if os.path.isfile(home + r"\bin\java.exe"):
294 impl = factory('package:windows:%s:%s:%s' % (package, zero_version, arch))
295 impl.machine = arch
296 impl.version = model.parse_version(zero_version)
297 impl.upstream_stability = model.packaged
298 impl.main = home + r"\bin\java.exe"
300 if package == 'openjdk-6-jre':
301 find_java("Java Runtime Environment", "1.6", '6')
302 elif package == 'openjdk-6-jdk':
303 find_java("Java Development Kit", "1.6", '6')
304 elif package == 'openjdk-7-jre':
305 find_java("Java Runtime Environment", "1.7", '7')
306 elif package == 'openjdk-7-jdk':
307 find_java("Java Development Kit", "1.7", '7')
309 def get_score(self, disto_name):
310 return int(disto_name == 'Windows')
312 class DarwinDistribution(Distribution):
313 def get_package_info(self, package, factory):
314 def java_home(version, arch):
315 null = os.open(os.devnull, os.O_WRONLY)
316 child = subprocess.Popen(["/usr/libexec/java_home", "--failfast", "--version", version, "--arch", arch],
317 stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
318 home = child.stdout.read().strip()
319 child.stdout.close()
320 child.wait()
321 return home
323 def find_java(part, jvm_version, zero_version):
324 for arch in ['i386', 'x86_64']:
325 home = java_home(jvm_version, arch)
326 if os.path.isfile(home + "/bin/java"):
327 impl = factory('package:darwin:%s:%s:%s' % (package, zero_version, arch))
328 impl.machine = arch
329 impl.version = model.parse_version(zero_version)
330 impl.upstream_stability = model.packaged
331 impl.main = home + "/bin/java"
333 if package == 'openjdk-6-jre':
334 find_java("Java Runtime Environment", "1.6", '6')
335 elif package == 'openjdk-6-jdk':
336 find_java("Java Development Kit", "1.6", '6')
337 elif package == 'openjdk-7-jre':
338 find_java("Java Runtime Environment", "1.7", '7')
339 elif package == 'openjdk-7-jdk':
340 find_java("Java Development Kit", "1.7", '7')
342 def get_score(self, disto_name):
343 return int(disto_name == 'Darwin')
345 class CachedDistribution(Distribution):
346 """For distributions where querying the package database is slow (e.g. requires running
347 an external command), we cache the results.
348 @since: 0.39
349 @deprecated: use Cache instead
352 def __init__(self, db_status_file):
353 """@param db_status_file: update the cache when the timestamp of this file changes"""
354 self._status_details = os.stat(db_status_file)
356 self.versions = {}
357 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
358 namespaces.config_prog)
360 try:
361 self._load_cache()
362 except Exception as ex:
363 logger.info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
364 try:
365 self.generate_cache()
366 self._load_cache()
367 except Exception as ex:
368 logger.warn(_("Failed to regenerate distribution database cache: %s"), ex)
370 def _load_cache(self):
371 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
372 Throws an exception if the cache should be (re)created."""
373 with open(os.path.join(self.cache_dir, self.cache_leaf), 'rt') as stream:
374 cache_version = None
375 for line in stream:
376 if line == '\n':
377 break
378 name, value = line.split(': ')
379 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
380 raise Exception(_("Modification time of package database file has changed"))
381 if name == 'size' and int(value) != self._status_details.st_size:
382 raise Exception(_("Size of package database file has changed"))
383 if name == 'version':
384 cache_version = int(value)
385 else:
386 raise Exception(_('Invalid cache format (bad header)'))
388 if cache_version is None:
389 raise Exception(_('Old cache format'))
391 versions = self.versions
392 for line in stream:
393 package, version, zi_arch = line[:-1].split('\t')
394 versionarch = (version, intern(zi_arch))
395 if package not in versions:
396 versions[package] = [versionarch]
397 else:
398 versions[package].append(versionarch)
400 def _write_cache(self, cache):
401 #cache.sort() # Might be useful later; currently we don't care
402 import tempfile
403 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
404 dir = self.cache_dir)
405 try:
406 stream = os.fdopen(fd, 'wt')
407 stream.write('version: 2\n')
408 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
409 stream.write('size: %d\n' % self._status_details.st_size)
410 stream.write('\n')
411 for line in cache:
412 stream.write(line + '\n')
413 stream.close()
415 portable_rename(tmpname,
416 os.path.join(self.cache_dir,
417 self.cache_leaf))
418 except:
419 os.unlink(tmpname)
420 raise
422 # Maps machine type names used in packages to their Zero Install versions
423 # (updates to this might require changing the reverse Java mapping)
424 _canonical_machine = {
425 'all' : '*',
426 'any' : '*',
427 'noarch' : '*',
428 '(none)' : '*',
429 'x86_64': 'x86_64',
430 'amd64': 'x86_64',
431 'i386': 'i386',
432 'i486': 'i486',
433 'i586': 'i586',
434 'i686': 'i686',
435 'ppc64': 'ppc64',
436 'ppc': 'ppc',
439 host_machine = arch.canonicalize_machine(platform.uname()[4])
440 def canonical_machine(package_machine):
441 machine = _canonical_machine.get(package_machine, None)
442 if machine is None:
443 # Safe default if we can't understand the arch
444 return host_machine
445 return machine
447 class DebianDistribution(Distribution):
448 """A dpkg-based distribution."""
450 cache_leaf = 'dpkg-status.cache'
452 def __init__(self, dpkg_status):
453 self.dpkg_cache = Cache('dpkg-status.cache', dpkg_status, 2)
454 self.apt_cache = {}
456 def _query_installed_package(self, package):
457 null = os.open(os.devnull, os.O_WRONLY)
458 child = subprocess.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package],
459 stdout = subprocess.PIPE, stderr = null,
460 universal_newlines = True) # Needed for Python 3
461 os.close(null)
462 stdout, stderr = child.communicate()
463 child.wait()
464 for line in stdout.split('\n'):
465 if not line: continue
466 version, debarch, status = line.split('\t', 2)
467 if not status.endswith(' installed'): continue
468 clean_version = try_cleanup_distro_version(version)
469 if debarch.find("-") != -1:
470 debarch = debarch.split("-")[-1]
471 if clean_version:
472 return '%s\t%s' % (clean_version, canonical_machine(debarch.strip()))
473 else:
474 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
476 return '-'
478 def get_package_info(self, package, factory):
479 # Add any already-installed package...
480 installed_cached_info = self._get_dpkg_info(package)
482 if installed_cached_info != '-':
483 installed_version, machine = installed_cached_info.split('\t')
484 impl = factory('package:deb:%s:%s:%s' % (package, installed_version, machine))
485 impl.version = model.parse_version(installed_version)
486 if machine != '*':
487 impl.machine = machine
488 else:
489 installed_version = None
491 # Add any uninstalled candidates (note: only one of these two methods will add anything)
493 # From PackageKit...
494 self.packagekit.get_candidates(package, factory, 'package:deb')
496 # From apt-cache...
497 cached = self.apt_cache.get(package, None)
498 if cached:
499 candidate_version = cached['version']
500 candidate_arch = cached['arch']
501 if candidate_version and candidate_version != installed_version:
502 impl = factory('package:deb:%s:%s:%s' % (package, candidate_version, candidate_arch), installed = False)
503 impl.version = model.parse_version(candidate_version)
504 if candidate_arch != '*':
505 impl.machine = candidate_arch
506 def install(handler):
507 raise model.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
508 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
509 "use that to install it.") % package)
510 impl.download_sources.append(model.DistributionSource(package, cached['size'], install, needs_confirmation = False))
512 def fixup(self, package, impl):
513 if impl.id.startswith('package:deb:openjdk-6-jre:') or \
514 impl.id.startswith('package:deb:openjdk-7-jre:'):
515 # Debian marks all Java versions as pre-releases
516 # See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=685276
517 impl.version = model.parse_version(impl.get_version().replace('-pre', '.'))
519 def installed_fixup(self, impl):
520 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
521 if impl.id.startswith('package:deb:openjdk-6-jre:'):
522 java_version = '6-openjdk'
523 elif impl.id.startswith('package:deb:openjdk-7-jre:'):
524 java_version = '7-openjdk'
525 else:
526 return
528 if impl.machine == 'x86_64':
529 java_arch = 'amd64'
530 else:
531 java_arch = impl.machine
533 java_bin = '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version, java_arch)
534 if not os.path.exists(java_bin):
535 # Try without the arch...
536 java_bin = '/usr/lib/jvm/java-%s/jre/bin/java' % java_version
537 if not os.path.exists(java_bin):
538 logger.info("Java binary not found (%s)", java_bin)
539 if impl.main is None:
540 java_bin = '/usr/bin/java'
541 else:
542 return
544 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command',
545 {'path': java_bin, 'name': 'run'}), None)
547 def get_score(self, disto_name):
548 return int(disto_name == 'Debian')
550 def _get_dpkg_info(self, package):
551 installed_cached_info = self.dpkg_cache.get(package)
552 if installed_cached_info == None:
553 installed_cached_info = self._query_installed_package(package)
554 self.dpkg_cache.put(package, installed_cached_info)
556 return installed_cached_info
558 def fetch_candidates(self, master_feed):
559 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
561 if self.packagekit.available:
562 return self.packagekit.fetch_candidates(package_names)
564 # No PackageKit. Use apt-cache directly.
565 for package in package_names:
566 # Check to see whether we could get a newer version using apt-get
567 try:
568 null = os.open(os.devnull, os.O_WRONLY)
569 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
570 os.close(null)
572 arch = version = size = None
573 for line in child.stdout:
574 line = line.strip()
575 if line.startswith('Version: '):
576 version = line[9:]
577 version = try_cleanup_distro_version(version)
578 elif line.startswith('Architecture: '):
579 arch = canonical_machine(line[14:].strip())
580 elif line.startswith('Size: '):
581 size = int(line[6:].strip())
582 if version and arch:
583 cached = {'version': version, 'arch': arch, 'size': size}
584 else:
585 cached = None
586 child.stdout.close()
587 child.wait()
588 except Exception as ex:
589 logger.warn("'apt-cache show %s' failed: %s", package, ex)
590 cached = None
591 # (multi-arch support? can there be multiple candidates?)
592 self.apt_cache[package] = cached
594 class RPMDistribution(CachedDistribution):
595 """An RPM-based distribution."""
597 cache_leaf = 'rpm-status.cache'
599 def generate_cache(self):
600 cache = []
602 child = subprocess.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
603 stdout = subprocess.PIPE, universal_newlines = True)
604 for line in child.stdout:
605 package, version, rpmarch = line.split('\t', 2)
606 if package == 'gpg-pubkey':
607 continue
608 zi_arch = canonical_machine(rpmarch.strip())
609 clean_version = try_cleanup_distro_version(version)
610 if clean_version:
611 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
612 else:
613 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
615 self._write_cache(cache)
616 child.stdout.close()
617 child.wait()
619 def get_package_info(self, package, factory):
620 # Add installed versions...
621 versions = self.versions.get(package, [])
623 for version, machine in versions:
624 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
625 impl.version = model.parse_version(version)
626 if machine != '*':
627 impl.machine = machine
629 # Add any uninstalled candidates found by PackageKit
630 self.packagekit.get_candidates(package, factory, 'package:rpm')
632 def installed_fixup(self, impl):
633 # OpenSUSE uses _, Fedora uses .
634 impl_id = impl.id.replace('_', '.')
636 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
638 if impl_id.startswith('package:rpm:java-1.6.0-openjdk:'):
639 java_version = '1.6.0-openjdk'
640 elif impl_id.startswith('package:rpm:java-1.7.0-openjdk:'):
641 java_version = '1.7.0-openjdk'
642 else:
643 return
645 # On Fedora, unlike Debian, the arch is x86_64, not amd64
647 java_bin = '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version, impl.machine)
648 if not os.path.exists(java_bin):
649 # Try without the arch...
650 java_bin = '/usr/lib/jvm/jre-%s/bin/java' % java_version
651 if not os.path.exists(java_bin):
652 logger.info("Java binary not found (%s)", java_bin)
653 if impl.main is None:
654 java_bin = '/usr/bin/java'
655 else:
656 return
658 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command',
659 {'path': java_bin, 'name': 'run'}), None)
661 def fixup(self, package, impl):
662 # OpenSUSE uses _, Fedora uses .
663 package = package.replace('_', '.')
665 if package in ('java-1.6.0-openjdk', 'java-1.7.0-openjdk',
666 'java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'):
667 if impl.version[0][0] == 1:
668 # OpenSUSE uses 1.6 to mean 6
669 del impl.version[0][0]
671 def get_score(self, disto_name):
672 return int(disto_name == 'RPM')
674 class SlackDistribution(Distribution):
675 """A Slack-based distribution."""
677 def __init__(self, packages_dir):
678 self._packages_dir = packages_dir
680 def get_package_info(self, package, factory):
681 # Add installed versions...
682 for entry in os.listdir(self._packages_dir):
683 name, version, arch, build = entry.rsplit('-', 3)
684 if name == package:
685 zi_arch = canonical_machine(arch)
686 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
687 if not clean_version:
688 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
689 continue
691 impl = factory('package:slack:%s:%s:%s' % \
692 (package, clean_version, zi_arch))
693 impl.version = model.parse_version(clean_version)
694 if zi_arch != '*':
695 impl.machine = zi_arch
697 # Add any uninstalled candidates found by PackageKit
698 self.packagekit.get_candidates(package, factory, 'package:slack')
700 def get_score(self, disto_name):
701 return int(disto_name == 'Slack')
703 class ArchDistribution(Distribution):
704 """An Arch Linux distribution."""
706 def __init__(self, packages_dir):
707 self._packages_dir = os.path.join(packages_dir, "local")
709 def get_package_info(self, package, factory):
710 # Add installed versions...
711 for entry in os.listdir(self._packages_dir):
712 name, version, build = entry.rsplit('-', 2)
713 if name == package:
714 gotarch = False
715 with open(os.path.join(self._packages_dir, entry, "desc"), 'rt') as stream:
716 for line in stream:
717 if line == "%ARCH%\n":
718 gotarch = True
719 continue
720 if gotarch:
721 arch = line.strip()
722 break
723 zi_arch = canonical_machine(arch)
724 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
725 if not clean_version:
726 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
727 continue
729 impl = factory('package:arch:%s:%s:%s' % \
730 (package, clean_version, zi_arch))
731 impl.version = model.parse_version(clean_version)
732 if zi_arch != '*':
733 impl.machine = zi_arch
735 # Add any uninstalled candidates found by PackageKit
736 self.packagekit.get_candidates(package, factory, 'package:arch')
738 def get_score(self, disto_name):
739 return int(disto_name == 'Arch')
741 class GentooDistribution(Distribution):
743 def __init__(self, pkgdir):
744 self._pkgdir = pkgdir
746 def get_package_info(self, package, factory):
747 # Add installed versions...
748 _version_start_reqexp = '-[0-9]'
750 if package.count('/') != 1: return
752 category, leafname = package.split('/')
753 category_dir = os.path.join(self._pkgdir, category)
754 match_prefix = leafname + '-'
756 if not os.path.isdir(category_dir): return
758 for filename in os.listdir(category_dir):
759 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit():
760 with open(os.path.join(category_dir, filename, 'PF'), 'rt') as stream:
761 name = stream.readline().strip()
763 match = re.search(_version_start_reqexp, name)
764 if match is None:
765 logger.warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name})
766 continue
767 else:
768 version = try_cleanup_distro_version(name[match.start() + 1:])
770 if category == 'app-emulation' and name.startswith('emul-'):
771 __, __, machine, __ = name.split('-', 3)
772 else:
773 with open(os.path.join(category_dir, filename, 'CHOST'), 'rt') as stream:
774 machine, __ = stream.readline().split('-', 1)
775 machine = arch.canonicalize_machine(machine)
777 impl = factory('package:gentoo:%s:%s:%s' % \
778 (package, version, machine))
779 impl.version = model.parse_version(version)
780 impl.machine = machine
782 # Add any uninstalled candidates found by PackageKit
783 self.packagekit.get_candidates(package, factory, 'package:gentoo')
785 def get_score(self, disto_name):
786 return int(disto_name == 'Gentoo')
788 class PortsDistribution(Distribution):
790 def __init__(self, pkgdir):
791 self._pkgdir = pkgdir
793 def get_package_info(self, package, factory):
794 _name_version_regexp = '^(.+)-([^-]+)$'
796 nameversion = re.compile(_name_version_regexp)
797 for pkgname in os.listdir(self._pkgdir):
798 pkgdir = os.path.join(self._pkgdir, pkgname)
799 if not os.path.isdir(pkgdir): continue
801 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
803 match = nameversion.search(pkgname)
804 if match is None:
805 logger.warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname})
806 continue
807 else:
808 name = match.group(1)
809 if name != package:
810 continue
811 version = try_cleanup_distro_version(match.group(2))
813 machine = host_machine
815 impl = factory('package:ports:%s:%s:%s' % \
816 (package, version, machine))
817 impl.version = model.parse_version(version)
818 impl.machine = machine
820 def get_score(self, disto_name):
821 return int(disto_name == 'Ports')
823 class MacPortsDistribution(CachedDistribution):
824 def __init__(self, db_status_file):
825 super(MacPortsDistribution, self).__init__(db_status_file)
826 self.darwin = DarwinDistribution()
828 cache_leaf = 'macports-status.cache'
830 def generate_cache(self):
831 cache = []
833 child = subprocess.Popen(["port", "-v", "installed"],
834 stdout = subprocess.PIPE, universal_newlines = True)
835 for line in child.stdout:
836 if not line.startswith(" "):
837 continue
838 if line.strip().count(" ") > 1:
839 package, version, extra = line.split(None, 2)
840 else:
841 package, version = line.split()
842 extra = ""
843 if not extra.startswith("(active)"):
844 continue
845 version = version.lstrip('@')
846 version = re.sub(r"\+.*", "", version) # strip variants
847 zi_arch = '*'
848 clean_version = try_cleanup_distro_version(version)
849 if clean_version:
850 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra)
851 if match:
852 platform, major, archs = match.groups()
853 for arch in archs.split():
854 zi_arch = canonical_machine(arch)
855 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
856 else:
857 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
858 else:
859 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
860 self._write_cache(cache)
861 child.stdout.close()
862 child.wait()
864 def get_package_info(self, package, factory):
865 self.darwin.get_package_info(package, factory)
867 # Add installed versions...
868 versions = self.versions.get(package, [])
870 for version, machine in versions:
871 impl = factory('package:macports:%s:%s:%s' % (package, version, machine))
872 impl.version = model.parse_version(version)
873 if machine != '*':
874 impl.machine = machine
876 def get_score(self, disto_name):
877 # We support both sources of packages.
878 # In theory, we should route 'Darwin' package names to DarwinDistribution, and
879 # Mac Ports names to MacPortsDistribution. But since we only use Darwin for Java,
880 # having one object handle both is OK.
881 return int(disto_name in ('Darwin', 'MacPorts'))
883 class CygwinDistribution(CachedDistribution):
884 """A Cygwin-based distribution."""
886 cache_leaf = 'cygcheck-status.cache'
888 def generate_cache(self):
889 cache = []
891 zi_arch = canonical_machine(arch)
892 for line in os.popen("cygcheck -c -d"):
893 if line == "Cygwin Package Information\r\n":
894 continue
895 if line == "\n":
896 continue
897 package, version = line.split()
898 if package == "Package" and version == "Version":
899 continue
900 clean_version = try_cleanup_distro_version(version)
901 if clean_version:
902 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
903 else:
904 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
906 self._write_cache(cache)
908 def get_package_info(self, package, factory):
909 # Add installed versions...
910 versions = self.versions.get(package, [])
912 for version, machine in versions:
913 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine))
914 impl.version = model.parse_version(version)
915 if machine != '*':
916 impl.machine = machine
918 def get_score(self, disto_name):
919 return int(disto_name == 'Cygwin')
922 _host_distribution = None
923 def get_host_distribution():
924 """Get a Distribution suitable for the host operating system.
925 Calling this twice will return the same object.
926 @rtype: L{Distribution}"""
927 global _host_distribution
928 if not _host_distribution:
929 dpkg_db_status = '/var/lib/dpkg/status'
930 rpm_db_packages = '/var/lib/rpm/Packages'
931 _slack_db = '/var/log/packages'
932 _arch_db = '/var/lib/pacman'
933 _pkg_db = '/var/db/pkg'
934 _macports_db = '/opt/local/var/macports/registry/registry.db'
935 _cygwin_log = '/var/log/setup.log'
937 if sys.prefix == "/sw":
938 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status)
939 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages)
941 if os.name == "nt":
942 _host_distribution = WindowsDistribution()
943 elif os.path.isdir(_pkg_db):
944 if sys.platform.startswith("linux"):
945 _host_distribution = GentooDistribution(_pkg_db)
946 elif sys.platform.startswith("freebsd"):
947 _host_distribution = PortsDistribution(_pkg_db)
948 elif os.path.isfile(_macports_db) \
949 and sys.prefix.startswith("/opt/local"):
950 _host_distribution = MacPortsDistribution(_macports_db)
951 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin":
952 _host_distribution = CygwinDistribution(_cygwin_log)
953 elif os.access(dpkg_db_status, os.R_OK) \
954 and os.path.getsize(dpkg_db_status) > 0:
955 _host_distribution = DebianDistribution(dpkg_db_status)
956 elif os.path.isfile(rpm_db_packages):
957 _host_distribution = RPMDistribution(rpm_db_packages)
958 elif os.path.isdir(_slack_db):
959 _host_distribution = SlackDistribution(_slack_db)
960 elif os.path.isdir(_arch_db):
961 _host_distribution = ArchDistribution(_arch_db)
962 elif sys.platform == "darwin":
963 _host_distribution = DarwinDistribution()
964 else:
965 _host_distribution = Distribution()
967 return _host_distribution