Add Cygwin distribution support
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob874d6c03540d1a85e7f5cc9d0c8dc350acfbd4ce
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
13 from zeroinstall.support import basedir
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 bit is for Java-style 6b17 syntax)
22 _version_regexp = '({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, tmp_name = tempfile.mkstemp(dir = self.cache_dir)
55 data = "mtime=%d\nsize=%d\nformat=%d\n\n" % (mtime, size, self.format)
56 while data:
57 wrote = os.write(tmp, data)
58 data = data[wrote:]
59 os.rename(tmp_name, os.path.join(self.cache_dir, self.cache_leaf))
61 self._load_cache()
63 # Populate self.cache from our saved cache file.
64 # Throws an exception if the cache doesn't exist or has the wrong format.
65 def _load_cache(self):
66 self.cache = cache = {}
67 stream = open(os.path.join(self.cache_dir, self.cache_leaf))
68 try:
69 for line in stream:
70 line = line.strip()
71 if not line:
72 break
73 key, value = line.split('=', 1)
74 if key in ('mtime', 'size', 'format'):
75 self.cached_for[key] = int(value)
77 self._check_valid()
79 for line in stream:
80 key, value = line.split('=', 1)
81 cache[key] = value[:-1]
82 finally:
83 stream.close()
85 # Check the source file hasn't changed since we created the cache
86 def _check_valid(self):
87 info = os.stat(self.source)
88 if self.cached_for['mtime'] != int(info.st_mtime):
89 raise Exception("Modification time of %s has changed" % self.source)
90 if self.cached_for['size'] != info.st_size:
91 raise Exception("Size of %s has changed" % self.source)
92 if self.cached_for.get('format', None) != self.format:
93 raise Exception("Format of cache has changed")
95 def get(self, key):
96 try:
97 self._check_valid()
98 except Exception as ex:
99 info(_("Cache needs to be refreshed: %s"), ex)
100 self.flush()
101 return None
102 else:
103 return self.cache.get(key, None)
105 def put(self, key, value):
106 cache_path = os.path.join(self.cache_dir, self.cache_leaf)
107 self.cache[key] = value
108 try:
109 stream = open(cache_path, 'a')
110 try:
111 stream.write('%s=%s\n' % (key, value))
112 finally:
113 stream.close()
114 except Exception as ex:
115 warn("Failed to write to cache %s: %s=%s: %s", cache_path, key, value, ex)
117 def try_cleanup_distro_version(version):
118 """Try to turn a distribution version string into one readable by Zero Install.
119 We do this by stripping off anything we can't parse.
120 @return: the part we understood, or None if we couldn't parse anything
121 @rtype: str"""
122 if ':' in version:
123 version = version.split(':')[1] # Skip 'epoch'
124 version = version.replace('_', '-')
125 match = re.match(_version_regexp, version)
126 if match:
127 major, version, revision = match.groups()
128 if major is not None:
129 version = major[:-1] + '.' + version
130 if revision is None:
131 return version
132 else:
133 return '%s-%s' % (version, revision[2:])
134 return None
136 class Distribution(object):
137 """Represents a distribution with which we can integrate.
138 Sub-classes should specialise this to integrate with the package managers of
139 particular distributions. This base class ignores the native package manager.
140 @since: 0.28
142 _packagekit = None
144 def get_package_info(self, package, factory):
145 """Get information about the given package.
146 Add zero or more implementations using the factory (typically at most two
147 will be added; the currently installed version and the latest available).
148 @param package: package name (e.g. "gimp")
149 @type package: str
150 @param factory: function for creating new DistributionImplementation objects from IDs
151 @type factory: str -> L{model.DistributionImplementation}
153 return
155 def get_score(self, distribution):
156 """Indicate how closely the host distribution matches this one.
157 The <package-implementation> with the highest score is passed
158 to L{Distribution.get_package_info}. If several elements get
159 the same score, get_package_info is called for all of them.
160 @param distribution: a distribution name
161 @type distribution: str
162 @return: an integer, or None if there is no match at all
163 @rtype: int | None
165 return 0
167 def get_feed(self, master_feed):
168 """Generate a feed containing information about distribution packages.
169 This should immediately return a feed containing an implementation for the
170 package if it's already installed. Information about versions that could be
171 installed using the distribution's package manager can be added asynchronously
172 later (see L{fetch_candidates}).
173 @param master_feed: feed containing the <package-implementation> elements
174 @type master_feed: L{model.ZeroInstallFeed}
175 @rtype: L{model.ZeroInstallFeed}"""
177 feed = model.ZeroInstallFeed(None)
178 feed.url = 'distribution:' + master_feed.url
180 for item, item_attrs in master_feed.get_package_impls(self):
181 package = item_attrs.get('package', None)
182 if package is None:
183 raise model.InvalidInterface(_("Missing 'package' attribute on %s") % item)
185 def factory(id, only_if_missing = False, installed = True):
186 assert id.startswith('package:')
187 if id in feed.implementations:
188 if only_if_missing:
189 return None
190 warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
191 impl = model.DistributionImplementation(feed, id, self)
192 feed.implementations[id] = impl
194 impl.installed = installed
195 impl.metadata = item_attrs
197 item_main = item_attrs.get('main', None)
198 if item_main and not item_main.startswith('/'):
199 raise model.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
200 item_main)
201 impl.main = item_main
202 impl.upstream_stability = model.packaged
204 return impl
206 self.get_package_info(package, factory)
208 if master_feed.url == 'http://repo.roscidus.com/python/python' and all(not impl.installed for impl in feed.implementations.values()):
209 # Hack: we can support Python on platforms with unsupported package managers
210 # by adding the implementation of Python running us now to the list.
211 python_version = '.'.join([str(v) for v in sys.version_info if isinstance(v, int)])
212 impl_id = 'package:host:python:' + python_version
213 assert impl_id not in feed.implementations
214 impl = model.DistributionImplementation(feed, impl_id, self)
215 impl.installed = True
216 impl.version = model.parse_version(python_version)
217 impl.main = sys.executable
218 impl.upstream_stability = model.packaged
219 impl.machine = host_machine # (hopefully)
220 feed.implementations[impl_id] = impl
222 return feed
224 def fetch_candidates(self, master_feed):
225 """Collect information about versions we could install using
226 the distribution's package manager. On success, the distribution
227 feed in iface_cache is updated.
228 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
229 if self.packagekit.available:
230 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
231 return self.packagekit.fetch_candidates(package_names)
233 @property
234 def packagekit(self):
235 """For use by subclasses.
236 @rtype: L{packagekit.PackageKit}"""
237 if not self._packagekit:
238 from zeroinstall.injector import packagekit
239 self._packagekit = packagekit.PackageKit()
240 return self._packagekit
242 class WindowsDistribution(Distribution):
243 def get_package_info(self, package, factory):
244 def _is_64bit_windows():
245 import sys
246 p = sys.platform
247 import platform
248 bits, linkage = platform.architecture()
249 from win32process import IsWow64Process
250 if p == 'win64' or (p == 'win32' and IsWow64Process()): return True
251 elif p == 'win32': return False
252 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
254 def _read_hklm_reg(key_name, value_name):
255 from win32api import RegOpenKeyEx, RegQueryValueEx, RegCloseKey
256 from win32con import HKEY_LOCAL_MACHINE, KEY_READ
257 KEY_WOW64_64KEY = 0x0100
258 KEY_WOW64_32KEY = 0x0200
259 if _is_64bit_windows():
260 try:
261 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_32KEY)
262 (value32, _) = RegQueryValueEx(key32, value_name)
263 RegCloseKey(key32)
264 except:
265 value32 = ''
266 try:
267 key64 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_64KEY)
268 (value64, _) = RegQueryValueEx(key64, value_name)
269 RegCloseKey(key64)
270 except:
271 value64 = ''
272 else:
273 try:
274 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ)
275 (value32, _) = RegQueryValueEx(key32, value_name)
276 RegCloseKey(key32)
277 except:
278 value32 = ''
279 value64 = ''
280 return (value32, value64)
282 if package == 'openjdk-6-jre':
283 (java32_home, java64_home) = _read_hklm_reg(r"SOFTWARE\JavaSoft\Java Runtime Environment\1.6", "JavaHome")
284 import os.path
286 if os.path.isfile(java32_home + r"\bin\java.exe"):
287 impl = factory('package:windows:%s:%s:%s' % (package, '6', 'i486'))
288 impl.machine = 'i486';
289 impl.version = model.parse_version('6')
290 impl.upstream_stability = model.packaged
291 impl.main = java32_home + r"\bin\java.exe"
293 if os.path.isfile(java64_home + r"\bin\java.exe"):
294 impl = factory('package:windows:%s:%s:%s' % (package, '6', 'x86_64'))
295 impl.machine = 'x86_64';
296 impl.version = model.parse_version('6')
297 impl.upstream_stability = model.packaged
298 impl.main = java64_home + r"\bin\java.exe"
300 if package == 'openjdk-6-jdk':
301 (java32_home, java64_home) = _read_hklm_reg(r"SOFTWARE\JavaSoft\Java Development Kit\1.6", "JavaHome")
302 import os.path
304 if os.path.isfile(java32_home + r"\bin\java.exe"):
305 impl = factory('package:windows:%s:%s:%s' % (package, '6', 'i486'))
306 impl.machine = 'i486';
307 impl.version = model.parse_version('6')
308 impl.upstream_stability = model.packaged
309 impl.main = java32_home + r"\bin\java.exe"
311 if os.path.isfile(java64_home + r"\bin\java.exe"):
312 impl = factory('package:windows:%s:%s:%s' % (package, '6', 'x86_64'))
313 impl.machine = 'x86_64';
314 impl.version = model.parse_version('6')
315 impl.upstream_stability = model.packaged
316 impl.main = java64_home + r"\bin\java.exe"
318 class CachedDistribution(Distribution):
319 """For distributions where querying the package database is slow (e.g. requires running
320 an external command), we cache the results.
321 @since: 0.39
322 @deprecated: use Cache instead
325 def __init__(self, db_status_file):
326 """@param db_status_file: update the cache when the timestamp of this file changes"""
327 self._status_details = os.stat(db_status_file)
329 self.versions = {}
330 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
331 namespaces.config_prog)
333 try:
334 self._load_cache()
335 except Exception as ex:
336 info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
337 try:
338 self.generate_cache()
339 self._load_cache()
340 except Exception as ex:
341 warn(_("Failed to regenerate distribution database cache: %s"), ex)
343 def _load_cache(self):
344 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
345 Throws an exception if the cache should be (re)created."""
346 stream = open(os.path.join(self.cache_dir, self.cache_leaf))
348 cache_version = None
349 for line in stream:
350 if line == '\n':
351 break
352 name, value = line.split(': ')
353 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
354 raise Exception(_("Modification time of package database file has changed"))
355 if name == 'size' and int(value) != self._status_details.st_size:
356 raise Exception(_("Size of package database file has changed"))
357 if name == 'version':
358 cache_version = int(value)
359 else:
360 raise Exception(_('Invalid cache format (bad header)'))
362 if cache_version is None:
363 raise Exception(_('Old cache format'))
365 versions = self.versions
366 for line in stream:
367 package, version, zi_arch = line[:-1].split('\t')
368 versionarch = (version, intern(zi_arch))
369 if package not in versions:
370 versions[package] = [versionarch]
371 else:
372 versions[package].append(versionarch)
374 def _write_cache(self, cache):
375 #cache.sort() # Might be useful later; currently we don't care
376 import tempfile
377 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
378 dir = self.cache_dir)
379 try:
380 stream = os.fdopen(fd, 'wb')
381 stream.write('version: 2\n')
382 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
383 stream.write('size: %d\n' % self._status_details.st_size)
384 stream.write('\n')
385 for line in cache:
386 stream.write(line + '\n')
387 stream.close()
389 os.rename(tmpname,
390 os.path.join(self.cache_dir,
391 self.cache_leaf))
392 except:
393 os.unlink(tmpname)
394 raise
396 # Maps machine type names used in packages to their Zero Install versions
397 _canonical_machine = {
398 'all' : '*',
399 'any' : '*',
400 'noarch' : '*',
401 '(none)' : '*',
402 'x86_64': 'x86_64',
403 'amd64': 'x86_64',
404 'i386': 'i386',
405 'i486': 'i486',
406 'i586': 'i586',
407 'i686': 'i686',
408 'ppc64': 'ppc64',
409 'ppc': 'ppc',
412 host_machine = arch.canonicalize_machine(platform.uname()[4])
413 def canonical_machine(package_machine):
414 machine = _canonical_machine.get(package_machine, None)
415 if machine is None:
416 # Safe default if we can't understand the arch
417 return host_machine
418 return machine
420 class DebianDistribution(Distribution):
421 """A dpkg-based distribution."""
423 cache_leaf = 'dpkg-status.cache'
425 def __init__(self, dpkg_status, pkgcache):
426 self.dpkg_cache = Cache('dpkg-status.cache', dpkg_status, 2)
427 self.apt_cache = {}
429 def _query_installed_package(self, package):
430 null = os.open('/dev/null', os.O_WRONLY)
431 child = subprocess.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package],
432 stdout = subprocess.PIPE, stderr = null)
433 os.close(null)
434 stdout, stderr = child.communicate()
435 child.wait()
436 for line in stdout.split('\n'):
437 if not line: continue
438 version, debarch, status = line.split('\t', 2)
439 if not status.endswith(' installed'): continue
440 clean_version = try_cleanup_distro_version(version)
441 if debarch.find("-") != -1:
442 debarch = debarch.split("-")[-1]
443 if clean_version:
444 return '%s\t%s' % (clean_version, canonical_machine(debarch.strip()))
445 else:
446 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
448 return '-'
450 def get_package_info(self, package, factory):
451 # Add any already-installed package...
452 installed_cached_info = self._get_dpkg_info(package)
454 if installed_cached_info != '-':
455 installed_version, machine = installed_cached_info.split('\t')
456 impl = factory('package:deb:%s:%s:%s' % (package, installed_version, machine))
457 impl.version = model.parse_version(installed_version)
458 if machine != '*':
459 impl.machine = machine
460 else:
461 installed_version = None
463 # Add any uninstalled candidates (note: only one of these two methods will add anything)
465 # From PackageKit...
466 self.packagekit.get_candidates(package, factory, 'package:deb')
468 # From apt-cache...
469 cached = self.apt_cache.get(package, None)
470 if cached:
471 candidate_version = cached['version']
472 candidate_arch = cached['arch']
473 if candidate_version and candidate_version != installed_version:
474 impl = factory('package:deb:%s:%s:%s' % (package, candidate_version, candidate_arch), installed = False)
475 impl.version = model.parse_version(candidate_version)
476 if candidate_arch != '*':
477 impl.machine = candidate_arch
478 def install(handler):
479 raise model.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
480 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
481 "use that to install it.") % package)
482 impl.download_sources.append(model.DistributionSource(package, cached['size'], install, needs_confirmation = False))
484 def get_score(self, disto_name):
485 return int(disto_name == 'Debian')
487 def _get_dpkg_info(self, package):
488 installed_cached_info = self.dpkg_cache.get(package)
489 if installed_cached_info == None:
490 installed_cached_info = self._query_installed_package(package)
491 self.dpkg_cache.put(package, installed_cached_info)
493 return installed_cached_info
495 def fetch_candidates(self, master_feed):
496 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
498 if self.packagekit.available:
499 return self.packagekit.fetch_candidates(package_names)
501 # No PackageKit. Use apt-cache directly.
502 for package in package_names:
503 # Check to see whether we could get a newer version using apt-get
504 try:
505 null = os.open('/dev/null', os.O_WRONLY)
506 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null)
507 os.close(null)
509 arch = version = size = None
510 for line in child.stdout:
511 line = line.strip()
512 if line.startswith('Version: '):
513 version = line[9:]
514 version = try_cleanup_distro_version(version)
515 elif line.startswith('Architecture: '):
516 arch = canonical_machine(line[14:].strip())
517 elif line.startswith('Size: '):
518 size = int(line[6:].strip())
519 if version and arch:
520 cached = {'version': version, 'arch': arch, 'size': size}
521 else:
522 cached = None
523 child.wait()
524 except Exception as ex:
525 warn("'apt-cache show %s' failed: %s", package, ex)
526 cached = None
527 # (multi-arch support? can there be multiple candidates?)
528 self.apt_cache[package] = cached
530 class RPMDistribution(CachedDistribution):
531 """An RPM-based distribution."""
533 cache_leaf = 'rpm-status.cache'
535 def generate_cache(self):
536 cache = []
538 for line in os.popen("rpm -qa --qf='%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n'"):
539 package, version, rpmarch = line.split('\t', 2)
540 if package == 'gpg-pubkey':
541 continue
542 zi_arch = canonical_machine(rpmarch.strip())
543 clean_version = try_cleanup_distro_version(version)
544 if clean_version:
545 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
546 else:
547 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
549 self._write_cache(cache)
551 def get_package_info(self, package, factory):
552 # Add installed versions...
553 versions = self.versions.get(package, [])
555 for version, machine in versions:
556 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
557 impl.version = model.parse_version(version)
558 if machine != '*':
559 impl.machine = machine
561 # Add any uninstalled candidates found by PackageKit
562 self.packagekit.get_candidates(package, factory, 'package:rpm')
564 def get_score(self, disto_name):
565 return int(disto_name == 'RPM')
567 class SlackDistribution(Distribution):
568 """A Slack-based distribution."""
570 def __init__(self, packages_dir):
571 self._packages_dir = packages_dir
573 def get_package_info(self, package, factory):
574 # Add installed versions...
575 for entry in os.listdir(self._packages_dir):
576 name, version, arch, build = entry.rsplit('-', 3)
577 if name == package:
578 zi_arch = canonical_machine(arch)
579 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
580 if not clean_version:
581 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
582 continue
584 impl = factory('package:slack:%s:%s:%s' % \
585 (package, clean_version, zi_arch))
586 impl.version = model.parse_version(clean_version)
587 if zi_arch != '*':
588 impl.machine = zi_arch
590 # Add any uninstalled candidates found by PackageKit
591 self.packagekit.get_candidates(package, factory, 'package:slack')
593 def get_score(self, disto_name):
594 return int(disto_name == 'Slack')
596 class ArchDistribution(Distribution):
597 """An Arch Linux distribution."""
599 def __init__(self, packages_dir):
600 self._packages_dir = os.path.join(packages_dir, "local")
602 def get_package_info(self, package, factory):
603 # Add installed versions...
604 for entry in os.listdir(self._packages_dir):
605 name, version, build = entry.rsplit('-', 2)
606 if name == package:
607 gotarch = False
608 for line in open(os.path.join(self._packages_dir, entry, "desc")):
609 if line == "%ARCH%\n":
610 gotarch = True
611 continue
612 if gotarch:
613 arch = line.strip()
614 break
615 zi_arch = canonical_machine(arch)
616 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
617 if not clean_version:
618 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
619 continue
621 impl = factory('package:arch:%s:%s:%s' % \
622 (package, clean_version, zi_arch))
623 impl.version = model.parse_version(clean_version)
624 if zi_arch != '*':
625 impl.machine = zi_arch
627 # Add any uninstalled candidates found by PackageKit
628 self.packagekit.get_candidates(package, factory, 'package:arch')
630 def get_score(self, disto_name):
631 return int(disto_name == 'Arch')
633 class GentooDistribution(Distribution):
635 def __init__(self, pkgdir):
636 self._pkgdir = pkgdir
638 def get_package_info(self, package, factory):
639 # Add installed versions...
640 _version_start_reqexp = '-[0-9]'
642 if package.count('/') != 1: return
644 category, leafname = package.split('/')
645 category_dir = os.path.join(self._pkgdir, category)
646 match_prefix = leafname + '-'
648 if not os.path.isdir(category_dir): return
650 for filename in os.listdir(category_dir):
651 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit():
652 name = open(os.path.join(category_dir, filename, 'PF')).readline().strip()
654 match = re.search(_version_start_reqexp, name)
655 if match is None:
656 warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name})
657 continue
658 else:
659 version = try_cleanup_distro_version(name[match.start() + 1:])
661 if category == 'app-emulation' and name.startswith('emul-'):
662 __, __, machine, __ = name.split('-', 3)
663 else:
664 machine, __ = open(os.path.join(category_dir, filename, 'CHOST')).readline().split('-', 1)
665 machine = arch.canonicalize_machine(machine)
667 impl = factory('package:gentoo:%s:%s:%s' % \
668 (package, version, machine))
669 impl.version = model.parse_version(version)
670 impl.machine = machine
672 # Add any uninstalled candidates found by PackageKit
673 self.packagekit.get_candidates(package, factory, 'package:gentoo')
675 def get_score(self, disto_name):
676 return int(disto_name == 'Gentoo')
678 class PortsDistribution(Distribution):
680 def __init__(self, pkgdir):
681 self._pkgdir = pkgdir
683 def get_package_info(self, package, factory):
684 _name_version_regexp = '^(.+)-([^-]+)$'
686 nameversion = re.compile(_name_version_regexp)
687 for pkgname in os.listdir(self._pkgdir):
688 pkgdir = os.path.join(self._pkgdir, pkgname)
689 if not os.path.isdir(pkgdir): continue
691 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
693 match = nameversion.search(pkgname)
694 if match is None:
695 warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname})
696 continue
697 else:
698 name = match.group(1)
699 if name != package:
700 continue
701 version = try_cleanup_distro_version(match.group(2))
703 machine = host_machine
705 impl = factory('package:ports:%s:%s:%s' % \
706 (package, version, machine))
707 impl.version = model.parse_version(version)
708 impl.machine = machine
710 def get_score(self, disto_name):
711 return int(disto_name == 'Ports')
713 class MacPortsDistribution(CachedDistribution):
715 cache_leaf = 'macports-status.cache'
717 def generate_cache(self):
718 cache = []
720 # for line in os.popen("port echo active"):
721 for line in os.popen("port -v installed"):
722 if not line.startswith(" "):
723 continue
724 if line.strip().count(" ") > 1:
725 package, version, extra = line.split(None, 2)
726 else:
727 package, version = line.split()
728 extra = ""
729 if not extra.startswith("(active)"):
730 continue
731 version = version.lstrip('@')
732 version = re.sub(r"\+.*","",version) # strip variants
733 zi_arch = '*'
734 clean_version = try_cleanup_distro_version(version)
735 if clean_version:
736 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra)
737 if match:
738 platform, major, archs = match.groups()
739 for arch in archs.split():
740 zi_arch = canonical_machine(arch)
741 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
742 else:
743 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
744 else:
745 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
747 self._write_cache(cache)
749 def get_package_info(self, package, factory):
750 # Add installed versions...
751 versions = self.versions.get(package, [])
753 for version, machine in versions:
754 impl = factory('package:macports:%s:%s:%s' % (package, version, machine))
755 impl.version = model.parse_version(version)
756 if machine != '*':
757 impl.machine = machine
759 def get_score(self, disto_name):
760 return int(disto_name == 'MacPorts')
762 class CygwinDistribution(CachedDistribution):
763 """A Cygwin-based distribution."""
765 cache_leaf = 'cygcheck-status.cache'
767 def generate_cache(self):
768 cache = []
770 zi_arch = canonical_machine(arch)
771 for line in os.popen("cygcheck -c -d"):
772 if line == "Cygwin Package Information\r\n":
773 continue
774 if line == "\n":
775 continue
776 package, version = line.split()
777 if package == "Package" and version == "Version":
778 continue
779 clean_version = try_cleanup_distro_version(version)
780 if clean_version:
781 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
782 else:
783 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
785 self._write_cache(cache)
787 def get_package_info(self, package, factory):
788 # Add installed versions...
789 versions = self.versions.get(package, [])
791 for version, machine in versions:
792 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine))
793 impl.version = model.parse_version(version)
794 if machine != '*':
795 impl.machine = machine
797 def get_score(self, disto_name):
798 return int(disto_name == 'Cygwin')
801 _host_distribution = None
802 def get_host_distribution():
803 """Get a Distribution suitable for the host operating system.
804 Calling this twice will return the same object.
805 @rtype: L{Distribution}"""
806 global _host_distribution
807 if not _host_distribution:
808 dpkg_db_status = '/var/lib/dpkg/status'
809 pkgcache = '/var/cache/apt/pkgcache.bin'
810 rpm_db_packages = '/var/lib/rpm/Packages'
811 _slack_db = '/var/log/packages'
812 _arch_db = '/var/lib/pacman'
813 _pkg_db = '/var/db/pkg'
814 _macports_db = '/opt/local/var/macports/registry/registry.db'
815 _cygwin_log = '/var/log/setup.log'
817 if sys.prefix == "/sw":
818 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status)
819 pkgcache = os.path.join(sys.prefix, pkgcache)
820 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages)
822 if os.name == "nt":
823 _host_distribution = WindowsDistribution()
824 elif os.path.isdir(_pkg_db):
825 if sys.platform.startswith("linux"):
826 _host_distribution = GentooDistribution(_pkg_db)
827 elif sys.platform.startswith("freebsd"):
828 _host_distribution = PortsDistribution(_pkg_db)
829 elif os.path.isfile(_macports_db) \
830 and sys.prefix.startswith("/opt/local"):
831 _host_distribution = MacPortsDistribution(_macports_db)
832 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin":
833 _host_distribution = CygwinDistribution(_cygwin_log)
834 elif os.access(dpkg_db_status, os.R_OK) \
835 and os.path.getsize(dpkg_db_status) > 0:
836 _host_distribution = DebianDistribution(dpkg_db_status, pkgcache)
837 elif os.path.isfile(rpm_db_packages):
838 _host_distribution = RPMDistribution(rpm_db_packages)
839 elif os.path.isdir(_slack_db):
840 _host_distribution = SlackDistribution(_slack_db)
841 elif os.path.isdir(_arch_db):
842 _host_distribution = ArchDistribution(_arch_db)
843 else:
844 _host_distribution = Distribution()
846 return _host_distribution