Package zeroinstall :: Package injector :: Module distro
[frames] | no frames]

Source Code for Module zeroinstall.injector.distro

   1  """ 
   2  Integration with native distribution package managers. 
   3  @since: 0.28 
   4  """ 
   5   
   6  # Copyright (C) 2009, Thomas Leonard 
   7  # See the README file for details, or visit http://0install.net. 
   8   
   9  from zeroinstall import _, logger, gobject 
  10  import os, platform, re, subprocess, sys 
  11  from zeroinstall.injector import namespaces, model, arch, qdom 
  12  from zeroinstall.support import basedir, portable_rename, intern 
  13   
  14  _dotted_ints = '[0-9]+(?:\.[0-9]+)*' 
  15   
  16  # This matches a version number that would be a valid Zero Install version without modification 
  17  _zeroinstall_regexp = '(?:%s)(?:-(?:pre|rc|post|)(?:%s))*' % (_dotted_ints, _dotted_ints) 
  18   
  19  # This matches the interesting bits of distribution version numbers 
  20  # (first matching group is for Java-style 6b17 or 7u9 syntax, or "major") 
  21  _version_regexp = '(?:[a-z])?({ints}\.?[bu])?({zero})(-r{ints})?'.format(zero = _zeroinstall_regexp, ints = _dotted_ints) 
  22   
  23  _PYTHON_URI = 'http://repo.roscidus.com/python/python' 
24 25 # We try to do updates atomically without locking, but we don't worry too much about 26 # duplicate entries or being a little out of sync with the on-disk copy. 27 -class Cache(object):
28 - def __init__(self, cache_leaf, source, format):
29 """Maintain a cache file (e.g. ~/.cache/0install.net/injector/$name). 30 If the size or mtime of $source has changed, or the cache 31 format version if different, reset the cache first. 32 @type cache_leaf: str 33 @type source: str 34 @type format: int""" 35 self.cache_leaf = cache_leaf 36 self.source = source 37 self.format = format 38 self.cache_dir = basedir.save_cache_path(namespaces.config_site, 39 namespaces.config_prog) 40 self.cached_for = {} # Attributes of source when cache was created 41 try: 42 self._load_cache() 43 except Exception as ex: 44 logger.info(_("Failed to load cache (%s). Flushing..."), ex) 45 self.flush()
46
47 - def flush(self):
48 # Wipe the cache 49 try: 50 info = os.stat(self.source) 51 mtime = int(info.st_mtime) 52 size = info.st_size 53 except Exception as ex: 54 logger.warning("Failed to stat %s: %s", self.source, ex) 55 mtime = size = 0 56 self.cache = {} 57 import tempfile 58 tmp = tempfile.NamedTemporaryFile(mode = 'wt', dir = self.cache_dir, delete = False) 59 tmp.write("mtime=%d\nsize=%d\nformat=%d\n\n" % (mtime, size, self.format)) 60 tmp.close() 61 portable_rename(tmp.name, os.path.join(self.cache_dir, self.cache_leaf)) 62 63 self._load_cache()
64 65 # Populate self.cache from our saved cache file. 66 # Throws an exception if the cache doesn't exist or has the wrong format.
67 - def _load_cache(self):
68 self.cache = cache = {} 69 with open(os.path.join(self.cache_dir, self.cache_leaf)) as stream: 70 for line in stream: 71 line = line.strip() 72 if not line: 73 break 74 key, value = line.split('=', 1) 75 if key in ('mtime', 'size', 'format'): 76 self.cached_for[key] = int(value) 77 78 self._check_valid() 79 80 for line in stream: 81 key, value = line.split('=', 1) 82 cache[key] = value[:-1]
83 84 # Check the source file hasn't changed since we created the cache
85 - def _check_valid(self):
86 info = os.stat(self.source) 87 if self.cached_for['mtime'] != int(info.st_mtime): 88 raise Exception("Modification time of %s has changed" % self.source) 89 if self.cached_for['size'] != info.st_size: 90 raise Exception("Size of %s has changed" % self.source) 91 if self.cached_for.get('format', None) != self.format: 92 raise Exception("Format of cache has changed")
93
94 - def get(self, key):
95 """@type key: str 96 @rtype: str""" 97 try: 98 self._check_valid() 99 except Exception as ex: 100 logger.info(_("Cache needs to be refreshed: %s"), ex) 101 self.flush() 102 return None 103 else: 104 return self.cache.get(key, None)
105
106 - def put(self, key, value):
107 """@type key: str 108 @type value: str""" 109 cache_path = os.path.join(self.cache_dir, self.cache_leaf) 110 self.cache[key] = value 111 try: 112 with open(cache_path, 'a') as stream: 113 stream.write('%s=%s\n' % (key, value)) 114 except Exception as ex: 115 logger.warning("Failed to write to cache %s: %s=%s: %s", cache_path, key, value, ex)
116
117 -def try_cleanup_distro_version(version):
118 """Try to turn a distribution version string into one readable by Zero Install. 119 We do this by stripping off anything we can't parse. 120 @type version: str 121 @return: the part we understood, or None if we couldn't parse anything 122 @rtype: str""" 123 if ':' in version: 124 version = version.split(':')[1] # Skip 'epoch' 125 version = version.replace('_', '-') 126 if '~' in version: 127 version, suffix = version.split('~', 1) 128 if suffix.startswith('pre'): 129 suffix = suffix[3:] 130 suffix = '-pre' + (try_cleanup_distro_version(suffix) or '') 131 else: 132 suffix = '' 133 match = re.match(_version_regexp, version) 134 if match: 135 major, version, revision = match.groups() 136 if major is not None: 137 version = major[:-1].rstrip('.') + '.' + version 138 if revision is not None: 139 version = '%s-%s' % (version, revision[2:]) 140 return version + suffix 141 return None
142
143 -class Distribution(object):
144 """Represents a distribution with which we can integrate. 145 Sub-classes should specialise this to integrate with the package managers of 146 particular distributions. This base class ignores the native package manager. 147 @since: 0.28 148 @ivar name: the default value for Implementation.distro_name for our implementations 149 @type name: str 150 @ivar system_paths: list of paths to search for binaries (we MUST NOT find 0install launchers, so only include directories where system packages install binaries - e.g. /usr/bin but not /usr/local/bin) 151 @type system_paths: [str] 152 """ 153 154 name = "fallback" 155 156 _packagekit = None 157 158 system_paths = ['/usr/bin', '/bin', '/usr/sbin', '/sbin'] 159
160 - def get_package_info(self, package, factory):
161 """Get information about the given package. 162 Add zero or more implementations using the factory (typically at most two 163 will be added; the currently installed version and the latest available). 164 @param package: package name (e.g. "gimp") 165 @type package: str 166 @param factory: function for creating new DistributionImplementation objects from IDs 167 @type factory: str -> L{model.DistributionImplementation}""" 168 return
169
170 - def get_score(self, distribution):
171 """Indicate how closely the host distribution matches this one. 172 The <package-implementation> with the highest score is passed 173 to L{Distribution.get_package_info}. If several elements get 174 the same score, get_package_info is called for all of them. 175 @param distribution: a distribution name 176 @type distribution: str 177 @return: an integer, or -1 if there is no match at all 178 @rtype: int""" 179 return 0
180
181 - def get_feed(self, master_feed):
182 """Generate a feed containing information about distribution packages. 183 This should immediately return a feed containing an implementation for the 184 package if it's already installed. Information about versions that could be 185 installed using the distribution's package manager can be added asynchronously 186 later (see L{fetch_candidates}). 187 @param master_feed: feed containing the <package-implementation> elements 188 @type master_feed: L{model.ZeroInstallFeed} 189 @rtype: L{model.ZeroInstallFeed}""" 190 191 feed = model.ZeroInstallFeed(None) 192 feed.url = 'distribution:' + master_feed.url 193 194 for item, item_attrs, depends in master_feed.get_package_impls(self): 195 package = item_attrs.get('package', None) 196 if package is None: 197 raise model.InvalidInterface(_("Missing 'package' attribute on %s") % item) 198 199 new_impls = [] 200 201 def factory(id, only_if_missing = False, installed = True): 202 assert id.startswith('package:') 203 if id in feed.implementations: 204 if only_if_missing: 205 return None 206 logger.warning(_("Duplicate ID '%s' for DistributionImplementation"), id) 207 impl = model.DistributionImplementation(feed, id, self, item) 208 feed.implementations[id] = impl 209 new_impls.append(impl) 210 211 impl.installed = installed 212 impl.metadata = item_attrs 213 impl.requires = depends 214 215 if 'run' not in impl.commands: 216 item_main = item_attrs.get('main', None) 217 if item_main: 218 impl.main = item_main 219 impl.upstream_stability = model.packaged 220 221 return impl
222 223 self.get_package_info(package, factory) 224 225 for impl in new_impls: 226 self.fixup(package, impl) 227 if impl.installed: 228 self.installed_fixup(impl) 229 230 if master_feed.url == _PYTHON_URI and os.name != "nt": 231 # Hack: we can support Python on platforms with unsupported package managers 232 # by adding the implementation of Python running us now to the list. 233 python_version = '.'.join([str(v) for v in sys.version_info if isinstance(v, int)]) 234 impl_id = 'package:host:python:' + python_version 235 assert impl_id not in feed.implementations 236 impl = model.DistributionImplementation(feed, impl_id, self, distro_name = 'host') 237 impl.installed = True 238 impl.version = model.parse_version(python_version) 239 impl.main = sys.executable or '/usr/bin/python' 240 impl.upstream_stability = model.packaged 241 impl.machine = host_machine # (hopefully) 242 feed.implementations[impl_id] = impl 243 elif master_feed.url == 'http://repo.roscidus.com/python/python-gobject' and os.name != "nt" and gobject: 244 # Likewise, we know that there is a native python-gobject available for our Python 245 impl_id = 'package:host:python-gobject:' + '.'.join(str(x) for x in gobject.pygobject_version) 246 assert impl_id not in feed.implementations 247 impl = model.DistributionImplementation(feed, impl_id, self, distro_name = 'host') 248 impl.installed = True 249 impl.version = [list(gobject.pygobject_version)] 250 impl.upstream_stability = model.packaged 251 impl.machine = host_machine # (hopefully) 252 253 # Requires our version of Python too 254 restriction_element = qdom.Element(namespaces.XMLNS_IFACE, 'restricts', {'interface': _PYTHON_URI, 'distribution': 'host'}) 255 impl.requires.append(model.process_depends(restriction_element, None)) 256 257 feed.implementations[impl_id] = impl 258 259 return feed
260
261 - def fetch_candidates(self, master_feed):
262 """Collect information about versions we could install using 263 the distribution's package manager. On success, the distribution 264 feed in iface_cache is updated. 265 @return: a L{tasks.Blocker} if the task is in progress, or None if not""" 266 if self.packagekit.available: 267 package_names = [item.getAttribute("package") for item, item_attrs, depends in master_feed.get_package_impls(self)] 268 return self.packagekit.fetch_candidates(package_names)
269 270 @property
271 - def packagekit(self):
272 """For use by subclasses. 273 @rtype: L{packagekit.PackageKit}""" 274 if not self._packagekit: 275 from zeroinstall.injector import packagekit 276 self._packagekit = packagekit.PackageKit() 277 return self._packagekit
278
279 - def fixup(self, package, impl):
280 """Some packages require special handling (e.g. Java). This is called for each 281 package that was added by L{get_package_info} after it returns. The default 282 method does nothing. 283 @param package: the name of the package 284 @param impl: the constructed implementation""" 285 pass
286
287 - def installed_fixup(self, impl):
288 """Called when an installed package is added (after L{fixup}), or when installation 289 completes. This is useful to fix up the main value. 290 The default implementation checks that main exists, and searches L{Distribution.system_paths} for 291 it if not. 292 @type impl: L{DistributionImplementation} 293 @since: 1.11""" 294 295 run = impl.commands.get('run', None) 296 if not run: return 297 298 path = run.path 299 300 if not path: return 301 302 if os.path.isabs(path) and os.path.exists(path): 303 return 304 305 basename = os.path.basename(path) 306 if os.name == "nt" and not basename.endswith('.exe'): 307 basename += '.exe' 308 309 for d in self.system_paths: 310 path = os.path.join(d, basename) 311 if os.path.isfile(path): 312 logger.info("Found %s by searching system paths", path) 313 run.qdom.attrs["path"] = path 314 return 315 else: 316 logger.info("Binary '%s' not found in any system path (checked %s)", basename, self.system_paths)
317
318 - def get_score(self, distro_name):
319 """@type distro_name: str 320 @rtype: int""" 321 return int(distro_name == self.name)
322
323 -class WindowsDistribution(Distribution):
324 name = 'Windows' 325 326 system_paths = [] 327
328 - def get_package_info(self, package, factory):
329 def _is_64bit_windows(): 330 p = sys.platform 331 from win32process import IsWow64Process 332 if p == 'win64' or (p == 'win32' and IsWow64Process()): return True 333 elif p == 'win32': return False 334 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
335 336 def _read_hklm_reg(key_name, value_name): 337 from win32api import RegOpenKeyEx, RegQueryValueEx, RegCloseKey 338 from win32con import HKEY_LOCAL_MACHINE, KEY_READ 339 KEY_WOW64_64KEY = 0x0100 340 KEY_WOW64_32KEY = 0x0200 341 if _is_64bit_windows(): 342 try: 343 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_32KEY) 344 (value32, _) = RegQueryValueEx(key32, value_name) 345 RegCloseKey(key32) 346 except: 347 value32 = '' 348 try: 349 key64 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ | KEY_WOW64_64KEY) 350 (value64, _) = RegQueryValueEx(key64, value_name) 351 RegCloseKey(key64) 352 except: 353 value64 = '' 354 else: 355 try: 356 key32 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ) 357 (value32, _) = RegQueryValueEx(key32, value_name) 358 RegCloseKey(key32) 359 except: 360 value32 = '' 361 value64 = '' 362 return (value32, value64)
363 364 def find_java(part, win_version, zero_version): 365 reg_path = r"SOFTWARE\JavaSoft\{part}\{win_version}".format(part = part, win_version = win_version) 366 (java32_home, java64_home) = _read_hklm_reg(reg_path, "JavaHome") 367 368 for (home, arch) in [(java32_home, 'i486'), (java64_home, 'x86_64')]: 369 if os.path.isfile(home + r"\bin\java.exe"): 370 impl = factory('package:windows:%s:%s:%s' % (package, zero_version, arch)) 371 impl.machine = arch 372 impl.version = model.parse_version(zero_version) 373 impl.upstream_stability = model.packaged 374 impl.main = home + r"\bin\java.exe" 375 376 def find_netfx(win_version, zero_version): 377 reg_path = r"SOFTWARE\Microsoft\NET Framework Setup\NDP\{win_version}".format(win_version = win_version) 378 (netfx32_install, netfx64_install) = _read_hklm_reg(reg_path, "Install") 379 380 for (install, arch) in [(netfx32_install, 'i486'), (netfx64_install, 'x86_64')]: 381 impl = factory('package:windows:%s:%s:%s' % (package, zero_version, arch)) 382 impl.installed = (install == 1) 383 impl.machine = arch 384 impl.version = model.parse_version(zero_version) 385 impl.upstream_stability = model.packaged 386 impl.main = "" # .NET executables do not need a runner on Windows but they need one elsewhere 387 388 def find_netfx_release(win_version, release_version, zero_version): 389 reg_path = r"SOFTWARE\Microsoft\NET Framework Setup\NDP\{win_version}".format(win_version = win_version) 390 (netfx32_install, netfx64_install) = _read_hklm_reg(reg_path, "Install") 391 (netfx32_release, netfx64_release) = _read_hklm_reg(reg_path, "Release") 392 393 for (install, release, arch) in [(netfx32_install, netfx32_release, 'i486'), (netfx64_install, netfx64_release, 'x86_64')]: 394 impl = factory('package:windows:%s:%s:%s' % (package, zero_version, arch)) 395 impl.installed = (install == 1 and release != '' and release >= release_version) 396 impl.machine = arch 397 impl.version = model.parse_version(zero_version) 398 impl.upstream_stability = model.packaged 399 impl.main = "" # .NET executables do not need a runner on Windows but they need one elsewhere 400 401 if package == 'openjdk-6-jre': 402 find_java("Java Runtime Environment", "1.6", '6') 403 elif package == 'openjdk-6-jdk': 404 find_java("Java Development Kit", "1.6", '6') 405 elif package == 'openjdk-7-jre': 406 find_java("Java Runtime Environment", "1.7", '7') 407 elif package == 'openjdk-7-jdk': 408 find_java("Java Development Kit", "1.7", '7') 409 elif package == 'netfx': 410 find_netfx("v2.0.50727", '2.0') 411 find_netfx("v3.0", '3.0') 412 find_netfx("v3.5", '3.5') 413 find_netfx("v4\\Full", '4.0') 414 find_netfx_release("v4\\Full", 378389, '4.5') 415 find_netfx("v5", '5.0') 416 elif package == 'netfx-client': 417 find_netfx("v4\\Client", '4.0') 418 find_netfx_release("v4\\Client", 378389, '4.5') 419
420 -class DarwinDistribution(Distribution):
421 """@since: 1.11""" 422 423 name = 'Darwin' 424
425 - def get_package_info(self, package, factory):
426 """@type package: str""" 427 def java_home(version, arch): 428 null = os.open(os.devnull, os.O_WRONLY) 429 child = subprocess.Popen(["/usr/libexec/java_home", "--failfast", "--version", version, "--arch", arch], 430 stdout = subprocess.PIPE, stderr = null, universal_newlines = True) 431 home = child.stdout.read().strip() 432 child.stdout.close() 433 child.wait() 434 return home
435 436 def find_java(part, jvm_version, zero_version): 437 for arch in ['i386', 'x86_64']: 438 home = java_home(jvm_version, arch) 439 if os.path.isfile(home + "/bin/java"): 440 impl = factory('package:darwin:%s:%s:%s' % (package, zero_version, arch)) 441 impl.machine = arch 442 impl.version = model.parse_version(zero_version) 443 impl.upstream_stability = model.packaged 444 impl.main = home + "/bin/java"
445 446 if package == 'openjdk-6-jre': 447 find_java("Java Runtime Environment", "1.6", '6') 448 elif package == 'openjdk-6-jdk': 449 find_java("Java Development Kit", "1.6", '6') 450 elif package == 'openjdk-7-jre': 451 find_java("Java Runtime Environment", "1.7", '7') 452 elif package == 'openjdk-7-jdk': 453 find_java("Java Development Kit", "1.7", '7') 454 455 def get_output(args): 456 child = subprocess.Popen(args, stdout = subprocess.PIPE, universal_newlines = True) 457 return child.communicate()[0] 458 459 def get_version(program): 460 stdout = get_output([program, "--version"]) 461 return stdout.strip().split('\n')[0].split()[-1] # the last word of the first line 462 463 def find_program(file): 464 if os.path.isfile(file) and os.access(file, os.X_OK): 465 program_version = try_cleanup_distro_version(get_version(file)) 466 impl = factory('package:darwin:%s:%s' % (package, program_version), True) 467 if impl: 468 impl.installed = True 469 impl.version = model.parse_version(program_version) 470 impl.upstream_stability = model.packaged 471 impl.machine = host_machine # (hopefully) 472 impl.main = file 473 474 if package == 'gnupg': 475 find_program("/usr/local/bin/gpg") 476 elif package == 'gnupg2': 477 find_program("/usr/local/bin/gpg2") 478
479 -class CachedDistribution(Distribution):
480 """For distributions where querying the package database is slow (e.g. requires running 481 an external command), we cache the results. 482 @since: 0.39 483 @deprecated: use Cache instead 484 """ 485
486 - def __init__(self, db_status_file):
487 """@param db_status_file: update the cache when the timestamp of this file changes 488 @type db_status_file: str""" 489 self._status_details = os.stat(db_status_file) 490 491 self.versions = {} 492 self.cache_dir = basedir.save_cache_path(namespaces.config_site, 493 namespaces.config_prog) 494 495 try: 496 self._load_cache() 497 except Exception as ex: 498 logger.info(_("Failed to load distribution database cache (%s). Regenerating..."), ex) 499 try: 500 self.generate_cache() 501 self._load_cache() 502 except Exception as ex: 503 logger.warning(_("Failed to regenerate distribution database cache: %s"), ex)
504
505 - def _load_cache(self):
506 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date. 507 Throws an exception if the cache should be (re)created.""" 508 with open(os.path.join(self.cache_dir, self.cache_leaf), 'rt') as stream: 509 cache_version = None 510 for line in stream: 511 if line == '\n': 512 break 513 name, value = line.split(': ') 514 if name == 'mtime' and int(value) != int(self._status_details.st_mtime): 515 raise Exception(_("Modification time of package database file has changed")) 516 if name == 'size' and int(value) != self._status_details.st_size: 517 raise Exception(_("Size of package database file has changed")) 518 if name == 'version': 519 cache_version = int(value) 520 else: 521 raise Exception(_('Invalid cache format (bad header)')) 522 523 if cache_version is None: 524 raise Exception(_('Old cache format')) 525 526 versions = self.versions 527 for line in stream: 528 package, version, zi_arch = line[:-1].split('\t') 529 versionarch = (version, intern(zi_arch)) 530 if package not in versions: 531 versions[package] = [versionarch] 532 else: 533 versions[package].append(versionarch)
534
535 - def _write_cache(self, cache):
536 #cache.sort() # Might be useful later; currently we don't care 537 """@type cache: [str]""" 538 import tempfile 539 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp', 540 dir = self.cache_dir) 541 try: 542 stream = os.fdopen(fd, 'wt') 543 stream.write('version: 2\n') 544 stream.write('mtime: %d\n' % int(self._status_details.st_mtime)) 545 stream.write('size: %d\n' % self._status_details.st_size) 546 stream.write('\n') 547 for line in cache: 548 stream.write(line + '\n') 549 stream.close() 550 551 portable_rename(tmpname, 552 os.path.join(self.cache_dir, 553 self.cache_leaf)) 554 except: 555 os.unlink(tmpname) 556 raise
557 558 # Maps machine type names used in packages to their Zero Install versions 559 # (updates to this might require changing the reverse Java mapping) 560 _canonical_machine = { 561 'all' : '*', 562 'any' : '*', 563 'noarch' : '*', 564 '(none)' : '*', 565 'x86_64': 'x86_64', 566 'amd64': 'x86_64', 567 'i386': 'i386', 568 'i486': 'i486', 569 'i586': 'i586', 570 'i686': 'i686', 571 'ppc64': 'ppc64', 572 'ppc': 'ppc', 573 } 574 575 host_machine = arch.canonicalize_machine(platform.uname()[4])
576 -def canonical_machine(package_machine):
577 """@type package_machine: str 578 @rtype: str""" 579 machine = _canonical_machine.get(package_machine.lower(), None) 580 if machine is None: 581 # Safe default if we can't understand the arch 582 return host_machine.lower() 583 return machine
584
585 -class DebianDistribution(Distribution):
586 """A dpkg-based distribution.""" 587 588 name = 'Debian' 589 590 cache_leaf = 'dpkg-status.cache' 591
592 - def __init__(self, dpkg_status):
593 """@type dpkg_status: str""" 594 self.dpkg_cache = Cache('dpkg-status.cache', dpkg_status, 2) 595 self.apt_cache = {}
596
597 - def _query_installed_package(self, package):
598 """@type package: str 599 @rtype: str""" 600 null = os.open(os.devnull, os.O_WRONLY) 601 child = subprocess.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package], 602 stdout = subprocess.PIPE, stderr = null, 603 universal_newlines = True) # Needed for Python 3 604 os.close(null) 605 stdout, stderr = child.communicate() 606 child.wait() 607 for line in stdout.split('\n'): 608 if not line: continue 609 version, debarch, status = line.split('\t', 2) 610 if not status.endswith(' installed'): continue 611 clean_version = try_cleanup_distro_version(version) 612 if debarch.find("-") != -1: 613 debarch = debarch.split("-")[-1] 614 if clean_version: 615 return '%s\t%s' % (clean_version, canonical_machine(debarch.strip())) 616 else: 617 logger.warning(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package}) 618 619 return '-'
620
621 - def get_package_info(self, package, factory):
622 # Add any already-installed package... 623 """@type package: str""" 624 installed_cached_info = self._get_dpkg_info(package) 625 626 if installed_cached_info != '-': 627 installed_version, machine = installed_cached_info.split('\t') 628 impl = factory('package:deb:%s:%s:%s' % (package, installed_version, machine)) 629 impl.version = model.parse_version(installed_version) 630 if machine != '*': 631 impl.machine = machine 632 else: 633 installed_version = None 634 635 # Add any uninstalled candidates (note: only one of these two methods will add anything) 636 637 # From PackageKit... 638 self.packagekit.get_candidates(package, factory, 'package:deb') 639 640 # From apt-cache... 641 cached = self.apt_cache.get(package, None) 642 if cached: 643 candidate_version = cached['version'] 644 candidate_arch = cached['arch'] 645 if candidate_version and candidate_version != installed_version: 646 impl = factory('package:deb:%s:%s:%s' % (package, candidate_version, candidate_arch), installed = False) 647 impl.version = model.parse_version(candidate_version) 648 if candidate_arch != '*': 649 impl.machine = candidate_arch 650 def install(handler): 651 raise model.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. " 652 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can " 653 "use that to install it.") % package)
654 impl.download_sources.append(model.DistributionSource(package, cached['size'], install, needs_confirmation = False))
655
656 - def fixup(self, package, impl):
657 """@type package: str 658 @type impl: L{zeroinstall.injector.model.DistributionImplementation}""" 659 if impl.id.startswith('package:deb:openjdk-6-jre:') or \ 660 impl.id.startswith('package:deb:openjdk-7-jre:'): 661 # Debian marks all Java versions as pre-releases 662 # See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=685276 663 impl.version = model.parse_version(impl.get_version().replace('-pre', '.'))
664
665 - def installed_fixup(self, impl):
666 """@type impl: L{zeroinstall.injector.model.DistributionImplementation}""" 667 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME... 668 if impl.id.startswith('package:deb:openjdk-6-jre:'): 669 java_version = '6-openjdk' 670 elif impl.id.startswith('package:deb:openjdk-7-jre:'): 671 java_version = '7-openjdk' 672 else: 673 return Distribution.installed_fixup(self, impl) # super 674 675 if impl.machine == 'x86_64': 676 java_arch = 'amd64' 677 else: 678 java_arch = impl.machine 679 680 java_bin = '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version, java_arch) 681 if not os.path.exists(java_bin): 682 # Try without the arch... 683 java_bin = '/usr/lib/jvm/java-%s/jre/bin/java' % java_version 684 if not os.path.exists(java_bin): 685 logger.info("Java binary not found (%s)", java_bin) 686 if impl.main is None: 687 java_bin = '/usr/bin/java' 688 else: 689 return 690 691 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command', 692 {'path': java_bin, 'name': 'run'}), None)
693
694 - def _get_dpkg_info(self, package):
695 """@type package: str 696 @rtype: str""" 697 installed_cached_info = self.dpkg_cache.get(package) 698 if installed_cached_info == None: 699 installed_cached_info = self._query_installed_package(package) 700 self.dpkg_cache.put(package, installed_cached_info) 701 702 return installed_cached_info
703
704 - def fetch_candidates(self, master_feed):
705 """@type master_feed: L{zeroinstall.injector.model.ZeroInstallFeed} 706 @rtype: [L{zeroinstall.support.tasks.Blocker}]""" 707 package_names = [item.getAttribute("package") for item, item_attrs, depends in master_feed.get_package_impls(self)] 708 709 if self.packagekit.available: 710 return self.packagekit.fetch_candidates(package_names) 711 712 # No PackageKit. Use apt-cache directly. 713 for package in package_names: 714 # Check to see whether we could get a newer version using apt-get 715 try: 716 null = os.open(os.devnull, os.O_WRONLY) 717 child = subprocess.Popen(['apt-cache', 'show', '--no-all-versions', '--', package], stdout = subprocess.PIPE, stderr = null, universal_newlines = True) 718 os.close(null) 719 720 arch = version = size = None 721 for line in child.stdout: 722 line = line.strip() 723 if line.startswith('Version: '): 724 version = line[9:] 725 version = try_cleanup_distro_version(version) 726 elif line.startswith('Architecture: '): 727 arch = canonical_machine(line[14:].strip()) 728 elif line.startswith('Size: '): 729 size = int(line[6:].strip()) 730 if version and arch: 731 cached = {'version': version, 'arch': arch, 'size': size} 732 else: 733 cached = None 734 child.stdout.close() 735 child.wait() 736 except Exception as ex: 737 logger.warning("'apt-cache show %s' failed: %s", package, ex) 738 cached = None 739 # (multi-arch support? can there be multiple candidates?) 740 self.apt_cache[package] = cached
741
742 -class RPMDistribution(CachedDistribution):
743 """An RPM-based distribution.""" 744 745 name = 'RPM' 746 747 cache_leaf = 'rpm-status.cache' 748
749 - def generate_cache(self):
750 cache = [] 751 752 child = subprocess.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"], 753 stdout = subprocess.PIPE, universal_newlines = True) 754 for line in child.stdout: 755 package, version, rpmarch = line.split('\t', 2) 756 if package == 'gpg-pubkey': 757 continue 758 zi_arch = canonical_machine(rpmarch.strip()) 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 logger.warning(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package}) 764 765 self._write_cache(cache) 766 child.stdout.close() 767 child.wait()
768
769 - def get_package_info(self, package, factory):
770 # Add installed versions... 771 """@type package: str""" 772 versions = self.versions.get(package, []) 773 774 for version, machine in versions: 775 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine)) 776 impl.version = model.parse_version(version) 777 if machine != '*': 778 impl.machine = machine 779 780 # Add any uninstalled candidates found by PackageKit 781 self.packagekit.get_candidates(package, factory, 'package:rpm')
782
783 - def installed_fixup(self, impl):
784 # OpenSUSE uses _, Fedora uses . 785 """@type impl: L{zeroinstall.injector.model.DistributionImplementation}""" 786 impl_id = impl.id.replace('_', '.') 787 788 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME... 789 790 if impl_id.startswith('package:rpm:java-1.6.0-openjdk:'): 791 java_version = '1.6.0-openjdk' 792 elif impl_id.startswith('package:rpm:java-1.7.0-openjdk:'): 793 java_version = '1.7.0-openjdk' 794 else: 795 return Distribution.installed_fixup(self, impl) # super 796 797 # On Fedora, unlike Debian, the arch is x86_64, not amd64 798 799 java_bin = '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version, impl.machine) 800 if not os.path.exists(java_bin): 801 # Try without the arch... 802 java_bin = '/usr/lib/jvm/jre-%s/bin/java' % java_version 803 if not os.path.exists(java_bin): 804 logger.info("Java binary not found (%s)", java_bin) 805 if impl.main is None: 806 java_bin = '/usr/bin/java' 807 else: 808 return 809 810 impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command', 811 {'path': java_bin, 'name': 'run'}), None)
812
813 - def fixup(self, package, impl):
814 # OpenSUSE uses _, Fedora uses . 815 """@type package: str 816 @type impl: L{zeroinstall.injector.model.DistributionImplementation}""" 817 package = package.replace('_', '.') 818 819 if package in ('java-1.6.0-openjdk', 'java-1.7.0-openjdk', 820 'java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'): 821 if impl.version[0][0] == 1: 822 # OpenSUSE uses 1.6 to mean 6 823 del impl.version[0][0]
824
825 -class SlackDistribution(Distribution):
826 """A Slack-based distribution.""" 827 828 name = 'Slack' 829
830 - def __init__(self, packages_dir):
831 """@type packages_dir: str""" 832 self._packages_dir = packages_dir
833
834 - def get_package_info(self, package, factory):
835 # Add installed versions... 836 """@type package: str""" 837 for entry in os.listdir(self._packages_dir): 838 name, version, arch, build = entry.rsplit('-', 3) 839 if name == package: 840 zi_arch = canonical_machine(arch) 841 clean_version = try_cleanup_distro_version("%s-%s" % (version, build)) 842 if not clean_version: 843 logger.warning(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name}) 844 continue 845 846 impl = factory('package:slack:%s:%s:%s' % \ 847 (package, clean_version, zi_arch)) 848 impl.version = model.parse_version(clean_version) 849 if zi_arch != '*': 850 impl.machine = zi_arch 851 852 # Add any uninstalled candidates found by PackageKit 853 self.packagekit.get_candidates(package, factory, 'package:slack')
854
855 -class ArchDistribution(Distribution):
856 """An Arch Linux distribution.""" 857 858 name = 'Arch' 859
860 - def __init__(self, packages_dir):
861 """@type packages_dir: str""" 862 self._packages_dir = os.path.join(packages_dir, "local")
863
864 - def get_package_info(self, package, factory):
865 # Add installed versions... 866 """@type package: str""" 867 for entry in os.listdir(self._packages_dir): 868 name, version, build = entry.rsplit('-', 2) 869 if name == package: 870 gotarch = False 871 with open(os.path.join(self._packages_dir, entry, "desc"), 'rt') as stream: 872 for line in stream: 873 if line == "%ARCH%\n": 874 gotarch = True 875 continue 876 if gotarch: 877 arch = line.strip() 878 break 879 zi_arch = canonical_machine(arch) 880 clean_version = try_cleanup_distro_version("%s-%s" % (version, build)) 881 if not clean_version: 882 logger.warning(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': name}) 883 continue 884 885 impl = factory('package:arch:%s:%s:%s' % \ 886 (package, clean_version, zi_arch)) 887 impl.version = model.parse_version(clean_version) 888 if zi_arch != '*': 889 impl.machine = zi_arch 890 891 # Add any uninstalled candidates found by PackageKit 892 self.packagekit.get_candidates(package, factory, 'package:arch')
893
894 -class GentooDistribution(Distribution):
895 name = 'Gentoo' 896
897 - def __init__(self, pkgdir):
898 """@type pkgdir: str""" 899 self._pkgdir = pkgdir
900
901 - def get_package_info(self, package, factory):
902 # Add installed versions... 903 """@type package: str""" 904 _version_start_reqexp = '-[0-9]' 905 906 if package.count('/') != 1: return 907 908 category, leafname = package.split('/') 909 category_dir = os.path.join(self._pkgdir, category) 910 match_prefix = leafname + '-' 911 912 if not os.path.isdir(category_dir): return 913 914 for filename in os.listdir(category_dir): 915 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit(): 916 with open(os.path.join(category_dir, filename, 'PF'), 'rt') as stream: 917 name = stream.readline().strip() 918 919 match = re.search(_version_start_reqexp, name) 920 if match is None: 921 logger.warning(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name}) 922 continue 923 else: 924 version = try_cleanup_distro_version(name[match.start() + 1:]) 925 926 if category == 'app-emulation' and name.startswith('emul-'): 927 __, __, machine, __ = name.split('-', 3) 928 else: 929 with open(os.path.join(category_dir, filename, 'CHOST'), 'rt') as stream: 930 machine, __ = stream.readline().split('-', 1) 931 machine = arch.canonicalize_machine(machine) 932 933 impl = factory('package:gentoo:%s:%s:%s' % \ 934 (package, version, machine)) 935 impl.version = model.parse_version(version) 936 impl.machine = machine 937 938 # Add any uninstalled candidates found by PackageKit 939 self.packagekit.get_candidates(package, factory, 'package:gentoo')
940
941 -class PortsDistribution(Distribution):
942 name = 'Ports' 943 944 system_paths = ['/usr/local/bin'] 945
946 - def __init__(self, pkgdir):
947 """@type pkgdir: str""" 948 self._pkgdir = pkgdir
949
950 - def get_package_info(self, package, factory):
951 """@type package: str""" 952 _name_version_regexp = '^(.+)-([^-]+)$' 953 954 nameversion = re.compile(_name_version_regexp) 955 for pkgname in os.listdir(self._pkgdir): 956 pkgdir = os.path.join(self._pkgdir, pkgname) 957 if not os.path.isdir(pkgdir): continue 958 959 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip() 960 961 match = nameversion.search(pkgname) 962 if match is None: 963 logger.warning(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname}) 964 continue 965 else: 966 name = match.group(1) 967 if name != package: 968 continue 969 version = try_cleanup_distro_version(match.group(2)) 970 971 machine = host_machine 972 973 impl = factory('package:ports:%s:%s:%s' % \ 974 (package, version, machine)) 975 impl.version = model.parse_version(version) 976 impl.machine = machine
977
978 -class MacPortsDistribution(CachedDistribution):
979 system_paths = ['/opt/local/bin'] 980 981 name = 'MacPorts' 982
983 - def __init__(self, db_status_file):
984 """@type db_status_file: str""" 985 super(MacPortsDistribution, self).__init__(db_status_file) 986 self.darwin = DarwinDistribution()
987 988 cache_leaf = 'macports-status.cache' 989
990 - def generate_cache(self):
991 cache = [] 992 993 child = subprocess.Popen(["port", "-v", "installed"], 994 stdout = subprocess.PIPE, universal_newlines = True) 995 for line in child.stdout: 996 if not line.startswith(" "): 997 continue 998 if line.strip().count(" ") > 1: 999 package, version, extra = line.split(None, 2) 1000 else: 1001 package, version = line.split() 1002 extra = "" 1003 if not extra.startswith("(active)"): 1004 continue 1005 version = version.lstrip('@') 1006 version = re.sub(r"\+.*", "", version) # strip variants 1007 zi_arch = '*' 1008 clean_version = try_cleanup_distro_version(version) 1009 if clean_version: 1010 match = re.match(r" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra) 1011 if match: 1012 platform, major, archs = match.groups() 1013 for arch in archs.split(): 1014 zi_arch = canonical_machine(arch) 1015 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch)) 1016 else: 1017 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch)) 1018 else: 1019 logger.warning(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package}) 1020 self._write_cache(cache) 1021 child.stdout.close() 1022 child.wait()
1023
1024 - def get_package_info(self, package, factory):
1025 """@type package: str""" 1026 self.darwin.get_package_info(package, factory) 1027 1028 # Add installed versions... 1029 versions = self.versions.get(package, []) 1030 1031 for version, machine in versions: 1032 impl = factory('package:macports:%s:%s:%s' % (package, version, machine)) 1033 impl.version = model.parse_version(version) 1034 if machine != '*': 1035 impl.machine = machine
1036
1037 - def get_score(self, distro_name):
1038 # We support both sources of packages. 1039 # In theory, we should route 'Darwin' package names to DarwinDistribution, and 1040 # Mac Ports names to MacPortsDistribution. But since we only use Darwin for Java, 1041 # having one object handle both is OK. 1042 return int(distro_name in ('Darwin', 'MacPorts'))
1043
1044 -class CygwinDistribution(CachedDistribution):
1045 """A Cygwin-based distribution.""" 1046 1047 name = 'Cygwin' 1048 1049 cache_leaf = 'cygcheck-status.cache' 1050
1051 - def generate_cache(self):
1052 cache = [] 1053 1054 zi_arch = canonical_machine(arch) 1055 for line in os.popen("cygcheck -c -d"): 1056 if line == "Cygwin Package Information\r\n": 1057 continue 1058 if line == "\n": 1059 continue 1060 package, version = line.split() 1061 if package == "Package" and version == "Version": 1062 continue 1063 clean_version = try_cleanup_distro_version(version) 1064 if clean_version: 1065 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch)) 1066 else: 1067 logger.warning(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package}) 1068 1069 self._write_cache(cache)
1070
1071 - def get_package_info(self, package, factory):
1072 # Add installed versions... 1073 versions = self.versions.get(package, []) 1074 1075 for version, machine in versions: 1076 impl = factory('package:cygwin:%s:%s:%s' % (package, version, machine)) 1077 impl.version = model.parse_version(version) 1078 if machine != '*': 1079 impl.machine = machine
1080 1081 1082 _host_distribution = None
1083 -def get_host_distribution():
1084 """Get a Distribution suitable for the host operating system. 1085 Calling this twice will return the same object. 1086 @rtype: L{Distribution}""" 1087 global _host_distribution 1088 if not _host_distribution: 1089 dpkg_db_status = '/var/lib/dpkg/status' 1090 rpm_db_packages = '/var/lib/rpm/Packages' 1091 _slack_db = '/var/log/packages' 1092 _arch_db = '/var/lib/pacman' 1093 _pkg_db = '/var/db/pkg' 1094 _macports_db = '/opt/local/var/macports/registry/registry.db' 1095 _cygwin_log = '/var/log/setup.log' 1096 1097 if sys.prefix == "/sw": 1098 dpkg_db_status = os.path.join(sys.prefix, dpkg_db_status) 1099 rpm_db_packages = os.path.join(sys.prefix, rpm_db_packages) 1100 1101 if os.name == "nt": 1102 _host_distribution = WindowsDistribution() 1103 elif os.path.isdir(_pkg_db): 1104 if sys.platform.startswith("linux"): 1105 _host_distribution = GentooDistribution(_pkg_db) 1106 elif sys.platform.startswith("freebsd"): 1107 _host_distribution = PortsDistribution(_pkg_db) 1108 elif os.path.isfile(_macports_db): 1109 _host_distribution = MacPortsDistribution(_macports_db) 1110 elif os.path.isfile(_cygwin_log) and sys.platform == "cygwin": 1111 _host_distribution = CygwinDistribution(_cygwin_log) 1112 elif os.access(dpkg_db_status, os.R_OK) \ 1113 and os.path.getsize(dpkg_db_status) > 0: 1114 _host_distribution = DebianDistribution(dpkg_db_status) 1115 elif os.path.isfile(rpm_db_packages): 1116 _host_distribution = RPMDistribution(rpm_db_packages) 1117 elif os.path.isdir(_slack_db): 1118 _host_distribution = SlackDistribution(_slack_db) 1119 elif os.path.isdir(_arch_db): 1120 _host_distribution = ArchDistribution(_arch_db) 1121 elif sys.platform == "darwin": 1122 _host_distribution = DarwinDistribution() 1123 else: 1124 _host_distribution = Distribution() 1125 1126 return _host_distribution
1127