Parse Debian snapshot version numbers
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob027a399cde49b9d64dc1622e8b83b3201d490de2
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, 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 def factory(id, only_if_missing = False, installed = True):
182 assert id.startswith('package:')
183 if id in feed.implementations:
184 if only_if_missing:
185 return None
186 warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
187 impl = model.DistributionImplementation(feed, id, self, item)
188 feed.implementations[id] = impl
190 impl.installed = installed
191 impl.metadata = item_attrs
193 if 'run' not in impl.commands:
194 item_main = item_attrs.get('main', None)
195 if item_main:
196 if item_main.startswith('/'):
197 impl.main = item_main
198 else:
199 raise model.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
200 item_main)
201 impl.upstream_stability = model.packaged
203 return impl
205 self.get_package_info(package, factory)
207 if master_feed.url == 'http://repo.roscidus.com/python/python' and all(not impl.installed for impl in feed.implementations.values()):
208 # Hack: we can support Python on platforms with unsupported package managers
209 # by adding the implementation of Python running us now to the list.
210 python_version = '.'.join([str(v) for v in sys.version_info if isinstance(v, int)])
211 impl_id = 'package:host:python:' + python_version
212 assert impl_id not in feed.implementations
213 impl = model.DistributionImplementation(feed, impl_id, self)
214 impl.installed = True
215 impl.version = model.parse_version(python_version)
216 impl.main = sys.executable
217 impl.upstream_stability = model.packaged
218 impl.machine = host_machine # (hopefully)
219 feed.implementations[impl_id] = impl
221 return feed
223 def fetch_candidates(self, master_feed):
224 """Collect information about versions we could install using
225 the distribution's package manager. On success, the distribution
226 feed in iface_cache is updated.
227 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
228 if self.packagekit.available:
229 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
230 return self.packagekit.fetch_candidates(package_names)
232 @property
233 def packagekit(self):
234 """For use by subclasses.
235 @rtype: L{packagekit.PackageKit}"""
236 if not self._packagekit:
237 from zeroinstall.injector import packagekit
238 self._packagekit = packagekit.PackageKit()
239 return self._packagekit
241 class WindowsDistribution(Distribution):
242 def get_package_info(self, package, factory):
243 def _is_64bit_windows():
244 p = sys.platform
245 from win32process import IsWow64Process
246 if p == 'win64' or (p == 'win32' and IsWow64Process()): return True
247 elif p == 'win32': return False
248 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
250 def _read_hklm_reg(key_name, value_name):
251 from win32api import RegOpenKeyEx, RegQueryValueEx, RegCloseKey
252 from win32con import HKEY_LOCAL_MACHINE, KEY_READ
253 KEY_WOW64_64KEY = 0x0100
254 KEY_WOW64_32KEY = 0x0200
255 if _is_64bit_windows():
256 try:
257 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_32KEY)
258 (value32, _) = RegQueryValueEx(key32, value_name)
259 RegCloseKey(key32)
260 except:
261 value32 = ''
262 try:
263 key64 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_64KEY)
264 (value64, _) = RegQueryValueEx(key64, value_name)
265 RegCloseKey(key64)
266 except:
267 value64 = ''
268 else:
269 try:
270 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ)
271 (value32, _) = RegQueryValueEx(key32, value_name)
272 RegCloseKey(key32)
273 except:
274 value32 = ''
275 value64 = ''
276 return (value32, value64)
278 def find_java(part, win_version, zero_version):
279 reg_path = r"SOFTWARE\JavaSoft\{part}\{win_version}".format(part = part, win_version = win_version)
280 (java32_home, java64_home) = _read_hklm_reg(reg_path, "JavaHome")
282 for (home, arch) in [(java32_home, 'i486'),
283 (java64_home, 'x86_64')]:
284 if os.path.isfile(home + r"\bin\java.exe"):
285 impl = factory('package:windows:%s:%s:%s' % (package, zero_version, arch))
286 impl.machine = arch
287 impl.version = model.parse_version(zero_version)
288 impl.upstream_stability = model.packaged
289 impl.main = home + r"\bin\java.exe"
291 if package == 'openjdk-6-jre':
292 find_java("Java Runtime Environment", "1.6", '6')
293 elif package == 'openjdk-6-jdk':
294 find_java("Java Development Kit", "1.6", '6')
295 elif package == 'openjdk-7-jre':
296 find_java("Java Runtime Environment", "1.7", '7')
297 elif package == 'openjdk-7-jdk':
298 find_java("Java Development Kit", "1.7", '7')
300 def get_score(self, disto_name):
301 return int(disto_name == 'Windows')
303 class DarwinDistribution(Distribution):
304 def get_package_info(self, package, factory):
305 def java_home(version, arch):
306 null = os.open(os.devnull, os.O_WRONLY)
307 child = subprocess.Popen(["/usr/libexec/java_home", "--failfast", "--version", version, "--arch", arch],
308 stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
309 home = child.stdout.read().strip()
310 child.stdout.close()
311 child.wait()
312 return home
314 def find_java(part, jvm_version, zero_version):
315 for arch in ['i386', 'x86_64']:
316 home = java_home(jvm_version, arch)
317 if os.path.isfile(home + "/bin/java"):
318 impl = factory('package:darwin:%s:%s:%s' % (package, zero_version, arch))
319 impl.machine = arch
320 impl.version = model.parse_version(zero_version)
321 impl.upstream_stability = model.packaged
322 impl.main = home + "/bin/java"
324 if package == 'openjdk-6-jre':
325 find_java("Java Runtime Environment", "1.6", '6')
326 elif package == 'openjdk-6-jdk':
327 find_java("Java Development Kit", "1.6", '6')
328 elif package == 'openjdk-7-jre':
329 find_java("Java Runtime Environment", "1.7", '7')
330 elif package == 'openjdk-7-jdk':
331 find_java("Java Development Kit", "1.7", '7')
333 def get_score(self, disto_name):
334 return int(disto_name == 'Darwin')
336 class CachedDistribution(Distribution):
337 """For distributions where querying the package database is slow (e.g. requires running
338 an external command), we cache the results.
339 @since: 0.39
340 @deprecated: use Cache instead
343 def __init__(self, db_status_file):
344 """@param db_status_file: update the cache when the timestamp of this file changes"""
345 self._status_details = os.stat(db_status_file)
347 self.versions = {}
348 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
349 namespaces.config_prog)
351 try:
352 self._load_cache()
353 except Exception as ex:
354 info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
355 try:
356 self.generate_cache()
357 self._load_cache()
358 except Exception as ex:
359 warn(_("Failed to regenerate distribution database cache: %s"), ex)
361 def _load_cache(self):
362 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
363 Throws an exception if the cache should be (re)created."""
364 with open(os.path.join(self.cache_dir, self.cache_leaf), 'rt') as stream:
365 cache_version = None
366 for line in stream:
367 if line == '\n':
368 break
369 name, value = line.split(': ')
370 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
371 raise Exception(_("Modification time of package database file has changed"))
372 if name == 'size' and int(value) != self._status_details.st_size:
373 raise Exception(_("Size of package database file has changed"))
374 if name == 'version':
375 cache_version = int(value)
376 else:
377 raise Exception(_('Invalid cache format (bad header)'))
379 if cache_version is None:
380 raise Exception(_('Old cache format'))
382 versions = self.versions
383 for line in stream:
384 package, version, zi_arch = line[:-1].split('\t')
385 versionarch = (version, intern(zi_arch))
386 if package not in versions:
387 versions[package] = [versionarch]
388 else:
389 versions[package].append(versionarch)
391 def _write_cache(self, cache):
392 #cache.sort() # Might be useful later; currently we don't care
393 import tempfile
394 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
395 dir = self.cache_dir)
396 try:
397 stream = os.fdopen(fd, 'wt')
398 stream.write('version: 2\n')
399 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
400 stream.write('size: %d\n' % self._status_details.st_size)
401 stream.write('\n')
402 for line in cache:
403 stream.write(line + '\n')
404 stream.close()
406 portable_rename(tmpname,
407 os.path.join(self.cache_dir,
408 self.cache_leaf))
409 except:
410 os.unlink(tmpname)
411 raise
413 # Maps machine type names used in packages to their Zero Install versions
414 _canonical_machine = {
415 'all' : '*',
416 'any' : '*',
417 'noarch' : '*',
418 '(none)' : '*',
419 'x86_64': 'x86_64',
420 'amd64': 'x86_64',
421 'i386': 'i386',
422 'i486': 'i486',
423 'i586': 'i586',
424 'i686': 'i686',
425 'ppc64': 'ppc64',
426 'ppc': 'ppc',
429 host_machine = arch.canonicalize_machine(platform.uname()[4])
430 def canonical_machine(package_machine):
431 machine = _canonical_machine.get(package_machine, None)
432 if machine is None:
433 # Safe default if we can't understand the arch
434 return host_machine
435 return machine
437 class DebianDistribution(Distribution):
438 """A dpkg-based distribution."""
440 cache_leaf = 'dpkg-status.cache'
442 def __init__(self, dpkg_status):
443 self.dpkg_cache = Cache('dpkg-status.cache', dpkg_status, 2)
444 self.apt_cache = {}
446 def _query_installed_package(self, package):
447 null = os.open(os.devnull, os.O_WRONLY)
448 child = subprocess.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package],
449 stdout = subprocess.PIPE, stderr = null,
450 universal_newlines = True) # Needed for Python 3
451 os.close(null)
452 stdout, stderr = child.communicate()
453 child.wait()
454 for line in stdout.split('\n'):
455 if not line: continue
456 version, debarch, status = line.split('\t', 2)
457 if not status.endswith(' installed'): continue
458 clean_version = try_cleanup_distro_version(version)
459 if debarch.find("-") != -1:
460 debarch = debarch.split("-")[-1]
461 if clean_version:
462 return '%s\t%s' % (clean_version, canonical_machine(debarch.strip()))
463 else:
464 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
466 return '-'
468 def get_package_info(self, package, factory):
469 # Add any already-installed package...
470 installed_cached_info = self._get_dpkg_info(package)
472 if installed_cached_info != '-':
473 installed_version, machine = installed_cached_info.split('\t')
474 impl = factory('package:deb:%s:%s:%s' % (package, installed_version, machine))
475 impl.version = model.parse_version(installed_version)
476 if machine != '*':
477 impl.machine = machine
478 else:
479 installed_version = None
481 # Add any uninstalled candidates (note: only one of these two methods will add anything)
483 # From PackageKit...
484 self.packagekit.get_candidates(package, factory, 'package:deb')
486 # From apt-cache...
487 cached = self.apt_cache.get(package, None)
488 if cached:
489 candidate_version = cached['version']
490 candidate_arch = cached['arch']
491 if candidate_version and candidate_version != installed_version:
492 impl = factory('package:deb:%s:%s:%s' % (package, candidate_version, candidate_arch), installed = False)
493 impl.version = model.parse_version(candidate_version)
494 if candidate_arch != '*':
495 impl.machine = candidate_arch
496 def install(handler):
497 raise model.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
498 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
499 "use that to install it.") % package)
500 impl.download_sources.append(model.DistributionSource(package, cached['size'], install, needs_confirmation = False))
502 def get_score(self, disto_name):
503 return int(disto_name == 'Debian')
505 def _get_dpkg_info(self, package):
506 installed_cached_info = self.dpkg_cache.get(package)
507 if installed_cached_info == None:
508 installed_cached_info = self._query_installed_package(package)
509 self.dpkg_cache.put(package, installed_cached_info)
511 return installed_cached_info
513 def fetch_candidates(self, master_feed):
514 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
516 if self.packagekit.available:
517 return self.packagekit.fetch_candidates(package_names)
519 # No PackageKit. Use apt-cache directly.
520 for package in package_names:
521 # Check to see whether we could get a newer version using apt-get
522 try:
523 null = os.open(os.devnull, os.O_WRONLY)
524 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
525 os.close(null)
527 arch = version = size = None
528 for line in child.stdout:
529 line = line.strip()
530 if line.startswith('Version: '):
531 version = line[9:]
532 version = try_cleanup_distro_version(version)
533 elif line.startswith('Architecture: '):
534 arch = canonical_machine(line[14:].strip())
535 elif line.startswith('Size: '):
536 size = int(line[6:].strip())
537 if version and arch:
538 cached = {'version': version, 'arch': arch, 'size': size}
539 else:
540 cached = None
541 child.stdout.close()
542 child.wait()
543 except Exception as ex:
544 warn("'apt-cache show %s' failed: %s", package, ex)
545 cached = None
546 # (multi-arch support? can there be multiple candidates?)
547 self.apt_cache[package] = cached
549 class RPMDistribution(CachedDistribution):
550 """An RPM-based distribution."""
552 cache_leaf = 'rpm-status.cache'
554 def generate_cache(self):
555 cache = []
557 child = subprocess.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
558 stdout = subprocess.PIPE, universal_newlines = True)
559 for line in child.stdout:
560 package, version, rpmarch = line.split('\t', 2)
561 if package == 'gpg-pubkey':
562 continue
563 zi_arch = canonical_machine(rpmarch.strip())
564 clean_version = try_cleanup_distro_version(version)
565 if clean_version:
566 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
567 else:
568 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
570 self._write_cache(cache)
571 child.stdout.close()
572 child.wait()
574 def get_package_info(self, package, factory):
575 # Add installed versions...
576 versions = self.versions.get(package, [])
578 for version, machine in versions:
579 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
580 impl.version = model.parse_version(version)
581 if machine != '*':
582 impl.machine = machine
584 # Add any uninstalled candidates found by PackageKit
585 self.packagekit.get_candidates(package, factory, 'package:rpm')
587 def get_score(self, disto_name):
588 return int(disto_name == 'RPM')
590 class SlackDistribution(Distribution):
591 """A Slack-based distribution."""
593 def __init__(self, packages_dir):
594 self._packages_dir = packages_dir
596 def get_package_info(self, package, factory):
597 # Add installed versions...
598 for entry in os.listdir(self._packages_dir):
599 name, version, arch, build = entry.rsplit('-', 3)
600 if name == package:
601 zi_arch = canonical_machine(arch)
602 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
603 if not clean_version:
604 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
605 continue
607 impl = factory('package:slack:%s:%s:%s' % \
608 (package, clean_version, zi_arch))
609 impl.version = model.parse_version(clean_version)
610 if zi_arch != '*':
611 impl.machine = zi_arch
613 # Add any uninstalled candidates found by PackageKit
614 self.packagekit.get_candidates(package, factory, 'package:slack')
616 def get_score(self, disto_name):
617 return int(disto_name == 'Slack')
619 class ArchDistribution(Distribution):
620 """An Arch Linux distribution."""
622 def __init__(self, packages_dir):
623 self._packages_dir = os.path.join(packages_dir, "local")
625 def get_package_info(self, package, factory):
626 # Add installed versions...
627 for entry in os.listdir(self._packages_dir):
628 name, version, build = entry.rsplit('-', 2)
629 if name == package:
630 gotarch = False
631 with open(os.path.join(self._packages_dir, entry, "desc"), 'rt') as stream:
632 for line in stream:
633 if line == "%ARCH%\n":
634 gotarch = True
635 continue
636 if gotarch:
637 arch = line.strip()
638 break
639 zi_arch = canonical_machine(arch)
640 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
641 if not clean_version:
642 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
643 continue
645 impl = factory('package:arch:%s:%s:%s' % \
646 (package, clean_version, zi_arch))
647 impl.version = model.parse_version(clean_version)
648 if zi_arch != '*':
649 impl.machine = zi_arch
651 # Add any uninstalled candidates found by PackageKit
652 self.packagekit.get_candidates(package, factory, 'package:arch')
654 def get_score(self, disto_name):
655 return int(disto_name == 'Arch')
657 class GentooDistribution(Distribution):
659 def __init__(self, pkgdir):
660 self._pkgdir = pkgdir
662 def get_package_info(self, package, factory):
663 # Add installed versions...
664 _version_start_reqexp = '-[0-9]'
666 if package.count('/') != 1: return
668 category, leafname = package.split('/')
669 category_dir = os.path.join(self._pkgdir, category)
670 match_prefix = leafname + '-'
672 if not os.path.isdir(category_dir): return
674 for filename in os.listdir(category_dir):
675 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit():
676 with open(os.path.join(category_dir, filename, 'PF'), 'rt') as stream:
677 name = stream.readline().strip()
679 match = re.search(_version_start_reqexp, name)
680 if match is None:
681 warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name})
682 continue
683 else:
684 version = try_cleanup_distro_version(name[match.start() + 1:])
686 if category == 'app-emulation' and name.startswith('emul-'):
687 __, __, machine, __ = name.split('-', 3)
688 else:
689 with open(os.path.join(category_dir, filename, 'CHOST'), 'rt') as stream:
690 machine, __ = stream.readline().split('-', 1)
691 machine = arch.canonicalize_machine(machine)
693 impl = factory('package:gentoo:%s:%s:%s' % \
694 (package, version, machine))
695 impl.version = model.parse_version(version)
696 impl.machine = machine
698 # Add any uninstalled candidates found by PackageKit
699 self.packagekit.get_candidates(package, factory, 'package:gentoo')
701 def get_score(self, disto_name):
702 return int(disto_name == 'Gentoo')
704 class PortsDistribution(Distribution):
706 def __init__(self, pkgdir):
707 self._pkgdir = pkgdir
709 def get_package_info(self, package, factory):
710 _name_version_regexp = '^(.+)-([^-]+)$'
712 nameversion = re.compile(_name_version_regexp)
713 for pkgname in os.listdir(self._pkgdir):
714 pkgdir = os.path.join(self._pkgdir, pkgname)
715 if not os.path.isdir(pkgdir): continue
717 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
719 match = nameversion.search(pkgname)
720 if match is None:
721 warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname})
722 continue
723 else:
724 name = match.group(1)
725 if name != package:
726 continue
727 version = try_cleanup_distro_version(match.group(2))
729 machine = host_machine
731 impl = factory('package:ports:%s:%s:%s' % \
732 (package, version, machine))
733 impl.version = model.parse_version(version)
734 impl.machine = machine
736 def get_score(self, disto_name):
737 return int(disto_name == 'Ports')
739 class MacPortsDistribution(CachedDistribution):
741 cache_leaf = 'macports-status.cache'
743 def generate_cache(self):
744 cache = []
746 child = subprocess.Popen(["port", "-v", "installed"],
747 stdout = subprocess.PIPE, universal_newlines = True)
748 for line in child.stdout:
749 if not line.startswith(" "):
750 continue
751 if line.strip().count(" ") > 1:
752 package, version, extra = line.split(None, 2)
753 else:
754 package, version = line.split()
755 extra = ""
756 if not extra.startswith("(active)"):
757 continue
758 version = version.lstrip('@')
759 version = re.sub(r"\+.*", "", version) # strip variants
760 zi_arch = '*'
761 clean_version = try_cleanup_distro_version(version)
762 if clean_version:
763 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra)
764 if match:
765 platform, major, archs = match.groups()
766 for arch in archs.split():
767 zi_arch = canonical_machine(arch)
768 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
769 else:
770 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
771 else:
772 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
773 self._write_cache(cache)
774 child.stdout.close()
775 child.wait()
777 def get_package_info(self, package, factory):
778 # Add installed versions...
779 versions = self.versions.get(package, [])
781 for version, machine in versions:
782 impl = factory('package:macports:%s:%s:%s' % (package, version, machine))
783 impl.version = model.parse_version(version)
784 if machine != '*':
785 impl.machine = machine
787 def get_score(self, disto_name):
788 return int(disto_name == 'MacPorts')
790 class CygwinDistribution(CachedDistribution):
791 """A Cygwin-based distribution."""
793 cache_leaf = 'cygcheck-status.cache'
795 def generate_cache(self):
796 cache = []
798 zi_arch = canonical_machine(arch)
799 for line in os.popen("cygcheck -c -d"):
800 if line == "Cygwin Package Information\r\n":
801 continue
802 if line == "\n":
803 continue
804 package, version = line.split()
805 if package == "Package" and version == "Version":
806 continue
807 clean_version = try_cleanup_distro_version(version)
808 if clean_version:
809 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
810 else:
811 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
813 self._write_cache(cache)
815 def get_package_info(self, package, factory):
816 # Add installed versions...
817 versions = self.versions.get(package, [])
819 for version, machine in versions:
820 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine))
821 impl.version = model.parse_version(version)
822 if machine != '*':
823 impl.machine = machine
825 def get_score(self, disto_name):
826 return int(disto_name == 'Cygwin')
829 _host_distribution = None
830 def get_host_distribution():
831 """Get a Distribution suitable for the host operating system.
832 Calling this twice will return the same object.
833 @rtype: L{Distribution}"""
834 global _host_distribution
835 if not _host_distribution:
836 dpkg_db_status = '/var/lib/dpkg/status'
837 rpm_db_packages = '/var/lib/rpm/Packages'
838 _slack_db = '/var/log/packages'
839 _arch_db = '/var/lib/pacman'
840 _pkg_db = '/var/db/pkg'
841 _macports_db = '/opt/local/var/macports/registry/registry.db'
842 _cygwin_log = '/var/log/setup.log'
844 if sys.prefix == "/sw":
845 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status)
846 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages)
848 if os.name == "nt":
849 _host_distribution = WindowsDistribution()
850 elif os.path.isdir(_pkg_db):
851 if sys.platform.startswith("linux"):
852 _host_distribution = GentooDistribution(_pkg_db)
853 elif sys.platform.startswith("freebsd"):
854 _host_distribution = PortsDistribution(_pkg_db)
855 elif os.path.isfile(_macports_db) \
856 and sys.prefix.startswith("/opt/local"):
857 _host_distribution = MacPortsDistribution(_macports_db)
858 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin":
859 _host_distribution = CygwinDistribution(_cygwin_log)
860 elif os.access(dpkg_db_status, os.R_OK) \
861 and os.path.getsize(dpkg_db_status) > 0:
862 _host_distribution = DebianDistribution(dpkg_db_status)
863 elif os.path.isfile(rpm_db_packages):
864 _host_distribution = RPMDistribution(rpm_db_packages)
865 elif os.path.isdir(_slack_db):
866 _host_distribution = SlackDistribution(_slack_db)
867 elif os.path.isdir(_arch_db):
868 _host_distribution = ArchDistribution(_arch_db)
869 elif sys.platform == "darwin":
870 _host_distribution = DarwinDistribution()
871 else:
872 _host_distribution = Distribution()
874 return _host_distribution