Fixup Java versions on Debian
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob12b0b3fc4a007247b7f668d881aae00d28556cca
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 suffix = '-pre' + try_cleanup_distro_version(suffix)
119 else:
120 suffix = ''
121 match = re.match(_version_regexp, version)
122 if match:
123 major, version, revision = match.groups()
124 if major is not None:
125 version = major[:-1] + '.' + version
126 if revision is not None:
127 version = '%s-%s' % (version, revision[2:])
128 return version + suffix
129 return None
131 class Distribution(object):
132 """Represents a distribution with which we can integrate.
133 Sub-classes should specialise this to integrate with the package managers of
134 particular distributions. This base class ignores the native package manager.
135 @since: 0.28
137 _packagekit = None
139 def get_package_info(self, package, factory):
140 """Get information about the given package.
141 Add zero or more implementations using the factory (typically at most two
142 will be added; the currently installed version and the latest available).
143 @param package: package name (e.g. "gimp")
144 @type package: str
145 @param factory: function for creating new DistributionImplementation objects from IDs
146 @type factory: str -> L{model.DistributionImplementation}
148 return
150 def get_score(self, distribution):
151 """Indicate how closely the host distribution matches this one.
152 The <package-implementation> with the highest score is passed
153 to L{Distribution.get_package_info}. If several elements get
154 the same score, get_package_info is called for all of them.
155 @param distribution: a distribution name
156 @type distribution: str
157 @return: an integer, or -1 if there is no match at all
158 @rtype: int
160 return 0
162 def get_feed(self, master_feed):
163 """Generate a feed containing information about distribution packages.
164 This should immediately return a feed containing an implementation for the
165 package if it's already installed. Information about versions that could be
166 installed using the distribution's package manager can be added asynchronously
167 later (see L{fetch_candidates}).
168 @param master_feed: feed containing the <package-implementation> elements
169 @type master_feed: L{model.ZeroInstallFeed}
170 @rtype: L{model.ZeroInstallFeed}"""
172 feed = model.ZeroInstallFeed(None)
173 feed.url = 'distribution:' + master_feed.url
175 for item, item_attrs in master_feed.get_package_impls(self):
176 package = item_attrs.get('package', None)
177 if package is None:
178 raise model.InvalidInterface(_("Missing 'package' attribute on %s") % item)
180 new_impls = []
182 def factory(id, only_if_missing = False, installed = True):
183 assert id.startswith('package:')
184 if id in feed.implementations:
185 if only_if_missing:
186 return None
187 logger.warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
188 impl = model.DistributionImplementation(feed, id, self, item)
189 feed.implementations[id] = impl
190 new_impls.append(impl)
192 impl.installed = installed
193 impl.metadata = item_attrs
195 if 'run' not in impl.commands:
196 item_main = item_attrs.get('main', None)
197 if item_main:
198 if item_main.startswith('/'):
199 impl.main = item_main
200 else:
201 raise model.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
202 item_main)
203 impl.upstream_stability = model.packaged
205 return impl
207 self.get_package_info(package, factory)
209 for impl in new_impls:
210 self.fixup(package, impl)
212 if master_feed.url == 'http://repo.roscidus.com/python/python' and all(not impl.installed for impl in feed.implementations.values()):
213 # Hack: we can support Python on platforms with unsupported package managers
214 # by adding the implementation of Python running us now to the list.
215 python_version = '.'.join([str(v) for v in sys.version_info if isinstance(v, int)])
216 impl_id = 'package:host:python:' + python_version
217 assert impl_id not in feed.implementations
218 impl = model.DistributionImplementation(feed, impl_id, self)
219 impl.installed = True
220 impl.version = model.parse_version(python_version)
221 impl.main = sys.executable
222 impl.upstream_stability = model.packaged
223 impl.machine = host_machine # (hopefully)
224 feed.implementations[impl_id] = impl
226 return feed
228 def fetch_candidates(self, master_feed):
229 """Collect information about versions we could install using
230 the distribution's package manager. On success, the distribution
231 feed in iface_cache is updated.
232 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
233 if self.packagekit.available:
234 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
235 return self.packagekit.fetch_candidates(package_names)
237 @property
238 def packagekit(self):
239 """For use by subclasses.
240 @rtype: L{packagekit.PackageKit}"""
241 if not self._packagekit:
242 from zeroinstall.injector import packagekit
243 self._packagekit = packagekit.PackageKit()
244 return self._packagekit
246 def fixup(self, package, impl):
247 """Some packages require special handling (e.g. Java). This is called for each
248 package that was added by L{get_package_info} after it returns. The default
249 method does nothing.
250 @param package: the name of the package
251 @param impl: the constructed implementation"""
252 pass
254 class WindowsDistribution(Distribution):
255 def get_package_info(self, package, factory):
256 def _is_64bit_windows():
257 p = sys.platform
258 from win32process import IsWow64Process
259 if p == 'win64' or (p == 'win32' and IsWow64Process()): return True
260 elif p == 'win32': return False
261 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
263 def _read_hklm_reg(key_name, value_name):
264 from win32api import RegOpenKeyEx, RegQueryValueEx, RegCloseKey
265 from win32con import HKEY_LOCAL_MACHINE, KEY_READ
266 KEY_WOW64_64KEY = 0x0100
267 KEY_WOW64_32KEY = 0x0200
268 if _is_64bit_windows():
269 try:
270 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_32KEY)
271 (value32, _) = RegQueryValueEx(key32, value_name)
272 RegCloseKey(key32)
273 except:
274 value32 = ''
275 try:
276 key64 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_64KEY)
277 (value64, _) = RegQueryValueEx(key64, value_name)
278 RegCloseKey(key64)
279 except:
280 value64 = ''
281 else:
282 try:
283 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ)
284 (value32, _) = RegQueryValueEx(key32, value_name)
285 RegCloseKey(key32)
286 except:
287 value32 = ''
288 value64 = ''
289 return (value32, value64)
291 def find_java(part, win_version, zero_version):
292 reg_path = r"SOFTWARE\JavaSoft\{part}\{win_version}".format(part = part, win_version = win_version)
293 (java32_home, java64_home) = _read_hklm_reg(reg_path, "JavaHome")
295 for (home, arch) in [(java32_home, 'i486'),
296 (java64_home, 'x86_64')]:
297 if os.path.isfile(home + r"\bin\java.exe"):
298 impl = factory('package:windows:%s:%s:%s' % (package, zero_version, arch))
299 impl.machine = arch
300 impl.version = model.parse_version(zero_version)
301 impl.upstream_stability = model.packaged
302 impl.main = home + r"\bin\java.exe"
304 if package == 'openjdk-6-jre':
305 find_java("Java Runtime Environment", "1.6", '6')
306 elif package == 'openjdk-6-jdk':
307 find_java("Java Development Kit", "1.6", '6')
308 elif package == 'openjdk-7-jre':
309 find_java("Java Runtime Environment", "1.7", '7')
310 elif package == 'openjdk-7-jdk':
311 find_java("Java Development Kit", "1.7", '7')
313 def get_score(self, disto_name):
314 return int(disto_name == 'Windows')
316 class DarwinDistribution(Distribution):
317 def get_package_info(self, package, factory):
318 def java_home(version, arch):
319 null = os.open(os.devnull, os.O_WRONLY)
320 child = subprocess.Popen(["/usr/libexec/java_home", "--failfast", "--version", version, "--arch", arch],
321 stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
322 home = child.stdout.read().strip()
323 child.stdout.close()
324 child.wait()
325 return home
327 def find_java(part, jvm_version, zero_version):
328 for arch in ['i386', 'x86_64']:
329 home = java_home(jvm_version, arch)
330 if os.path.isfile(home + "/bin/java"):
331 impl = factory('package:darwin:%s:%s:%s' % (package, zero_version, arch))
332 impl.machine = arch
333 impl.version = model.parse_version(zero_version)
334 impl.upstream_stability = model.packaged
335 impl.main = home + "/bin/java"
337 if package == 'openjdk-6-jre':
338 find_java("Java Runtime Environment", "1.6", '6')
339 elif package == 'openjdk-6-jdk':
340 find_java("Java Development Kit", "1.6", '6')
341 elif package == 'openjdk-7-jre':
342 find_java("Java Runtime Environment", "1.7", '7')
343 elif package == 'openjdk-7-jdk':
344 find_java("Java Development Kit", "1.7", '7')
346 def get_score(self, disto_name):
347 return int(disto_name == 'Darwin')
349 class CachedDistribution(Distribution):
350 """For distributions where querying the package database is slow (e.g. requires running
351 an external command), we cache the results.
352 @since: 0.39
353 @deprecated: use Cache instead
356 def __init__(self, db_status_file):
357 """@param db_status_file: update the cache when the timestamp of this file changes"""
358 self._status_details = os.stat(db_status_file)
360 self.versions = {}
361 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
362 namespaces.config_prog)
364 try:
365 self._load_cache()
366 except Exception as ex:
367 logger.info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
368 try:
369 self.generate_cache()
370 self._load_cache()
371 except Exception as ex:
372 logger.warn(_("Failed to regenerate distribution database cache: %s"), ex)
374 def _load_cache(self):
375 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
376 Throws an exception if the cache should be (re)created."""
377 with open(os.path.join(self.cache_dir, self.cache_leaf), 'rt') as stream:
378 cache_version = None
379 for line in stream:
380 if line == '\n':
381 break
382 name, value = line.split(': ')
383 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
384 raise Exception(_("Modification time of package database file has changed"))
385 if name == 'size' and int(value) != self._status_details.st_size:
386 raise Exception(_("Size of package database file has changed"))
387 if name == 'version':
388 cache_version = int(value)
389 else:
390 raise Exception(_('Invalid cache format (bad header)'))
392 if cache_version is None:
393 raise Exception(_('Old cache format'))
395 versions = self.versions
396 for line in stream:
397 package, version, zi_arch = line[:-1].split('\t')
398 versionarch = (version, intern(zi_arch))
399 if package not in versions:
400 versions[package] = [versionarch]
401 else:
402 versions[package].append(versionarch)
404 def _write_cache(self, cache):
405 #cache.sort() # Might be useful later; currently we don't care
406 import tempfile
407 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
408 dir = self.cache_dir)
409 try:
410 stream = os.fdopen(fd, 'wt')
411 stream.write('version: 2\n')
412 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
413 stream.write('size: %d\n' % self._status_details.st_size)
414 stream.write('\n')
415 for line in cache:
416 stream.write(line + '\n')
417 stream.close()
419 portable_rename(tmpname,
420 os.path.join(self.cache_dir,
421 self.cache_leaf))
422 except:
423 os.unlink(tmpname)
424 raise
426 # Maps machine type names used in packages to their Zero Install versions
427 # (updates to this might require changing the reverse Java mapping)
428 _canonical_machine = {
429 'all' : '*',
430 'any' : '*',
431 'noarch' : '*',
432 '(none)' : '*',
433 'x86_64': 'x86_64',
434 'amd64': 'x86_64',
435 'i386': 'i386',
436 'i486': 'i486',
437 'i586': 'i586',
438 'i686': 'i686',
439 'ppc64': 'ppc64',
440 'ppc': 'ppc',
443 host_machine = arch.canonicalize_machine(platform.uname()[4])
444 def canonical_machine(package_machine):
445 machine = _canonical_machine.get(package_machine, None)
446 if machine is None:
447 # Safe default if we can't understand the arch
448 return host_machine
449 return machine
451 class DebianDistribution(Distribution):
452 """A dpkg-based distribution."""
454 cache_leaf = 'dpkg-status.cache'
456 def __init__(self, dpkg_status):
457 self.dpkg_cache = Cache('dpkg-status.cache', dpkg_status, 2)
458 self.apt_cache = {}
460 def _query_installed_package(self, package):
461 null = os.open(os.devnull, os.O_WRONLY)
462 child = subprocess.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package],
463 stdout = subprocess.PIPE, stderr = null,
464 universal_newlines = True) # Needed for Python 3
465 os.close(null)
466 stdout, stderr = child.communicate()
467 child.wait()
468 for line in stdout.split('\n'):
469 if not line: continue
470 version, debarch, status = line.split('\t', 2)
471 if not status.endswith(' installed'): continue
472 clean_version = try_cleanup_distro_version(version)
473 if debarch.find("-") != -1:
474 debarch = debarch.split("-")[-1]
475 if clean_version:
476 return '%s\t%s' % (clean_version, canonical_machine(debarch.strip()))
477 else:
478 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
480 return '-'
482 def get_package_info(self, package, factory):
483 # Add any already-installed package...
484 installed_cached_info = self._get_dpkg_info(package)
486 if installed_cached_info != '-':
487 installed_version, machine = installed_cached_info.split('\t')
488 impl = factory('package:deb:%s:%s:%s' % (package, installed_version, machine))
489 impl.version = model.parse_version(installed_version)
490 if machine != '*':
491 impl.machine = machine
492 else:
493 installed_version = None
495 # Add any uninstalled candidates (note: only one of these two methods will add anything)
497 # From PackageKit...
498 self.packagekit.get_candidates(package, factory, 'package:deb')
500 # From apt-cache...
501 cached = self.apt_cache.get(package, None)
502 if cached:
503 candidate_version = cached['version']
504 candidate_arch = cached['arch']
505 if candidate_version and candidate_version != installed_version:
506 impl = factory('package:deb:%s:%s:%s' % (package, candidate_version, candidate_arch), installed = False)
507 impl.version = model.parse_version(candidate_version)
508 if candidate_arch != '*':
509 impl.machine = candidate_arch
510 def install(handler):
511 raise model.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
512 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
513 "use that to install it.") % package)
514 impl.download_sources.append(model.DistributionSource(package, cached['size'], install, needs_confirmation = False))
516 def fixup(self, package, impl):
517 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
518 if package == 'openjdk-6-jre':
519 java_version = '6-openjdk'
520 elif package == 'openjdk-7-jre':
521 java_version = '7-openjdk'
522 else:
523 return
525 # Debian marks all Java versions as pre-releases
526 # See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=685276
527 impl.version = model.parse_version(impl.get_version().replace('-pre', '.'))
529 if impl.machine == 'x86_64':
530 java_arch = 'amd64'
531 else:
532 java_arch = impl.machine
534 java_bin = '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version, java_arch)
535 if not os.path.exists(java_bin):
536 # Try without the arch...
537 java_bin = '/usr/lib/jvm/java-%s/jre/bin/java' % java_version
538 if not os.path.exists(java_bin):
539 logger.info("Java binary not found (%s)", java_bin)
540 if impl.main is None:
541 java_bin = '/usr/bin/java'
542 else:
543 return
545 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command',
546 {'path': java_bin, 'name': 'run'}), None)
548 def get_score(self, disto_name):
549 return int(disto_name == 'Debian')
551 def _get_dpkg_info(self, package):
552 installed_cached_info = self.dpkg_cache.get(package)
553 if installed_cached_info == None:
554 installed_cached_info = self._query_installed_package(package)
555 self.dpkg_cache.put(package, installed_cached_info)
557 return installed_cached_info
559 def fetch_candidates(self, master_feed):
560 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
562 if self.packagekit.available:
563 return self.packagekit.fetch_candidates(package_names)
565 # No PackageKit. Use apt-cache directly.
566 for package in package_names:
567 # Check to see whether we could get a newer version using apt-get
568 try:
569 null = os.open(os.devnull, os.O_WRONLY)
570 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
571 os.close(null)
573 arch = version = size = None
574 for line in child.stdout:
575 line = line.strip()
576 if line.startswith('Version: '):
577 version = line[9:]
578 version = try_cleanup_distro_version(version)
579 elif line.startswith('Architecture: '):
580 arch = canonical_machine(line[14:].strip())
581 elif line.startswith('Size: '):
582 size = int(line[6:].strip())
583 if version and arch:
584 cached = {'version': version, 'arch': arch, 'size': size}
585 else:
586 cached = None
587 child.stdout.close()
588 child.wait()
589 except Exception as ex:
590 logger.warn("'apt-cache show %s' failed: %s", package, ex)
591 cached = None
592 # (multi-arch support? can there be multiple candidates?)
593 self.apt_cache[package] = cached
595 class RPMDistribution(CachedDistribution):
596 """An RPM-based distribution."""
598 cache_leaf = 'rpm-status.cache'
600 def generate_cache(self):
601 cache = []
603 child = subprocess.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
604 stdout = subprocess.PIPE, universal_newlines = True)
605 for line in child.stdout:
606 package, version, rpmarch = line.split('\t', 2)
607 if package == 'gpg-pubkey':
608 continue
609 zi_arch = canonical_machine(rpmarch.strip())
610 clean_version = try_cleanup_distro_version(version)
611 if clean_version:
612 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
613 else:
614 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
616 self._write_cache(cache)
617 child.stdout.close()
618 child.wait()
620 def get_package_info(self, package, factory):
621 # Add installed versions...
622 versions = self.versions.get(package, [])
624 for version, machine in versions:
625 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
626 impl.version = model.parse_version(version)
627 if machine != '*':
628 impl.machine = machine
630 # Add any uninstalled candidates found by PackageKit
631 self.packagekit.get_candidates(package, factory, 'package:rpm')
633 def fixup(self, package, impl):
634 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
636 # OpenSUSE uses _, Fedora uses .
637 package = package.replace('_', '.')
639 if package == 'java-1.6.0-openjdk':
640 java_version = '1.6.0-openjdk'
641 elif package == 'java-1.7.0-openjdk':
642 java_version = '1.7.0-openjdk'
643 elif package in ('java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'):
644 if impl.version[0][0] == 1:
645 # OpenSUSE uses 1.6 to mean 6
646 del impl.version[0][0]
647 return
648 else:
649 return
651 if impl.version[0][0] == 1:
652 # OpenSUSE uses 1.6 to mean 6
653 del impl.version[0][0]
655 # On Fedora, unlike Debian, the arch is x86_64, not amd64
657 java_bin = '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version, impl.machine)
658 if not os.path.exists(java_bin):
659 # Try without the arch...
660 java_bin = '/usr/lib/jvm/jre-%s/bin/java' % java_version
661 if not os.path.exists(java_bin):
662 logger.info("Java binary not found (%s)", java_bin)
663 if impl.main is None:
664 java_bin = '/usr/bin/java'
665 else:
666 return
668 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command',
669 {'path': java_bin, 'name': 'run'}), None)
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):
825 cache_leaf = 'macports-status.cache'
827 def generate_cache(self):
828 cache = []
830 child = subprocess.Popen(["port", "-v", "installed"],
831 stdout = subprocess.PIPE, universal_newlines = True)
832 for line in child.stdout:
833 if not line.startswith(" "):
834 continue
835 if line.strip().count(" ") > 1:
836 package, version, extra = line.split(None, 2)
837 else:
838 package, version = line.split()
839 extra = ""
840 if not extra.startswith("(active)"):
841 continue
842 version = version.lstrip('@')
843 version = re.sub(r"\+.*", "", version) # strip variants
844 zi_arch = '*'
845 clean_version = try_cleanup_distro_version(version)
846 if clean_version:
847 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra)
848 if match:
849 platform, major, archs = match.groups()
850 for arch in archs.split():
851 zi_arch = canonical_machine(arch)
852 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
853 else:
854 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
855 else:
856 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
857 self._write_cache(cache)
858 child.stdout.close()
859 child.wait()
861 def get_package_info(self, package, factory):
862 # Add installed versions...
863 versions = self.versions.get(package, [])
865 for version, machine in versions:
866 impl = factory('package:macports:%s:%s:%s' % (package, version, machine))
867 impl.version = model.parse_version(version)
868 if machine != '*':
869 impl.machine = machine
871 def get_score(self, disto_name):
872 return int(disto_name == 'MacPorts')
874 class CygwinDistribution(CachedDistribution):
875 """A Cygwin-based distribution."""
877 cache_leaf = 'cygcheck-status.cache'
879 def generate_cache(self):
880 cache = []
882 zi_arch = canonical_machine(arch)
883 for line in os.popen("cygcheck -c -d"):
884 if line == "Cygwin Package Information\r\n":
885 continue
886 if line == "\n":
887 continue
888 package, version = line.split()
889 if package == "Package" and version == "Version":
890 continue
891 clean_version = try_cleanup_distro_version(version)
892 if clean_version:
893 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
894 else:
895 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
897 self._write_cache(cache)
899 def get_package_info(self, package, factory):
900 # Add installed versions...
901 versions = self.versions.get(package, [])
903 for version, machine in versions:
904 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine))
905 impl.version = model.parse_version(version)
906 if machine != '*':
907 impl.machine = machine
909 def get_score(self, disto_name):
910 return int(disto_name == 'Cygwin')
913 _host_distribution = None
914 def get_host_distribution():
915 """Get a Distribution suitable for the host operating system.
916 Calling this twice will return the same object.
917 @rtype: L{Distribution}"""
918 global _host_distribution
919 if not _host_distribution:
920 dpkg_db_status = '/var/lib/dpkg/status'
921 rpm_db_packages = '/var/lib/rpm/Packages'
922 _slack_db = '/var/log/packages'
923 _arch_db = '/var/lib/pacman'
924 _pkg_db = '/var/db/pkg'
925 _macports_db = '/opt/local/var/macports/registry/registry.db'
926 _cygwin_log = '/var/log/setup.log'
928 if sys.prefix == "/sw":
929 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status)
930 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages)
932 if os.name == "nt":
933 _host_distribution = WindowsDistribution()
934 elif os.path.isdir(_pkg_db):
935 if sys.platform.startswith("linux"):
936 _host_distribution = GentooDistribution(_pkg_db)
937 elif sys.platform.startswith("freebsd"):
938 _host_distribution = PortsDistribution(_pkg_db)
939 elif os.path.isfile(_macports_db) \
940 and sys.prefix.startswith("/opt/local"):
941 _host_distribution = MacPortsDistribution(_macports_db)
942 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin":
943 _host_distribution = CygwinDistribution(_cygwin_log)
944 elif os.access(dpkg_db_status, os.R_OK) \
945 and os.path.getsize(dpkg_db_status) > 0:
946 _host_distribution = DebianDistribution(dpkg_db_status)
947 elif os.path.isfile(rpm_db_packages):
948 _host_distribution = RPMDistribution(rpm_db_packages)
949 elif os.path.isdir(_slack_db):
950 _host_distribution = SlackDistribution(_slack_db)
951 elif os.path.isdir(_arch_db):
952 _host_distribution = ArchDistribution(_arch_db)
953 elif sys.platform == "darwin":
954 _host_distribution = DarwinDistribution()
955 else:
956 _host_distribution = Distribution()
958 return _host_distribution