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