Implemented installation-time fixup for Fedora and OpenSUSE too
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob335209c17dede934096a2c5dde4ad538e312b7c5
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)
216 if master_feed.url == 'http://repo.roscidus.com/python/python' and all(not impl.installed for impl in feed.implementations.values()):
217 # Hack: we can support Python on platforms with unsupported package managers
218 # by adding the implementation of Python running us now to the list.
219 python_version = '.'.join([str(v) for v in sys.version_info if isinstance(v, int)])
220 impl_id = 'package:host:python:' + python_version
221 assert impl_id not in feed.implementations
222 impl = model.DistributionImplementation(feed, impl_id, self)
223 impl.installed = True
224 impl.version = model.parse_version(python_version)
225 impl.main = sys.executable
226 impl.upstream_stability = model.packaged
227 impl.machine = host_machine # (hopefully)
228 feed.implementations[impl_id] = impl
230 return feed
232 def fetch_candidates(self, master_feed):
233 """Collect information about versions we could install using
234 the distribution's package manager. On success, the distribution
235 feed in iface_cache is updated.
236 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
237 if self.packagekit.available:
238 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
239 return self.packagekit.fetch_candidates(package_names)
241 @property
242 def packagekit(self):
243 """For use by subclasses.
244 @rtype: L{packagekit.PackageKit}"""
245 if not self._packagekit:
246 from zeroinstall.injector import packagekit
247 self._packagekit = packagekit.PackageKit()
248 return self._packagekit
250 def fixup(self, package, impl):
251 """Some packages require special handling (e.g. Java). This is called for each
252 package that was added by L{get_package_info} after it returns. The default
253 method does nothing.
254 @param package: the name of the package
255 @param impl: the constructed implementation"""
256 pass
258 def installed_fixup(self, impl):
259 """Called when an installed package is added (after L{fixup}), or when installation
260 completes. This is useful to fix up the main value.
261 @type impl: L{DistributionImplementation}
262 @since: 1.11"""
263 pass
265 class WindowsDistribution(Distribution):
266 def get_package_info(self, package, factory):
267 def _is_64bit_windows():
268 p = sys.platform
269 from win32process import IsWow64Process
270 if p == 'win64' or (p == 'win32' and IsWow64Process()): return True
271 elif p == 'win32': return False
272 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
274 def _read_hklm_reg(key_name, value_name):
275 from win32api import RegOpenKeyEx, RegQueryValueEx, RegCloseKey
276 from win32con import HKEY_LOCAL_MACHINE, KEY_READ
277 KEY_WOW64_64KEY = 0x0100
278 KEY_WOW64_32KEY = 0x0200
279 if _is_64bit_windows():
280 try:
281 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_32KEY)
282 (value32, _) = RegQueryValueEx(key32, value_name)
283 RegCloseKey(key32)
284 except:
285 value32 = ''
286 try:
287 key64 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_64KEY)
288 (value64, _) = RegQueryValueEx(key64, value_name)
289 RegCloseKey(key64)
290 except:
291 value64 = ''
292 else:
293 try:
294 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ)
295 (value32, _) = RegQueryValueEx(key32, value_name)
296 RegCloseKey(key32)
297 except:
298 value32 = ''
299 value64 = ''
300 return (value32, value64)
302 def find_java(part, win_version, zero_version):
303 reg_path = r"SOFTWARE\JavaSoft\{part}\{win_version}".format(part = part, win_version = win_version)
304 (java32_home, java64_home) = _read_hklm_reg(reg_path, "JavaHome")
306 for (home, arch) in [(java32_home, 'i486'),
307 (java64_home, 'x86_64')]:
308 if os.path.isfile(home + r"\bin\java.exe"):
309 impl = factory('package:windows:%s:%s:%s' % (package, zero_version, arch))
310 impl.machine = arch
311 impl.version = model.parse_version(zero_version)
312 impl.upstream_stability = model.packaged
313 impl.main = home + r"\bin\java.exe"
315 if package == 'openjdk-6-jre':
316 find_java("Java Runtime Environment", "1.6", '6')
317 elif package == 'openjdk-6-jdk':
318 find_java("Java Development Kit", "1.6", '6')
319 elif package == 'openjdk-7-jre':
320 find_java("Java Runtime Environment", "1.7", '7')
321 elif package == 'openjdk-7-jdk':
322 find_java("Java Development Kit", "1.7", '7')
324 def get_score(self, disto_name):
325 return int(disto_name == 'Windows')
327 class DarwinDistribution(Distribution):
328 def get_package_info(self, package, factory):
329 def java_home(version, arch):
330 null = os.open(os.devnull, os.O_WRONLY)
331 child = subprocess.Popen(["/usr/libexec/java_home", "--failfast", "--version", version, "--arch", arch],
332 stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
333 home = child.stdout.read().strip()
334 child.stdout.close()
335 child.wait()
336 return home
338 def find_java(part, jvm_version, zero_version):
339 for arch in ['i386', 'x86_64']:
340 home = java_home(jvm_version, arch)
341 if os.path.isfile(home + "/bin/java"):
342 impl = factory('package:darwin:%s:%s:%s' % (package, zero_version, arch))
343 impl.machine = arch
344 impl.version = model.parse_version(zero_version)
345 impl.upstream_stability = model.packaged
346 impl.main = home + "/bin/java"
348 if package == 'openjdk-6-jre':
349 find_java("Java Runtime Environment", "1.6", '6')
350 elif package == 'openjdk-6-jdk':
351 find_java("Java Development Kit", "1.6", '6')
352 elif package == 'openjdk-7-jre':
353 find_java("Java Runtime Environment", "1.7", '7')
354 elif package == 'openjdk-7-jdk':
355 find_java("Java Development Kit", "1.7", '7')
357 def get_score(self, disto_name):
358 return int(disto_name == 'Darwin')
360 class CachedDistribution(Distribution):
361 """For distributions where querying the package database is slow (e.g. requires running
362 an external command), we cache the results.
363 @since: 0.39
364 @deprecated: use Cache instead
367 def __init__(self, db_status_file):
368 """@param db_status_file: update the cache when the timestamp of this file changes"""
369 self._status_details = os.stat(db_status_file)
371 self.versions = {}
372 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
373 namespaces.config_prog)
375 try:
376 self._load_cache()
377 except Exception as ex:
378 logger.info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
379 try:
380 self.generate_cache()
381 self._load_cache()
382 except Exception as ex:
383 logger.warn(_("Failed to regenerate distribution database cache: %s"), ex)
385 def _load_cache(self):
386 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
387 Throws an exception if the cache should be (re)created."""
388 with open(os.path.join(self.cache_dir, self.cache_leaf), 'rt') as stream:
389 cache_version = None
390 for line in stream:
391 if line == '\n':
392 break
393 name, value = line.split(': ')
394 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
395 raise Exception(_("Modification time of package database file has changed"))
396 if name == 'size' and int(value) != self._status_details.st_size:
397 raise Exception(_("Size of package database file has changed"))
398 if name == 'version':
399 cache_version = int(value)
400 else:
401 raise Exception(_('Invalid cache format (bad header)'))
403 if cache_version is None:
404 raise Exception(_('Old cache format'))
406 versions = self.versions
407 for line in stream:
408 package, version, zi_arch = line[:-1].split('\t')
409 versionarch = (version, intern(zi_arch))
410 if package not in versions:
411 versions[package] = [versionarch]
412 else:
413 versions[package].append(versionarch)
415 def _write_cache(self, cache):
416 #cache.sort() # Might be useful later; currently we don't care
417 import tempfile
418 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
419 dir = self.cache_dir)
420 try:
421 stream = os.fdopen(fd, 'wt')
422 stream.write('version: 2\n')
423 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
424 stream.write('size: %d\n' % self._status_details.st_size)
425 stream.write('\n')
426 for line in cache:
427 stream.write(line + '\n')
428 stream.close()
430 portable_rename(tmpname,
431 os.path.join(self.cache_dir,
432 self.cache_leaf))
433 except:
434 os.unlink(tmpname)
435 raise
437 # Maps machine type names used in packages to their Zero Install versions
438 # (updates to this might require changing the reverse Java mapping)
439 _canonical_machine = {
440 'all' : '*',
441 'any' : '*',
442 'noarch' : '*',
443 '(none)' : '*',
444 'x86_64': 'x86_64',
445 'amd64': 'x86_64',
446 'i386': 'i386',
447 'i486': 'i486',
448 'i586': 'i586',
449 'i686': 'i686',
450 'ppc64': 'ppc64',
451 'ppc': 'ppc',
454 host_machine = arch.canonicalize_machine(platform.uname()[4])
455 def canonical_machine(package_machine):
456 machine = _canonical_machine.get(package_machine, None)
457 if machine is None:
458 # Safe default if we can't understand the arch
459 return host_machine
460 return machine
462 class DebianDistribution(Distribution):
463 """A dpkg-based distribution."""
465 cache_leaf = 'dpkg-status.cache'
467 def __init__(self, dpkg_status):
468 self.dpkg_cache = Cache('dpkg-status.cache', dpkg_status, 2)
469 self.apt_cache = {}
471 def _query_installed_package(self, package):
472 null = os.open(os.devnull, os.O_WRONLY)
473 child = subprocess.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package],
474 stdout = subprocess.PIPE, stderr = null,
475 universal_newlines = True) # Needed for Python 3
476 os.close(null)
477 stdout, stderr = child.communicate()
478 child.wait()
479 for line in stdout.split('\n'):
480 if not line: continue
481 version, debarch, status = line.split('\t', 2)
482 if not status.endswith(' installed'): continue
483 clean_version = try_cleanup_distro_version(version)
484 if debarch.find("-") != -1:
485 debarch = debarch.split("-")[-1]
486 if clean_version:
487 return '%s\t%s' % (clean_version, canonical_machine(debarch.strip()))
488 else:
489 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
491 return '-'
493 def get_package_info(self, package, factory):
494 # Add any already-installed package...
495 installed_cached_info = self._get_dpkg_info(package)
497 if installed_cached_info != '-':
498 installed_version, machine = installed_cached_info.split('\t')
499 impl = factory('package:deb:%s:%s:%s' % (package, installed_version, machine))
500 impl.version = model.parse_version(installed_version)
501 if machine != '*':
502 impl.machine = machine
503 else:
504 installed_version = None
506 # Add any uninstalled candidates (note: only one of these two methods will add anything)
508 # From PackageKit...
509 self.packagekit.get_candidates(package, factory, 'package:deb')
511 # From apt-cache...
512 cached = self.apt_cache.get(package, None)
513 if cached:
514 candidate_version = cached['version']
515 candidate_arch = cached['arch']
516 if candidate_version and candidate_version != installed_version:
517 impl = factory('package:deb:%s:%s:%s' % (package, candidate_version, candidate_arch), installed = False)
518 impl.version = model.parse_version(candidate_version)
519 if candidate_arch != '*':
520 impl.machine = candidate_arch
521 def install(handler):
522 raise model.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
523 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
524 "use that to install it.") % package)
525 impl.download_sources.append(model.DistributionSource(package, cached['size'], install, needs_confirmation = False))
527 def fixup(self, package, impl):
528 if impl.id.startswith('package:deb:openjdk-6-jre:') or \
529 impl.id.startswith('package:deb:openjdk-7-jre:'):
530 # Debian marks all Java versions as pre-releases
531 # See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=685276
532 impl.version = model.parse_version(impl.get_version().replace('-pre', '.'))
534 def installed_fixup(self, impl):
535 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
536 if impl.id.startswith('package:deb:openjdk-6-jre:'):
537 java_version = '6-openjdk'
538 elif impl.id.startswith('package:deb:openjdk-7-jre:'):
539 java_version = '7-openjdk'
540 else:
541 return
543 if impl.machine == 'x86_64':
544 java_arch = 'amd64'
545 else:
546 java_arch = impl.machine
548 java_bin = '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version, java_arch)
549 if not os.path.exists(java_bin):
550 # Try without the arch...
551 java_bin = '/usr/lib/jvm/java-%s/jre/bin/java' % java_version
552 if not os.path.exists(java_bin):
553 logger.info("Java binary not found (%s)", java_bin)
554 if impl.main is None:
555 java_bin = '/usr/bin/java'
556 else:
557 return
559 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command',
560 {'path': java_bin, 'name': 'run'}), None)
562 def get_score(self, disto_name):
563 return int(disto_name == 'Debian')
565 def _get_dpkg_info(self, package):
566 installed_cached_info = self.dpkg_cache.get(package)
567 if installed_cached_info == None:
568 installed_cached_info = self._query_installed_package(package)
569 self.dpkg_cache.put(package, installed_cached_info)
571 return installed_cached_info
573 def fetch_candidates(self, master_feed):
574 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
576 if self.packagekit.available:
577 return self.packagekit.fetch_candidates(package_names)
579 # No PackageKit. Use apt-cache directly.
580 for package in package_names:
581 # Check to see whether we could get a newer version using apt-get
582 try:
583 null = os.open(os.devnull, os.O_WRONLY)
584 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
585 os.close(null)
587 arch = version = size = None
588 for line in child.stdout:
589 line = line.strip()
590 if line.startswith('Version: '):
591 version = line[9:]
592 version = try_cleanup_distro_version(version)
593 elif line.startswith('Architecture: '):
594 arch = canonical_machine(line[14:].strip())
595 elif line.startswith('Size: '):
596 size = int(line[6:].strip())
597 if version and arch:
598 cached = {'version': version, 'arch': arch, 'size': size}
599 else:
600 cached = None
601 child.stdout.close()
602 child.wait()
603 except Exception as ex:
604 logger.warn("'apt-cache show %s' failed: %s", package, ex)
605 cached = None
606 # (multi-arch support? can there be multiple candidates?)
607 self.apt_cache[package] = cached
609 class RPMDistribution(CachedDistribution):
610 """An RPM-based distribution."""
612 cache_leaf = 'rpm-status.cache'
614 def generate_cache(self):
615 cache = []
617 child = subprocess.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
618 stdout = subprocess.PIPE, universal_newlines = True)
619 for line in child.stdout:
620 package, version, rpmarch = line.split('\t', 2)
621 if package == 'gpg-pubkey':
622 continue
623 zi_arch = canonical_machine(rpmarch.strip())
624 clean_version = try_cleanup_distro_version(version)
625 if clean_version:
626 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
627 else:
628 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
630 self._write_cache(cache)
631 child.stdout.close()
632 child.wait()
634 def get_package_info(self, package, factory):
635 # Add installed versions...
636 versions = self.versions.get(package, [])
638 for version, machine in versions:
639 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
640 impl.version = model.parse_version(version)
641 if machine != '*':
642 impl.machine = machine
644 # Add any uninstalled candidates found by PackageKit
645 self.packagekit.get_candidates(package, factory, 'package:rpm')
647 def installed_fixup(self, impl):
648 # OpenSUSE uses _, Fedora uses .
649 impl_id = impl.id.replace('_', '.')
651 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
653 if impl_id.startswith('package:rpm:java-1.6.0-openjdk:'):
654 java_version = '1.6.0-openjdk'
655 elif impl_id.startswith('package:rpm:java-1.7.0-openjdk:'):
656 java_version = '1.7.0-openjdk'
657 else:
658 return
660 # On Fedora, unlike Debian, the arch is x86_64, not amd64
662 java_bin = '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version, impl.machine)
663 if not os.path.exists(java_bin):
664 # Try without the arch...
665 java_bin = '/usr/lib/jvm/jre-%s/bin/java' % java_version
666 if not os.path.exists(java_bin):
667 logger.info("Java binary not found (%s)", java_bin)
668 if impl.main is None:
669 java_bin = '/usr/bin/java'
670 else:
671 return
673 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command',
674 {'path': java_bin, 'name': 'run'}), None)
676 def fixup(self, package, impl):
677 # OpenSUSE uses _, Fedora uses .
678 package = package.replace('_', '.')
680 if package in ('java-1.6.0-openjdk', 'java-1.7.0-openjdk',
681 'java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'):
682 if impl.version[0][0] == 1:
683 # OpenSUSE uses 1.6 to mean 6
684 del impl.version[0][0]
686 def get_score(self, disto_name):
687 return int(disto_name == 'RPM')
689 class SlackDistribution(Distribution):
690 """A Slack-based distribution."""
692 def __init__(self, packages_dir):
693 self._packages_dir = packages_dir
695 def get_package_info(self, package, factory):
696 # Add installed versions...
697 for entry in os.listdir(self._packages_dir):
698 name, version, arch, build = entry.rsplit('-', 3)
699 if name == package:
700 zi_arch = canonical_machine(arch)
701 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
702 if not clean_version:
703 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
704 continue
706 impl = factory('package:slack:%s:%s:%s' % \
707 (package, clean_version, zi_arch))
708 impl.version = model.parse_version(clean_version)
709 if zi_arch != '*':
710 impl.machine = zi_arch
712 # Add any uninstalled candidates found by PackageKit
713 self.packagekit.get_candidates(package, factory, 'package:slack')
715 def get_score(self, disto_name):
716 return int(disto_name == 'Slack')
718 class ArchDistribution(Distribution):
719 """An Arch Linux distribution."""
721 def __init__(self, packages_dir):
722 self._packages_dir = os.path.join(packages_dir, "local")
724 def get_package_info(self, package, factory):
725 # Add installed versions...
726 for entry in os.listdir(self._packages_dir):
727 name, version, build = entry.rsplit('-', 2)
728 if name == package:
729 gotarch = False
730 with open(os.path.join(self._packages_dir, entry, "desc"), 'rt') as stream:
731 for line in stream:
732 if line == "%ARCH%\n":
733 gotarch = True
734 continue
735 if gotarch:
736 arch = line.strip()
737 break
738 zi_arch = canonical_machine(arch)
739 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
740 if not clean_version:
741 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
742 continue
744 impl = factory('package:arch:%s:%s:%s' % \
745 (package, clean_version, zi_arch))
746 impl.version = model.parse_version(clean_version)
747 if zi_arch != '*':
748 impl.machine = zi_arch
750 # Add any uninstalled candidates found by PackageKit
751 self.packagekit.get_candidates(package, factory, 'package:arch')
753 def get_score(self, disto_name):
754 return int(disto_name == 'Arch')
756 class GentooDistribution(Distribution):
758 def __init__(self, pkgdir):
759 self._pkgdir = pkgdir
761 def get_package_info(self, package, factory):
762 # Add installed versions...
763 _version_start_reqexp = '-[0-9]'
765 if package.count('/') != 1: return
767 category, leafname = package.split('/')
768 category_dir = os.path.join(self._pkgdir, category)
769 match_prefix = leafname + '-'
771 if not os.path.isdir(category_dir): return
773 for filename in os.listdir(category_dir):
774 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit():
775 with open(os.path.join(category_dir, filename, 'PF'), 'rt') as stream:
776 name = stream.readline().strip()
778 match = re.search(_version_start_reqexp, name)
779 if match is None:
780 logger.warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name})
781 continue
782 else:
783 version = try_cleanup_distro_version(name[match.start() + 1:])
785 if category == 'app-emulation' and name.startswith('emul-'):
786 __, __, machine, __ = name.split('-', 3)
787 else:
788 with open(os.path.join(category_dir, filename, 'CHOST'), 'rt') as stream:
789 machine, __ = stream.readline().split('-', 1)
790 machine = arch.canonicalize_machine(machine)
792 impl = factory('package:gentoo:%s:%s:%s' % \
793 (package, version, machine))
794 impl.version = model.parse_version(version)
795 impl.machine = machine
797 # Add any uninstalled candidates found by PackageKit
798 self.packagekit.get_candidates(package, factory, 'package:gentoo')
800 def get_score(self, disto_name):
801 return int(disto_name == 'Gentoo')
803 class PortsDistribution(Distribution):
805 def __init__(self, pkgdir):
806 self._pkgdir = pkgdir
808 def get_package_info(self, package, factory):
809 _name_version_regexp = '^(.+)-([^-]+)$'
811 nameversion = re.compile(_name_version_regexp)
812 for pkgname in os.listdir(self._pkgdir):
813 pkgdir = os.path.join(self._pkgdir, pkgname)
814 if not os.path.isdir(pkgdir): continue
816 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
818 match = nameversion.search(pkgname)
819 if match is None:
820 logger.warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname})
821 continue
822 else:
823 name = match.group(1)
824 if name != package:
825 continue
826 version = try_cleanup_distro_version(match.group(2))
828 machine = host_machine
830 impl = factory('package:ports:%s:%s:%s' % \
831 (package, version, machine))
832 impl.version = model.parse_version(version)
833 impl.machine = machine
835 def get_score(self, disto_name):
836 return int(disto_name == 'Ports')
838 class MacPortsDistribution(CachedDistribution):
839 def __init__(self, db_status_file):
840 super(MacPortsDistribution, self).__init__(db_status_file)
841 self.darwin = DarwinDistribution()
843 cache_leaf = 'macports-status.cache'
845 def generate_cache(self):
846 cache = []
848 child = subprocess.Popen(["port", "-v", "installed"],
849 stdout = subprocess.PIPE, universal_newlines = True)
850 for line in child.stdout:
851 if not line.startswith(" "):
852 continue
853 if line.strip().count(" ") > 1:
854 package, version, extra = line.split(None, 2)
855 else:
856 package, version = line.split()
857 extra = ""
858 if not extra.startswith("(active)"):
859 continue
860 version = version.lstrip('@')
861 version = re.sub(r"\+.*", "", version) # strip variants
862 zi_arch = '*'
863 clean_version = try_cleanup_distro_version(version)
864 if clean_version:
865 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra)
866 if match:
867 platform, major, archs = match.groups()
868 for arch in archs.split():
869 zi_arch = canonical_machine(arch)
870 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
871 else:
872 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
873 else:
874 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
875 self._write_cache(cache)
876 child.stdout.close()
877 child.wait()
879 def get_package_info(self, package, factory):
880 self.darwin.get_package_info(package, factory)
882 # Add installed versions...
883 versions = self.versions.get(package, [])
885 for version, machine in versions:
886 impl = factory('package:macports:%s:%s:%s' % (package, version, machine))
887 impl.version = model.parse_version(version)
888 if machine != '*':
889 impl.machine = machine
891 def get_score(self, disto_name):
892 # We support both sources of packages.
893 # In theory, we should route 'Darwin' package names to DarwinDistribution, and
894 # Mac Ports names to MacPortsDistribution. But since we only use Darwin for Java,
895 # having one object handle both is OK.
896 return int(disto_name in ('Darwin', 'MacPorts'))
898 class CygwinDistribution(CachedDistribution):
899 """A Cygwin-based distribution."""
901 cache_leaf = 'cygcheck-status.cache'
903 def generate_cache(self):
904 cache = []
906 zi_arch = canonical_machine(arch)
907 for line in os.popen("cygcheck -c -d"):
908 if line == "Cygwin Package Information\r\n":
909 continue
910 if line == "\n":
911 continue
912 package, version = line.split()
913 if package == "Package" and version == "Version":
914 continue
915 clean_version = try_cleanup_distro_version(version)
916 if clean_version:
917 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
918 else:
919 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
921 self._write_cache(cache)
923 def get_package_info(self, package, factory):
924 # Add installed versions...
925 versions = self.versions.get(package, [])
927 for version, machine in versions:
928 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine))
929 impl.version = model.parse_version(version)
930 if machine != '*':
931 impl.machine = machine
933 def get_score(self, disto_name):
934 return int(disto_name == 'Cygwin')
937 _host_distribution = None
938 def get_host_distribution():
939 """Get a Distribution suitable for the host operating system.
940 Calling this twice will return the same object.
941 @rtype: L{Distribution}"""
942 global _host_distribution
943 if not _host_distribution:
944 dpkg_db_status = '/var/lib/dpkg/status'
945 rpm_db_packages = '/var/lib/rpm/Packages'
946 _slack_db = '/var/log/packages'
947 _arch_db = '/var/lib/pacman'
948 _pkg_db = '/var/db/pkg'
949 _macports_db = '/opt/local/var/macports/registry/registry.db'
950 _cygwin_log = '/var/log/setup.log'
952 if sys.prefix == "/sw":
953 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status)
954 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages)
956 if os.name == "nt":
957 _host_distribution = WindowsDistribution()
958 elif os.path.isdir(_pkg_db):
959 if sys.platform.startswith("linux"):
960 _host_distribution = GentooDistribution(_pkg_db)
961 elif sys.platform.startswith("freebsd"):
962 _host_distribution = PortsDistribution(_pkg_db)
963 elif os.path.isfile(_macports_db) \
964 and sys.prefix.startswith("/opt/local"):
965 _host_distribution = MacPortsDistribution(_macports_db)
966 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin":
967 _host_distribution = CygwinDistribution(_cygwin_log)
968 elif os.access(dpkg_db_status, os.R_OK) \
969 and os.path.getsize(dpkg_db_status) > 0:
970 _host_distribution = DebianDistribution(dpkg_db_status)
971 elif os.path.isfile(rpm_db_packages):
972 _host_distribution = RPMDistribution(rpm_db_packages)
973 elif os.path.isdir(_slack_db):
974 _host_distribution = SlackDistribution(_slack_db)
975 elif os.path.isdir(_arch_db):
976 _host_distribution = ArchDistribution(_arch_db)
977 elif sys.platform == "darwin":
978 _host_distribution = DarwinDistribution()
979 else:
980 _host_distribution = Distribution()
982 return _host_distribution