Fixed Python 3 whitespace error in 0alias
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob4894114202facbce91b53acc1297928020a76b3a
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 if impl.machine == 'x86_64':
526 java_arch = 'amd64'
527 else:
528 java_arch = impl.machine
530 java_bin = '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version, java_arch)
531 if not os.path.exists(java_bin):
532 # Try without the arch...
533 java_bin = '/usr/lib/jvm/java-%s/jre/bin/java' % java_version
534 if not os.path.exists(java_bin):
535 logger.info("Java binary not found (%s)", java_bin)
536 if impl.main is None:
537 java_bin = '/usr/bin/java'
538 else:
539 return
541 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command',
542 {'path': java_bin, 'name': 'run'}), None)
544 def get_score(self, disto_name):
545 return int(disto_name == 'Debian')
547 def _get_dpkg_info(self, package):
548 installed_cached_info = self.dpkg_cache.get(package)
549 if installed_cached_info == None:
550 installed_cached_info = self._query_installed_package(package)
551 self.dpkg_cache.put(package, installed_cached_info)
553 return installed_cached_info
555 def fetch_candidates(self, master_feed):
556 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
558 if self.packagekit.available:
559 return self.packagekit.fetch_candidates(package_names)
561 # No PackageKit. Use apt-cache directly.
562 for package in package_names:
563 # Check to see whether we could get a newer version using apt-get
564 try:
565 null = os.open(os.devnull, os.O_WRONLY)
566 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
567 os.close(null)
569 arch = version = size = None
570 for line in child.stdout:
571 line = line.strip()
572 if line.startswith('Version: '):
573 version = line[9:]
574 version = try_cleanup_distro_version(version)
575 elif line.startswith('Architecture: '):
576 arch = canonical_machine(line[14:].strip())
577 elif line.startswith('Size: '):
578 size = int(line[6:].strip())
579 if version and arch:
580 cached = {'version': version, 'arch': arch, 'size': size}
581 else:
582 cached = None
583 child.stdout.close()
584 child.wait()
585 except Exception as ex:
586 logger.warn("'apt-cache show %s' failed: %s", package, ex)
587 cached = None
588 # (multi-arch support? can there be multiple candidates?)
589 self.apt_cache[package] = cached
591 class RPMDistribution(CachedDistribution):
592 """An RPM-based distribution."""
594 cache_leaf = 'rpm-status.cache'
596 def generate_cache(self):
597 cache = []
599 child = subprocess.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
600 stdout = subprocess.PIPE, universal_newlines = True)
601 for line in child.stdout:
602 package, version, rpmarch = line.split('\t', 2)
603 if package == 'gpg-pubkey':
604 continue
605 zi_arch = canonical_machine(rpmarch.strip())
606 clean_version = try_cleanup_distro_version(version)
607 if clean_version:
608 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
609 else:
610 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
612 self._write_cache(cache)
613 child.stdout.close()
614 child.wait()
616 def get_package_info(self, package, factory):
617 # Add installed versions...
618 versions = self.versions.get(package, [])
620 for version, machine in versions:
621 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
622 impl.version = model.parse_version(version)
623 if machine != '*':
624 impl.machine = machine
626 # Add any uninstalled candidates found by PackageKit
627 self.packagekit.get_candidates(package, factory, 'package:rpm')
629 def fixup(self, package, impl):
630 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
632 # OpenSUSE uses _, Fedora uses .
633 package = package.replace('_', '.')
635 if package == 'java-1.6.0-openjdk':
636 java_version = '1.6.0-openjdk'
637 elif package == 'java-1.7.0-openjdk':
638 java_version = '1.7.0-openjdk'
639 elif package in ('java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'):
640 if impl.version[0][0] == 1:
641 # OpenSUSE uses 1.6 to mean 6
642 del impl.version[0][0]
643 return
644 else:
645 return
647 if impl.version[0][0] == 1:
648 # OpenSUSE uses 1.6 to mean 6
649 del impl.version[0][0]
651 # On Fedora, unlike Debian, the arch is x86_64, not amd64
653 java_bin = '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version, impl.machine)
654 if not os.path.exists(java_bin):
655 # Try without the arch...
656 java_bin = '/usr/lib/jvm/jre-%s/bin/java' % java_version
657 if not os.path.exists(java_bin):
658 logger.info("Java binary not found (%s)", java_bin)
659 if impl.main is None:
660 java_bin = '/usr/bin/java'
661 else:
662 return
664 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command',
665 {'path': java_bin, 'name': 'run'}), None)
667 def get_score(self, disto_name):
668 return int(disto_name == 'RPM')
670 class SlackDistribution(Distribution):
671 """A Slack-based distribution."""
673 def __init__(self, packages_dir):
674 self._packages_dir = packages_dir
676 def get_package_info(self, package, factory):
677 # Add installed versions...
678 for entry in os.listdir(self._packages_dir):
679 name, version, arch, build = entry.rsplit('-', 3)
680 if name == package:
681 zi_arch = canonical_machine(arch)
682 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
683 if not clean_version:
684 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
685 continue
687 impl = factory('package:slack:%s:%s:%s' % \
688 (package, clean_version, zi_arch))
689 impl.version = model.parse_version(clean_version)
690 if zi_arch != '*':
691 impl.machine = zi_arch
693 # Add any uninstalled candidates found by PackageKit
694 self.packagekit.get_candidates(package, factory, 'package:slack')
696 def get_score(self, disto_name):
697 return int(disto_name == 'Slack')
699 class ArchDistribution(Distribution):
700 """An Arch Linux distribution."""
702 def __init__(self, packages_dir):
703 self._packages_dir = os.path.join(packages_dir, "local")
705 def get_package_info(self, package, factory):
706 # Add installed versions...
707 for entry in os.listdir(self._packages_dir):
708 name, version, build = entry.rsplit('-', 2)
709 if name == package:
710 gotarch = False
711 with open(os.path.join(self._packages_dir, entry, "desc"), 'rt') as stream:
712 for line in stream:
713 if line == "%ARCH%\n":
714 gotarch = True
715 continue
716 if gotarch:
717 arch = line.strip()
718 break
719 zi_arch = canonical_machine(arch)
720 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
721 if not clean_version:
722 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
723 continue
725 impl = factory('package:arch:%s:%s:%s' % \
726 (package, clean_version, zi_arch))
727 impl.version = model.parse_version(clean_version)
728 if zi_arch != '*':
729 impl.machine = zi_arch
731 # Add any uninstalled candidates found by PackageKit
732 self.packagekit.get_candidates(package, factory, 'package:arch')
734 def get_score(self, disto_name):
735 return int(disto_name == 'Arch')
737 class GentooDistribution(Distribution):
739 def __init__(self, pkgdir):
740 self._pkgdir = pkgdir
742 def get_package_info(self, package, factory):
743 # Add installed versions...
744 _version_start_reqexp = '-[0-9]'
746 if package.count('/') != 1: return
748 category, leafname = package.split('/')
749 category_dir = os.path.join(self._pkgdir, category)
750 match_prefix = leafname + '-'
752 if not os.path.isdir(category_dir): return
754 for filename in os.listdir(category_dir):
755 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit():
756 with open(os.path.join(category_dir, filename, 'PF'), 'rt') as stream:
757 name = stream.readline().strip()
759 match = re.search(_version_start_reqexp, name)
760 if match is None:
761 logger.warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name})
762 continue
763 else:
764 version = try_cleanup_distro_version(name[match.start() + 1:])
766 if category == 'app-emulation' and name.startswith('emul-'):
767 __, __, machine, __ = name.split('-', 3)
768 else:
769 with open(os.path.join(category_dir, filename, 'CHOST'), 'rt') as stream:
770 machine, __ = stream.readline().split('-', 1)
771 machine = arch.canonicalize_machine(machine)
773 impl = factory('package:gentoo:%s:%s:%s' % \
774 (package, version, machine))
775 impl.version = model.parse_version(version)
776 impl.machine = machine
778 # Add any uninstalled candidates found by PackageKit
779 self.packagekit.get_candidates(package, factory, 'package:gentoo')
781 def get_score(self, disto_name):
782 return int(disto_name == 'Gentoo')
784 class PortsDistribution(Distribution):
786 def __init__(self, pkgdir):
787 self._pkgdir = pkgdir
789 def get_package_info(self, package, factory):
790 _name_version_regexp = '^(.+)-([^-]+)$'
792 nameversion = re.compile(_name_version_regexp)
793 for pkgname in os.listdir(self._pkgdir):
794 pkgdir = os.path.join(self._pkgdir, pkgname)
795 if not os.path.isdir(pkgdir): continue
797 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
799 match = nameversion.search(pkgname)
800 if match is None:
801 logger.warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname})
802 continue
803 else:
804 name = match.group(1)
805 if name != package:
806 continue
807 version = try_cleanup_distro_version(match.group(2))
809 machine = host_machine
811 impl = factory('package:ports:%s:%s:%s' % \
812 (package, version, machine))
813 impl.version = model.parse_version(version)
814 impl.machine = machine
816 def get_score(self, disto_name):
817 return int(disto_name == 'Ports')
819 class MacPortsDistribution(CachedDistribution):
821 cache_leaf = 'macports-status.cache'
823 def generate_cache(self):
824 cache = []
826 child = subprocess.Popen(["port", "-v", "installed"],
827 stdout = subprocess.PIPE, universal_newlines = True)
828 for line in child.stdout:
829 if not line.startswith(" "):
830 continue
831 if line.strip().count(" ") > 1:
832 package, version, extra = line.split(None, 2)
833 else:
834 package, version = line.split()
835 extra = ""
836 if not extra.startswith("(active)"):
837 continue
838 version = version.lstrip('@')
839 version = re.sub(r"\+.*", "", version) # strip variants
840 zi_arch = '*'
841 clean_version = try_cleanup_distro_version(version)
842 if clean_version:
843 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra)
844 if match:
845 platform, major, archs = match.groups()
846 for arch in archs.split():
847 zi_arch = canonical_machine(arch)
848 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
849 else:
850 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
851 else:
852 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
853 self._write_cache(cache)
854 child.stdout.close()
855 child.wait()
857 def get_package_info(self, package, factory):
858 # Add installed versions...
859 versions = self.versions.get(package, [])
861 for version, machine in versions:
862 impl = factory('package:macports:%s:%s:%s' % (package, version, machine))
863 impl.version = model.parse_version(version)
864 if machine != '*':
865 impl.machine = machine
867 def get_score(self, disto_name):
868 return int(disto_name == 'MacPorts')
870 class CygwinDistribution(CachedDistribution):
871 """A Cygwin-based distribution."""
873 cache_leaf = 'cygcheck-status.cache'
875 def generate_cache(self):
876 cache = []
878 zi_arch = canonical_machine(arch)
879 for line in os.popen("cygcheck -c -d"):
880 if line == "Cygwin Package Information\r\n":
881 continue
882 if line == "\n":
883 continue
884 package, version = line.split()
885 if package == "Package" and version == "Version":
886 continue
887 clean_version = try_cleanup_distro_version(version)
888 if clean_version:
889 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
890 else:
891 logger.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
893 self._write_cache(cache)
895 def get_package_info(self, package, factory):
896 # Add installed versions...
897 versions = self.versions.get(package, [])
899 for version, machine in versions:
900 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine))
901 impl.version = model.parse_version(version)
902 if machine != '*':
903 impl.machine = machine
905 def get_score(self, disto_name):
906 return int(disto_name == 'Cygwin')
909 _host_distribution = None
910 def get_host_distribution():
911 """Get a Distribution suitable for the host operating system.
912 Calling this twice will return the same object.
913 @rtype: L{Distribution}"""
914 global _host_distribution
915 if not _host_distribution:
916 dpkg_db_status = '/var/lib/dpkg/status'
917 rpm_db_packages = '/var/lib/rpm/Packages'
918 _slack_db = '/var/log/packages'
919 _arch_db = '/var/lib/pacman'
920 _pkg_db = '/var/db/pkg'
921 _macports_db = '/opt/local/var/macports/registry/registry.db'
922 _cygwin_log = '/var/log/setup.log'
924 if sys.prefix == "/sw":
925 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status)
926 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages)
928 if os.name == "nt":
929 _host_distribution = WindowsDistribution()
930 elif os.path.isdir(_pkg_db):
931 if sys.platform.startswith("linux"):
932 _host_distribution = GentooDistribution(_pkg_db)
933 elif sys.platform.startswith("freebsd"):
934 _host_distribution = PortsDistribution(_pkg_db)
935 elif os.path.isfile(_macports_db) \
936 and sys.prefix.startswith("/opt/local"):
937 _host_distribution = MacPortsDistribution(_macports_db)
938 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin":
939 _host_distribution = CygwinDistribution(_cygwin_log)
940 elif os.access(dpkg_db_status, os.R_OK) \
941 and os.path.getsize(dpkg_db_status) > 0:
942 _host_distribution = DebianDistribution(dpkg_db_status)
943 elif os.path.isfile(rpm_db_packages):
944 _host_distribution = RPMDistribution(rpm_db_packages)
945 elif os.path.isdir(_slack_db):
946 _host_distribution = SlackDistribution(_slack_db)
947 elif os.path.isdir(_arch_db):
948 _host_distribution = ArchDistribution(_arch_db)
949 elif sys.platform == "darwin":
950 _host_distribution = DarwinDistribution()
951 else:
952 _host_distribution = Distribution()
954 return _host_distribution