Implement a Darwin distro, for Java
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob9cdb3e52416bed161c8e25a9468e8b93d18865cd
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 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 -1 if there is no match at all
155 @rtype: int
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 DarwinDistribution(Distribution):
300 def get_package_info(self, package, factory):
301 def java_home(version, arch):
302 null = os.open(os.devnull, os.O_WRONLY)
303 child = subprocess.Popen(["/usr/libexec/java_home", "--failfast", "--version", version, "--arch", arch],
304 stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
305 home = child.stdout.read().strip()
306 child.stdout.close()
307 child.wait()
308 return home
310 def find_java(part, jvm_version, zero_version):
311 for arch in ['i386', 'x86_64']:
312 home = java_home(jvm_version, arch)
313 if os.path.isfile(home + "/bin/java"):
314 impl = factory('package:darwin:%s:%s:%s' % (package, zero_version, arch))
315 impl.machine = arch
316 impl.version = model.parse_version(zero_version)
317 impl.upstream_stability = model.packaged
318 impl.main = home + "/bin/java"
320 if package == 'openjdk-6-jre':
321 find_java("Java Runtime Environment", "1.6", '6')
322 elif package == 'openjdk-6-jdk':
323 find_java("Java Development Kit", "1.6", '6')
324 elif package == 'openjdk-7-jre':
325 find_java("Java Runtime Environment", "1.7", '7')
326 elif package == 'openjdk-7-jdk':
327 find_java("Java Development Kit", "1.7", '7')
329 def get_score(self, disto_name):
330 return int(disto_name == 'Darwin')
332 class CachedDistribution(Distribution):
333 """For distributions where querying the package database is slow (e.g. requires running
334 an external command), we cache the results.
335 @since: 0.39
336 @deprecated: use Cache instead
339 def __init__(self, db_status_file):
340 """@param db_status_file: update the cache when the timestamp of this file changes"""
341 self._status_details = os.stat(db_status_file)
343 self.versions = {}
344 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
345 namespaces.config_prog)
347 try:
348 self._load_cache()
349 except Exception as ex:
350 info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
351 try:
352 self.generate_cache()
353 self._load_cache()
354 except Exception as ex:
355 warn(_("Failed to regenerate distribution database cache: %s"), ex)
357 def _load_cache(self):
358 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
359 Throws an exception if the cache should be (re)created."""
360 with open(os.path.join(self.cache_dir, self.cache_leaf), 'rt') as stream:
361 cache_version = None
362 for line in stream:
363 if line == '\n':
364 break
365 name, value = line.split(': ')
366 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
367 raise Exception(_("Modification time of package database file has changed"))
368 if name == 'size' and int(value) != self._status_details.st_size:
369 raise Exception(_("Size of package database file has changed"))
370 if name == 'version':
371 cache_version = int(value)
372 else:
373 raise Exception(_('Invalid cache format (bad header)'))
375 if cache_version is None:
376 raise Exception(_('Old cache format'))
378 versions = self.versions
379 for line in stream:
380 package, version, zi_arch = line[:-1].split('\t')
381 versionarch = (version, intern(zi_arch))
382 if package not in versions:
383 versions[package] = [versionarch]
384 else:
385 versions[package].append(versionarch)
387 def _write_cache(self, cache):
388 #cache.sort() # Might be useful later; currently we don't care
389 import tempfile
390 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
391 dir = self.cache_dir)
392 try:
393 stream = os.fdopen(fd, 'wt')
394 stream.write('version: 2\n')
395 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
396 stream.write('size: %d\n' % self._status_details.st_size)
397 stream.write('\n')
398 for line in cache:
399 stream.write(line + '\n')
400 stream.close()
402 portable_rename(tmpname,
403 os.path.join(self.cache_dir,
404 self.cache_leaf))
405 except:
406 os.unlink(tmpname)
407 raise
409 # Maps machine type names used in packages to their Zero Install versions
410 _canonical_machine = {
411 'all' : '*',
412 'any' : '*',
413 'noarch' : '*',
414 '(none)' : '*',
415 'x86_64': 'x86_64',
416 'amd64': 'x86_64',
417 'i386': 'i386',
418 'i486': 'i486',
419 'i586': 'i586',
420 'i686': 'i686',
421 'ppc64': 'ppc64',
422 'ppc': 'ppc',
425 host_machine = arch.canonicalize_machine(platform.uname()[4])
426 def canonical_machine(package_machine):
427 machine = _canonical_machine.get(package_machine, None)
428 if machine is None:
429 # Safe default if we can't understand the arch
430 return host_machine
431 return machine
433 class DebianDistribution(Distribution):
434 """A dpkg-based distribution."""
436 cache_leaf = 'dpkg-status.cache'
438 def __init__(self, dpkg_status):
439 self.dpkg_cache = Cache('dpkg-status.cache', dpkg_status, 2)
440 self.apt_cache = {}
442 def _query_installed_package(self, package):
443 null = os.open(os.devnull, os.O_WRONLY)
444 child = subprocess.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package],
445 stdout = subprocess.PIPE, stderr = null,
446 universal_newlines = True) # Needed for Python 3
447 os.close(null)
448 stdout, stderr = child.communicate()
449 child.wait()
450 for line in stdout.split('\n'):
451 if not line: continue
452 version, debarch, status = line.split('\t', 2)
453 if not status.endswith(' installed'): continue
454 clean_version = try_cleanup_distro_version(version)
455 if debarch.find("-") != -1:
456 debarch = debarch.split("-")[-1]
457 if clean_version:
458 return '%s\t%s' % (clean_version, canonical_machine(debarch.strip()))
459 else:
460 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
462 return '-'
464 def get_package_info(self, package, factory):
465 # Add any already-installed package...
466 installed_cached_info = self._get_dpkg_info(package)
468 if installed_cached_info != '-':
469 installed_version, machine = installed_cached_info.split('\t')
470 impl = factory('package:deb:%s:%s:%s' % (package, installed_version, machine))
471 impl.version = model.parse_version(installed_version)
472 if machine != '*':
473 impl.machine = machine
474 else:
475 installed_version = None
477 # Add any uninstalled candidates (note: only one of these two methods will add anything)
479 # From PackageKit...
480 self.packagekit.get_candidates(package, factory, 'package:deb')
482 # From apt-cache...
483 cached = self.apt_cache.get(package, None)
484 if cached:
485 candidate_version = cached['version']
486 candidate_arch = cached['arch']
487 if candidate_version and candidate_version != installed_version:
488 impl = factory('package:deb:%s:%s:%s' % (package, candidate_version, candidate_arch), installed = False)
489 impl.version = model.parse_version(candidate_version)
490 if candidate_arch != '*':
491 impl.machine = candidate_arch
492 def install(handler):
493 raise model.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
494 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
495 "use that to install it.") % package)
496 impl.download_sources.append(model.DistributionSource(package, cached['size'], install, needs_confirmation = False))
498 def get_score(self, disto_name):
499 return int(disto_name == 'Debian')
501 def _get_dpkg_info(self, package):
502 installed_cached_info = self.dpkg_cache.get(package)
503 if installed_cached_info == None:
504 installed_cached_info = self._query_installed_package(package)
505 self.dpkg_cache.put(package, installed_cached_info)
507 return installed_cached_info
509 def fetch_candidates(self, master_feed):
510 package_names = [item.getAttribute("package") for item, item_attrs in master_feed.get_package_impls(self)]
512 if self.packagekit.available:
513 return self.packagekit.fetch_candidates(package_names)
515 # No PackageKit. Use apt-cache directly.
516 for package in package_names:
517 # Check to see whether we could get a newer version using apt-get
518 try:
519 null = os.open(os.devnull, os.O_WRONLY)
520 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null, universal_newlines = True)
521 os.close(null)
523 arch = version = size = None
524 for line in child.stdout:
525 line = line.strip()
526 if line.startswith('Version: '):
527 version = line[9:]
528 version = try_cleanup_distro_version(version)
529 elif line.startswith('Architecture: '):
530 arch = canonical_machine(line[14:].strip())
531 elif line.startswith('Size: '):
532 size = int(line[6:].strip())
533 if version and arch:
534 cached = {'version': version, 'arch': arch, 'size': size}
535 else:
536 cached = None
537 child.stdout.close()
538 child.wait()
539 except Exception as ex:
540 warn("'apt-cache show %s' failed: %s", package, ex)
541 cached = None
542 # (multi-arch support? can there be multiple candidates?)
543 self.apt_cache[package] = cached
545 class RPMDistribution(CachedDistribution):
546 """An RPM-based distribution."""
548 cache_leaf = 'rpm-status.cache'
550 def generate_cache(self):
551 cache = []
553 child = subprocess.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
554 stdout = subprocess.PIPE, universal_newlines = True)
555 for line in child.stdout:
556 package, version, rpmarch = line.split('\t', 2)
557 if package == 'gpg-pubkey':
558 continue
559 zi_arch = canonical_machine(rpmarch.strip())
560 clean_version = try_cleanup_distro_version(version)
561 if clean_version:
562 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
563 else:
564 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
566 self._write_cache(cache)
567 child.stdout.close()
568 child.wait()
570 def get_package_info(self, package, factory):
571 # Add installed versions...
572 versions = self.versions.get(package, [])
574 for version, machine in versions:
575 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
576 impl.version = model.parse_version(version)
577 if machine != '*':
578 impl.machine = machine
580 # Add any uninstalled candidates found by PackageKit
581 self.packagekit.get_candidates(package, factory, 'package:rpm')
583 def get_score(self, disto_name):
584 return int(disto_name == 'RPM')
586 class SlackDistribution(Distribution):
587 """A Slack-based distribution."""
589 def __init__(self, packages_dir):
590 self._packages_dir = packages_dir
592 def get_package_info(self, package, factory):
593 # Add installed versions...
594 for entry in os.listdir(self._packages_dir):
595 name, version, arch, build = entry.rsplit('-', 3)
596 if name == package:
597 zi_arch = canonical_machine(arch)
598 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
599 if not clean_version:
600 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
601 continue
603 impl = factory('package:slack:%s:%s:%s' % \
604 (package, clean_version, zi_arch))
605 impl.version = model.parse_version(clean_version)
606 if zi_arch != '*':
607 impl.machine = zi_arch
609 # Add any uninstalled candidates found by PackageKit
610 self.packagekit.get_candidates(package, factory, 'package:slack')
612 def get_score(self, disto_name):
613 return int(disto_name == 'Slack')
615 class ArchDistribution(Distribution):
616 """An Arch Linux distribution."""
618 def __init__(self, packages_dir):
619 self._packages_dir = os.path.join(packages_dir, "local")
621 def get_package_info(self, package, factory):
622 # Add installed versions...
623 for entry in os.listdir(self._packages_dir):
624 name, version, build = entry.rsplit('-', 2)
625 if name == package:
626 gotarch = False
627 with open(os.path.join(self._packages_dir, entry, "desc"), 'rt') as stream:
628 for line in stream:
629 if line == "%ARCH%\n":
630 gotarch = True
631 continue
632 if gotarch:
633 arch = line.strip()
634 break
635 zi_arch = canonical_machine(arch)
636 clean_version = try_cleanup_distro_version("%s-%s" % (version, build))
637 if not clean_version:
638 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name})
639 continue
641 impl = factory('package:arch:%s:%s:%s' % \
642 (package, clean_version, zi_arch))
643 impl.version = model.parse_version(clean_version)
644 if zi_arch != '*':
645 impl.machine = zi_arch
647 # Add any uninstalled candidates found by PackageKit
648 self.packagekit.get_candidates(package, factory, 'package:arch')
650 def get_score(self, disto_name):
651 return int(disto_name == 'Arch')
653 class GentooDistribution(Distribution):
655 def __init__(self, pkgdir):
656 self._pkgdir = pkgdir
658 def get_package_info(self, package, factory):
659 # Add installed versions...
660 _version_start_reqexp = '-[0-9]'
662 if package.count('/') != 1: return
664 category, leafname = package.split('/')
665 category_dir = os.path.join(self._pkgdir, category)
666 match_prefix = leafname + '-'
668 if not os.path.isdir(category_dir): return
670 for filename in os.listdir(category_dir):
671 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit():
672 with open(os.path.join(category_dir, filename, 'PF'), 'rt') as stream:
673 name = stream.readline().strip()
675 match = re.search(_version_start_reqexp, name)
676 if match is None:
677 warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name})
678 continue
679 else:
680 version = try_cleanup_distro_version(name[match.start() + 1:])
682 if category == 'app-emulation' and name.startswith('emul-'):
683 __, __, machine, __ = name.split('-', 3)
684 else:
685 with open(os.path.join(category_dir, filename, 'CHOST'), 'rt') as stream:
686 machine, __ = stream.readline().split('-', 1)
687 machine = arch.canonicalize_machine(machine)
689 impl = factory('package:gentoo:%s:%s:%s' % \
690 (package, version, machine))
691 impl.version = model.parse_version(version)
692 impl.machine = machine
694 # Add any uninstalled candidates found by PackageKit
695 self.packagekit.get_candidates(package, factory, 'package:gentoo')
697 def get_score(self, disto_name):
698 return int(disto_name == 'Gentoo')
700 class PortsDistribution(Distribution):
702 def __init__(self, pkgdir):
703 self._pkgdir = pkgdir
705 def get_package_info(self, package, factory):
706 _name_version_regexp = '^(.+)-([^-]+)$'
708 nameversion = re.compile(_name_version_regexp)
709 for pkgname in os.listdir(self._pkgdir):
710 pkgdir = os.path.join(self._pkgdir, pkgname)
711 if not os.path.isdir(pkgdir): continue
713 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
715 match = nameversion.search(pkgname)
716 if match is None:
717 warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname})
718 continue
719 else:
720 name = match.group(1)
721 if name != package:
722 continue
723 version = try_cleanup_distro_version(match.group(2))
725 machine = host_machine
727 impl = factory('package:ports:%s:%s:%s' % \
728 (package, version, machine))
729 impl.version = model.parse_version(version)
730 impl.machine = machine
732 def get_score(self, disto_name):
733 return int(disto_name == 'Ports')
735 class MacPortsDistribution(CachedDistribution):
737 cache_leaf = 'macports-status.cache'
739 def generate_cache(self):
740 cache = []
742 child = subprocess.Popen(["port", "-v", "installed"],
743 stdout = subprocess.PIPE, universal_newlines = True)
744 for line in child.stdout:
745 if not line.startswith(" "):
746 continue
747 if line.strip().count(" ") > 1:
748 package, version, extra = line.split(None, 2)
749 else:
750 package, version = line.split()
751 extra = ""
752 if not extra.startswith("(active)"):
753 continue
754 version = version.lstrip('@')
755 version = re.sub(r"\+.*", "", version) # strip variants
756 zi_arch = '*'
757 clean_version = try_cleanup_distro_version(version)
758 if clean_version:
759 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra)
760 if match:
761 platform, major, archs = match.groups()
762 for arch in archs.split():
763 zi_arch = canonical_machine(arch)
764 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
765 else:
766 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
767 else:
768 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
769 self._write_cache(cache)
770 child.stdout.close()
771 child.wait()
773 def get_package_info(self, package, factory):
774 # Add installed versions...
775 versions = self.versions.get(package, [])
777 for version, machine in versions:
778 impl = factory('package:macports:%s:%s:%s' % (package, version, machine))
779 impl.version = model.parse_version(version)
780 if machine != '*':
781 impl.machine = machine
783 def get_score(self, disto_name):
784 return int(disto_name == 'MacPorts')
786 class CygwinDistribution(CachedDistribution):
787 """A Cygwin-based distribution."""
789 cache_leaf = 'cygcheck-status.cache'
791 def generate_cache(self):
792 cache = []
794 zi_arch = canonical_machine(arch)
795 for line in os.popen("cygcheck -c -d"):
796 if line == "Cygwin Package Information\r\n":
797 continue
798 if line == "\n":
799 continue
800 package, version = line.split()
801 if package == "Package" and version == "Version":
802 continue
803 clean_version = try_cleanup_distro_version(version)
804 if clean_version:
805 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
806 else:
807 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
809 self._write_cache(cache)
811 def get_package_info(self, package, factory):
812 # Add installed versions...
813 versions = self.versions.get(package, [])
815 for version, machine in versions:
816 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine))
817 impl.version = model.parse_version(version)
818 if machine != '*':
819 impl.machine = machine
821 def get_score(self, disto_name):
822 return int(disto_name == 'Cygwin')
825 _host_distribution = None
826 def get_host_distribution():
827 """Get a Distribution suitable for the host operating system.
828 Calling this twice will return the same object.
829 @rtype: L{Distribution}"""
830 global _host_distribution
831 if not _host_distribution:
832 dpkg_db_status = '/var/lib/dpkg/status'
833 rpm_db_packages = '/var/lib/rpm/Packages'
834 _slack_db = '/var/log/packages'
835 _arch_db = '/var/lib/pacman'
836 _pkg_db = '/var/db/pkg'
837 _macports_db = '/opt/local/var/macports/registry/registry.db'
838 _cygwin_log = '/var/log/setup.log'
840 if sys.prefix == "/sw":
841 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status)
842 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages)
844 if os.name == "nt":
845 _host_distribution = WindowsDistribution()
846 elif os.path.isdir(_pkg_db):
847 if sys.platform.startswith("linux"):
848 _host_distribution = GentooDistribution(_pkg_db)
849 elif sys.platform.startswith("freebsd"):
850 _host_distribution = PortsDistribution(_pkg_db)
851 elif os.path.isfile(_macports_db) \
852 and sys.prefix.startswith("/opt/local"):
853 _host_distribution = MacPortsDistribution(_macports_db)
854 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin":
855 _host_distribution = CygwinDistribution(_cygwin_log)
856 elif os.access(dpkg_db_status, os.R_OK) \
857 and os.path.getsize(dpkg_db_status) > 0:
858 _host_distribution = DebianDistribution(dpkg_db_status)
859 elif os.path.isfile(rpm_db_packages):
860 _host_distribution = RPMDistribution(rpm_db_packages)
861 elif os.path.isdir(_slack_db):
862 _host_distribution = SlackDistribution(_slack_db)
863 elif os.path.isdir(_arch_db):
864 _host_distribution = ArchDistribution(_arch_db)
865 elif sys.platform == "darwin":
866 _host_distribution = DarwinDistribution()
867 else:
868 _host_distribution = Distribution()
870 return _host_distribution