More Python 3 support
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob03b30efd96319ac430fd19a22050e5b9876024b7
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, portable_rename
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 match = re.match(_version_regexp, version)
118 if match:
119 major, version, revision = match.groups()
120 if major is not None:
121 version = major[:-1] + '.' + version
122 if revision is None:
123 return version
124 else:
125 return '%s-%s' % (version, revision[2:])
126 return None
128 class Distribution(object):
129 """Represents a distribution with which we can integrate.
130 Sub-classes should specialise this to integrate with the package managers of
131 particular distributions. This base class ignores the native package manager.
132 @since: 0.28
134 _packagekit = None
136 def get_package_info(self, package, factory):
137 """Get information about the given package.
138 Add zero or more implementations using the factory (typically at most two
139 will be added; the currently installed version and the latest available).
140 @param package: package name (e.g. "gimp")
141 @type package: str
142 @param factory: function for creating new DistributionImplementation objects from IDs
143 @type factory: str -> L{model.DistributionImplementation}
145 return
147 def get_score(self, distribution):
148 """Indicate how closely the host distribution matches this one.
149 The <package-implementation> with the highest score is passed
150 to L{Distribution.get_package_info}. If several elements get
151 the same score, get_package_info is called for all of them.
152 @param distribution: a distribution name
153 @type distribution: str
154 @return: an integer, or None if there is no match at all
155 @rtype: int | None
157 return 0
159 def get_feed(self, master_feed):
160 """Generate a feed containing information about distribution packages.
161 This should immediately return a feed containing an implementation for the
162 package if it's already installed. Information about versions that could be
163 installed using the distribution's package manager can be added asynchronously
164 later (see L{fetch_candidates}).
165 @param master_feed: feed containing the <package-implementation> elements
166 @type master_feed: L{model.ZeroInstallFeed}
167 @rtype: L{model.ZeroInstallFeed}"""
169 feed = model.ZeroInstallFeed(None)
170 feed.url = 'distribution:' + master_feed.url
172 for item, item_attrs in master_feed.get_package_impls(self):
173 package = item_attrs.get('package', None)
174 if package is None:
175 raise model.InvalidInterface(_("Missing 'package' attribute on %s") % item)
177 def factory(id, only_if_missing = False, installed = True):
178 assert id.startswith('package:')
179 if id in feed.implementations:
180 if only_if_missing:
181 return None
182 warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
183 impl = model.DistributionImplementation(feed, id, self, item)
184 feed.implementations[id] = impl
186 impl.installed = installed
187 impl.metadata = item_attrs
189 if 'run' not in impl.commands:
190 item_main = item_attrs.get('main', None)
191 if item_main:
192 if item_main.startswith('/'):
193 impl.main = item_main
194 else:
195 raise model.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
196 item_main)
197 impl.upstream_stability = model.packaged
199 return impl
201 self.get_package_info(package, factory)
203 if master_feed.url == 'http://repo.roscidus.com/python/python' and all(not impl.installed for impl in feed.implementations.values()):
204 # Hack: we can support Python on platforms with unsupported package managers
205 # by adding the implementation of Python running us now to the list.
206 python_version = '.'.join([str(v) for v in sys.version_info if isinstance(v, int)])
207 impl_id = 'package:host:python:' + python_version
208 assert impl_id not in feed.implementations
209 impl = model.DistributionImplementation(feed, impl_id, self)
210 impl.installed = True
211 impl.version = model.parse_version(python_version)
212 impl.main = sys.executable
213 impl.upstream_stability = model.packaged
214 impl.machine = host_machine # (hopefully)
215 feed.implementations[impl_id] = impl
217 return feed
219 def fetch_candidates(self, master_feed):
220 """Collect information about versions we could install using
221 the distribution's package manager. On success, the distribution
222 feed in iface_cache is updated.
223 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
224 if self.packagekit.available:
225 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
226 return self.packagekit.fetch_candidates(package_names)
228 @property
229 def packagekit(self):
230 """For use by subclasses.
231 @rtype: L{packagekit.PackageKit}"""
232 if not self._packagekit:
233 from zeroinstall.injector import packagekit
234 self._packagekit = packagekit.PackageKit()
235 return self._packagekit
237 class WindowsDistribution(Distribution):
238 def get_package_info(self, package, factory):
239 def _is_64bit_windows():
240 p = sys.platform
241 from win32process import IsWow64Process
242 if p == 'win64' or (p == 'win32' and IsWow64Process()): return True
243 elif p == 'win32': return False
244 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
246 def _read_hklm_reg(key_name, value_name):
247 from win32api import RegOpenKeyEx, RegQueryValueEx, RegCloseKey
248 from win32con import HKEY_LOCAL_MACHINE, KEY_READ
249 KEY_WOW64_64KEY = 0x0100
250 KEY_WOW64_32KEY = 0x0200
251 if _is_64bit_windows():
252 try:
253 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_32KEY)
254 (value32, _) = RegQueryValueEx(key32, value_name)
255 RegCloseKey(key32)
256 except:
257 value32 = ''
258 try:
259 key64 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_64KEY)
260 (value64, _) = RegQueryValueEx(key64, value_name)
261 RegCloseKey(key64)
262 except:
263 value64 = ''
264 else:
265 try:
266 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ)
267 (value32, _) = RegQueryValueEx(key32, value_name)
268 RegCloseKey(key32)
269 except:
270 value32 = ''
271 value64 = ''
272 return (value32, value64)
274 def find_java(part, win_version, zero_version):
275 reg_path = r"SOFTWARE\JavaSoft\{part}\{win_version}".format(part = part, win_version = win_version)
276 (java32_home, java64_home) = _read_hklm_reg(reg_path, "JavaHome")
278 for (home, arch) in [(java32_home, 'i486'),
279 (java64_home, 'x86_64')]:
280 if os.path.isfile(home + r"\bin\java.exe"):
281 impl = factory('package:windows:%s:%s:%s' % (package, zero_version, arch))
282 impl.machine = arch
283 impl.version = model.parse_version(zero_version)
284 impl.upstream_stability = model.packaged
285 impl.main = home + r"\bin\java.exe"
287 if package == 'openjdk-6-jre':
288 find_java("Java Runtime Environment", "1.6", '6')
289 elif package == 'openjdk-6-jdk':
290 find_java("Java Development Kit", "1.6", '6')
291 elif package == 'openjdk-7-jre':
292 find_java("Java Runtime Environment", "1.7", '7')
293 elif package == 'openjdk-7-jdk':
294 find_java("Java Development Kit", "1.7", '7')
296 def get_score(self, disto_name):
297 return int(disto_name == 'Windows')
299 class CachedDistribution(Distribution):
300 """For distributions where querying the package database is slow (e.g. requires running
301 an external command), we cache the results.
302 @since: 0.39
303 @deprecated: use Cache instead
306 def __init__(self, db_status_file):
307 """@param db_status_file: update the cache when the timestamp of this file changes"""
308 self._status_details = os.stat(db_status_file)
310 self.versions = {}
311 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
312 namespaces.config_prog)
314 try:
315 self._load_cache()
316 except Exception as ex:
317 info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
318 try:
319 self.generate_cache()
320 self._load_cache()
321 except Exception as ex:
322 warn(_("Failed to regenerate distribution database cache: %s"), ex)
324 def _load_cache(self):
325 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
326 Throws an exception if the cache should be (re)created."""
327 with open(os.path.join(self.cache_dir, self.cache_leaf)) as stream:
328 cache_version = None
329 for line in stream:
330 if line == '\n':
331 break
332 name, value = line.split(': ')
333 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
334 raise Exception(_("Modification time of package database file has changed"))
335 if name == 'size' and int(value) != self._status_details.st_size:
336 raise Exception(_("Size of package database file has changed"))
337 if name == 'version':
338 cache_version = int(value)
339 else:
340 raise Exception(_('Invalid cache format (bad header)'))
342 if cache_version is None:
343 raise Exception(_('Old cache format'))
345 versions = self.versions
346 for line in stream:
347 package, version, zi_arch = line[:-1].split('\t')
348 versionarch = (version, intern(zi_arch))
349 if package not in versions:
350 versions[package] = [versionarch]
351 else:
352 versions[package].append(versionarch)
354 def _write_cache(self, cache):
355 #cache.sort() # Might be useful later; currently we don't care
356 import tempfile
357 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
358 dir = self.cache_dir)
359 try:
360 stream = os.fdopen(fd, 'wb')
361 stream.write('version: 2\n')
362 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
363 stream.write('size: %d\n' % self._status_details.st_size)
364 stream.write('\n')
365 for line in cache:
366 stream.write(line + '\n')
367 stream.close()
369 portable_rename(tmpname,
370 os.path.join(self.cache_dir,
371 self.cache_leaf))
372 except:
373 os.unlink(tmpname)
374 raise
376 # Maps machine type names used in packages to their Zero Install versions
377 _canonical_machine = {
378 'all' : '*',
379 'any' : '*',
380 'noarch' : '*',
381 '(none)' : '*',
382 'x86_64': 'x86_64',
383 'amd64': 'x86_64',
384 'i386': 'i386',
385 'i486': 'i486',
386 'i586': 'i586',
387 'i686': 'i686',
388 'ppc64': 'ppc64',
389 'ppc': 'ppc',
392 host_machine = arch.canonicalize_machine(platform.uname()[4])
393 def canonical_machine(package_machine):
394 machine = _canonical_machine.get(package_machine, None)
395 if machine is None:
396 # Safe default if we can't understand the arch
397 return host_machine
398 return machine
400 class DebianDistribution(Distribution):
401 """A dpkg-based distribution."""
403 cache_leaf = 'dpkg-status.cache'
405 def __init__(self, dpkg_status):
406 self.dpkg_cache = Cache('dpkg-status.cache', dpkg_status, 2)
407 self.apt_cache = {}
409 def _query_installed_package(self, package):
410 null = os.open(os.devnull, os.O_WRONLY)
411 child = subprocess.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package],
412 stdout = subprocess.PIPE, stderr = null)
413 os.close(null)
414 stdout, stderr = child.communicate()
415 child.wait()
416 for line in stdout.split('\n'):
417 if not line: continue
418 version, debarch, status = line.split('\t', 2)
419 if not status.endswith(' installed'): continue
420 clean_version = try_cleanup_distro_version(version)
421 if debarch.find("-") != -1:
422 debarch = debarch.split("-")[-1]
423 if clean_version:
424 return '%s\t%s' % (clean_version, canonical_machine(debarch.strip()))
425 else:
426 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
428 return '-'
430 def get_package_info(self, package, factory):
431 # Add any already-installed package...
432 installed_cached_info = self._get_dpkg_info(package)
434 if installed_cached_info != '-':
435 installed_version, machine = installed_cached_info.split('\t')
436 impl = factory('package:deb:%s:%s:%s' % (package, installed_version, machine))
437 impl.version = model.parse_version(installed_version)
438 if machine != '*':
439 impl.machine = machine
440 else:
441 installed_version = None
443 # Add any uninstalled candidates (note: only one of these two methods will add anything)
445 # From PackageKit...
446 self.packagekit.get_candidates(package, factory, 'package:deb')
448 # From apt-cache...
449 cached = self.apt_cache.get(package, None)
450 if cached:
451 candidate_version = cached['version']
452 candidate_arch = cached['arch']
453 if candidate_version and candidate_version != installed_version:
454 impl = factory('package:deb:%s:%s:%s' % (package, candidate_version, candidate_arch), installed = False)
455 impl.version = model.parse_version(candidate_version)
456 if candidate_arch != '*':
457 impl.machine = candidate_arch
458 def install(handler):
459 raise model.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
460 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
461 "use that to install it.") % package)
462 impl.download_sources.append(model.DistributionSource(package, cached['size'], install, needs_confirmation = False))
464 def get_score(self, disto_name):
465 return int(disto_name == 'Debian')
467 def _get_dpkg_info(self, package):
468 installed_cached_info = self.dpkg_cache.get(package)
469 if installed_cached_info == None:
470 installed_cached_info = self._query_installed_package(package)
471 self.dpkg_cache.put(package, installed_cached_info)
473 return installed_cached_info
475 def fetch_candidates(self, master_feed):
476 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
478 if self.packagekit.available:
479 return self.packagekit.fetch_candidates(package_names)
481 # No PackageKit. Use apt-cache directly.
482 for package in package_names:
483 # Check to see whether we could get a newer version using apt-get
484 try:
485 null = os.open(os.devnull, os.O_WRONLY)
486 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null)
487 os.close(null)
489 arch = version = size = None
490 for line in child.stdout:
491 line = line.strip()
492 if line.startswith('Version: '):
493 version = line[9:]
494 version = try_cleanup_distro_version(version)
495 elif line.startswith('Architecture: '):
496 arch = canonical_machine(line[14:].strip())
497 elif line.startswith('Size: '):
498 size = int(line[6:].strip())
499 if version and arch:
500 cached = {'version': version, 'arch': arch, 'size': size}
501 else:
502 cached = None
503 child.wait()
504 except Exception as ex:
505 warn("'apt-cache show %s' failed: %s", package, ex)
506 cached = None
507 # (multi-arch support? can there be multiple candidates?)
508 self.apt_cache[package] = cached
510 class RPMDistribution(CachedDistribution):
511 """An RPM-based distribution."""
513 cache_leaf = 'rpm-status.cache'
515 def generate_cache(self):
516 cache = []
518 for line in os.popen("rpm -qa --qf='%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n'"):
519 package, version, rpmarch = line.split('\t', 2)
520 if package == 'gpg-pubkey':
521 continue
522 zi_arch = canonical_machine(rpmarch.strip())
523 clean_version = try_cleanup_distro_version(version)
524 if clean_version:
525 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
526 else:
527 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
529 self._write_cache(cache)
531 def get_package_info(self, package, factory):
532 # Add installed versions...
533 versions = self.versions.get(package, [])
535 for version, machine in versions:
536 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
537 impl.version = model.parse_version(version)
538 if machine != '*':
539 impl.machine = machine
541 # Add any uninstalled candidates found by PackageKit
542 self.packagekit.get_candidates(package, factory, 'package:rpm')
544 def get_score(self, disto_name):
545 return int(disto_name == 'RPM')
547 class SlackDistribution(Distribution):
548 """A Slack-based distribution."""
550 def __init__(self, packages_dir):
551 self._packages_dir = packages_dir
553 def get_package_info(self, package, factory):
554 # Add installed versions...
555 for entry in os.listdir(self._packages_dir):
556 name, version, arch, build = entry.rsplit('-', 3)
557 if name == package:
558 zi_arch = canonical_machine(arch)
559 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
560 if not clean_version:
561 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
562 continue
564 impl = factory('package:slack:%s:%s:%s' % \
565 (package, clean_version, zi_arch))
566 impl.version = model.parse_version(clean_version)
567 if zi_arch != '*':
568 impl.machine = zi_arch
570 # Add any uninstalled candidates found by PackageKit
571 self.packagekit.get_candidates(package, factory, 'package:slack')
573 def get_score(self, disto_name):
574 return int(disto_name == 'Slack')
576 class ArchDistribution(Distribution):
577 """An Arch Linux distribution."""
579 def __init__(self, packages_dir):
580 self._packages_dir = os.path.join(packages_dir, "local")
582 def get_package_info(self, package, factory):
583 # Add installed versions...
584 for entry in os.listdir(self._packages_dir):
585 name, version, build = entry.rsplit('-', 2)
586 if name == package:
587 gotarch = False
588 for line in open(os.path.join(self._packages_dir, entry, "desc")):
589 if line == "%ARCH%\n":
590 gotarch = True
591 continue
592 if gotarch:
593 arch = line.strip()
594 break
595 zi_arch = canonical_machine(arch)
596 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
597 if not clean_version:
598 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
599 continue
601 impl = factory('package:arch:%s:%s:%s' % \
602 (package, clean_version, zi_arch))
603 impl.version = model.parse_version(clean_version)
604 if zi_arch != '*':
605 impl.machine = zi_arch
607 # Add any uninstalled candidates found by PackageKit
608 self.packagekit.get_candidates(package, factory, 'package:arch')
610 def get_score(self, disto_name):
611 return int(disto_name == 'Arch')
613 class GentooDistribution(Distribution):
615 def __init__(self, pkgdir):
616 self._pkgdir = pkgdir
618 def get_package_info(self, package, factory):
619 # Add installed versions...
620 _version_start_reqexp = '-[0-9]'
622 if package.count('/') != 1: return
624 category, leafname = package.split('/')
625 category_dir = os.path.join(self._pkgdir, category)
626 match_prefix = leafname + '-'
628 if not os.path.isdir(category_dir): return
630 for filename in os.listdir(category_dir):
631 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit():
632 name = open(os.path.join(category_dir, filename, 'PF')).readline().strip()
634 match = re.search(_version_start_reqexp, name)
635 if match is None:
636 warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name})
637 continue
638 else:
639 version = try_cleanup_distro_version(name[match.start() + 1:])
641 if category == 'app-emulation' and name.startswith('emul-'):
642 __, __, machine, __ = name.split('-', 3)
643 else:
644 machine, __ = open(os.path.join(category_dir, filename, 'CHOST')).readline().split('-', 1)
645 machine = arch.canonicalize_machine(machine)
647 impl = factory('package:gentoo:%s:%s:%s' % \
648 (package, version, machine))
649 impl.version = model.parse_version(version)
650 impl.machine = machine
652 # Add any uninstalled candidates found by PackageKit
653 self.packagekit.get_candidates(package, factory, 'package:gentoo')
655 def get_score(self, disto_name):
656 return int(disto_name == 'Gentoo')
658 class PortsDistribution(Distribution):
660 def __init__(self, pkgdir):
661 self._pkgdir = pkgdir
663 def get_package_info(self, package, factory):
664 _name_version_regexp = '^(.+)-([^-]+)$'
666 nameversion = re.compile(_name_version_regexp)
667 for pkgname in os.listdir(self._pkgdir):
668 pkgdir = os.path.join(self._pkgdir, pkgname)
669 if not os.path.isdir(pkgdir): continue
671 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
673 match = nameversion.search(pkgname)
674 if match is None:
675 warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname})
676 continue
677 else:
678 name = match.group(1)
679 if name != package:
680 continue
681 version = try_cleanup_distro_version(match.group(2))
683 machine = host_machine
685 impl = factory('package:ports:%s:%s:%s' % \
686 (package, version, machine))
687 impl.version = model.parse_version(version)
688 impl.machine = machine
690 def get_score(self, disto_name):
691 return int(disto_name == 'Ports')
693 class MacPortsDistribution(CachedDistribution):
695 cache_leaf = 'macports-status.cache'
697 def generate_cache(self):
698 cache = []
700 # for line in os.popen("port echo active"):
701 for line in os.popen("port -v installed"):
702 if not line.startswith(" "):
703 continue
704 if line.strip().count(" ") > 1:
705 package, version, extra = line.split(None, 2)
706 else:
707 package, version = line.split()
708 extra = ""
709 if not extra.startswith("(active)"):
710 continue
711 version = version.lstrip('@')
712 version = re.sub(r"\+.*", "", version) # strip variants
713 zi_arch = '*'
714 clean_version = try_cleanup_distro_version(version)
715 if clean_version:
716 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra)
717 if match:
718 platform, major, archs = match.groups()
719 for arch in archs.split():
720 zi_arch = canonical_machine(arch)
721 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
722 else:
723 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
724 else:
725 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
727 self._write_cache(cache)
729 def get_package_info(self, package, factory):
730 # Add installed versions...
731 versions = self.versions.get(package, [])
733 for version, machine in versions:
734 impl = factory('package:macports:%s:%s:%s' % (package, version, machine))
735 impl.version = model.parse_version(version)
736 if machine != '*':
737 impl.machine = machine
739 def get_score(self, disto_name):
740 return int(disto_name == 'MacPorts')
742 class CygwinDistribution(CachedDistribution):
743 """A Cygwin-based distribution."""
745 cache_leaf = 'cygcheck-status.cache'
747 def generate_cache(self):
748 cache = []
750 zi_arch = canonical_machine(arch)
751 for line in os.popen("cygcheck -c -d"):
752 if line == "Cygwin Package Information\r\n":
753 continue
754 if line == "\n":
755 continue
756 package, version = line.split()
757 if package == "Package" and version == "Version":
758 continue
759 clean_version = try_cleanup_distro_version(version)
760 if clean_version:
761 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
762 else:
763 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
765 self._write_cache(cache)
767 def get_package_info(self, package, factory):
768 # Add installed versions...
769 versions = self.versions.get(package, [])
771 for version, machine in versions:
772 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine))
773 impl.version = model.parse_version(version)
774 if machine != '*':
775 impl.machine = machine
777 def get_score(self, disto_name):
778 return int(disto_name == 'Cygwin')
781 _host_distribution = None
782 def get_host_distribution():
783 """Get a Distribution suitable for the host operating system.
784 Calling this twice will return the same object.
785 @rtype: L{Distribution}"""
786 global _host_distribution
787 if not _host_distribution:
788 dpkg_db_status = '/var/lib/dpkg/status'
789 rpm_db_packages = '/var/lib/rpm/Packages'
790 _slack_db = '/var/log/packages'
791 _arch_db = '/var/lib/pacman'
792 _pkg_db = '/var/db/pkg'
793 _macports_db = '/opt/local/var/macports/registry/registry.db'
794 _cygwin_log = '/var/log/setup.log'
796 if sys.prefix == "/sw":
797 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status)
798 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages)
800 if os.name == "nt":
801 _host_distribution = WindowsDistribution()
802 elif os.path.isdir(_pkg_db):
803 if sys.platform.startswith("linux"):
804 _host_distribution = GentooDistribution(_pkg_db)
805 elif sys.platform.startswith("freebsd"):
806 _host_distribution = PortsDistribution(_pkg_db)
807 elif os.path.isfile(_macports_db) \
808 and sys.prefix.startswith("/opt/local"):
809 _host_distribution = MacPortsDistribution(_macports_db)
810 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin":
811 _host_distribution = CygwinDistribution(_cygwin_log)
812 elif os.access(dpkg_db_status, os.R_OK) \
813 and os.path.getsize(dpkg_db_status) > 0:
814 _host_distribution = DebianDistribution(dpkg_db_status)
815 elif os.path.isfile(rpm_db_packages):
816 _host_distribution = RPMDistribution(rpm_db_packages)
817 elif os.path.isdir(_slack_db):
818 _host_distribution = SlackDistribution(_slack_db)
819 elif os.path.isdir(_arch_db):
820 _host_distribution = ArchDistribution(_arch_db)
821 else:
822 _host_distribution = Distribution()
824 return _host_distribution