2 Integration with native distribution package managers.
6 # Copyright (C) 2009, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
9 from zeroinstall
import _
, logger
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
14 _dotted_ints
= '[0-9]+(?:\.[0-9]+)*'
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
)
19 # This matches the interesting bits of distribution version numbers
20 # (first matching group is for Java-style 6b17 syntax, or "major")
21 _version_regexp
= '(?:[a-z])?({ints}b)?({zero})(-r{ints})?'.format(zero
= _zeroinstall_regexp
, ints
= _dotted_ints
)
23 # We try to do updates atomically without locking, but we don't worry too much about
24 # duplicate entries or being a little out of sync with the on-disk copy.
26 def __init__(self
, cache_leaf
, source
, format
):
27 """Maintain a cache file (e.g. ~/.cache/0install.net/injector/$name).
28 If the size or mtime of $source has changed, or the cache
29 format version if different, reset the cache first."""
30 self
.cache_leaf
= cache_leaf
33 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
34 namespaces
.config_prog
)
35 self
.cached_for
= {} # Attributes of source when cache was created
38 except Exception as ex
:
39 logger
.info(_("Failed to load cache (%s). Flushing..."), ex
)
45 info
= os
.stat(self
.source
)
46 mtime
= int(info
.st_mtime
)
48 except Exception as ex
:
49 logger
.warn("Failed to stat %s: %s", self
.source
, ex
)
53 tmp
= tempfile
.NamedTemporaryFile(mode
= 'wt', dir = self
.cache_dir
, delete
= False)
54 tmp
.write("mtime=%d\nsize=%d\nformat=%d\n\n" % (mtime
, size
, self
.format
))
56 portable_rename(tmp
.name
, os
.path
.join(self
.cache_dir
, self
.cache_leaf
))
60 # Populate self.cache from our saved cache file.
61 # Throws an exception if the cache doesn't exist or has the wrong format.
62 def _load_cache(self
):
63 self
.cache
= cache
= {}
64 with
open(os
.path
.join(self
.cache_dir
, self
.cache_leaf
)) as stream
:
69 key
, value
= line
.split('=', 1)
70 if key
in ('mtime', 'size', 'format'):
71 self
.cached_for
[key
] = int(value
)
76 key
, value
= line
.split('=', 1)
77 cache
[key
] = value
[:-1]
79 # Check the source file hasn't changed since we created the cache
80 def _check_valid(self
):
81 info
= os
.stat(self
.source
)
82 if self
.cached_for
['mtime'] != int(info
.st_mtime
):
83 raise Exception("Modification time of %s has changed" % self
.source
)
84 if self
.cached_for
['size'] != info
.st_size
:
85 raise Exception("Size of %s has changed" % self
.source
)
86 if self
.cached_for
.get('format', None) != self
.format
:
87 raise Exception("Format of cache has changed")
92 except Exception as ex
:
93 logger
.info(_("Cache needs to be refreshed: %s"), ex
)
97 return self
.cache
.get(key
, None)
99 def put(self
, key
, value
):
100 cache_path
= os
.path
.join(self
.cache_dir
, self
.cache_leaf
)
101 self
.cache
[key
] = value
103 with
open(cache_path
, 'a') as stream
:
104 stream
.write('%s=%s\n' % (key
, value
))
105 except Exception as ex
:
106 logger
.warn("Failed to write to cache %s: %s=%s: %s", cache_path
, key
, value
, ex
)
108 def try_cleanup_distro_version(version
):
109 """Try to turn a distribution version string into one readable by Zero Install.
110 We do this by stripping off anything we can't parse.
111 @return: the part we understood, or None if we couldn't parse anything
114 version
= version
.split(':')[1] # Skip 'epoch'
115 version
= version
.replace('_', '-')
117 version
, suffix
= version
.split('~', 1)
118 if suffix
.startswith('pre'):
120 suffix
= '-pre' + (try_cleanup_distro_version(suffix
) or '')
123 match
= re
.match(_version_regexp
, version
)
125 major
, version
, revision
= match
.groups()
126 if major
is not None:
127 version
= major
[:-1] + '.' + version
128 if revision
is not None:
129 version
= '%s-%s' % (version
, revision
[2:])
130 return version
+ suffix
133 class Distribution(object):
134 """Represents a distribution with which we can integrate.
135 Sub-classes should specialise this to integrate with the package managers of
136 particular distributions. This base class ignores the native package manager.
141 def get_package_info(self
, package
, factory
):
142 """Get information about the given package.
143 Add zero or more implementations using the factory (typically at most two
144 will be added; the currently installed version and the latest available).
145 @param package: package name (e.g. "gimp")
147 @param factory: function for creating new DistributionImplementation objects from IDs
148 @type factory: str -> L{model.DistributionImplementation}
152 def get_score(self
, distribution
):
153 """Indicate how closely the host distribution matches this one.
154 The <package-implementation> with the highest score is passed
155 to L{Distribution.get_package_info}. If several elements get
156 the same score, get_package_info is called for all of them.
157 @param distribution: a distribution name
158 @type distribution: str
159 @return: an integer, or -1 if there is no match at all
164 def get_feed(self
, master_feed
):
165 """Generate a feed containing information about distribution packages.
166 This should immediately return a feed containing an implementation for the
167 package if it's already installed. Information about versions that could be
168 installed using the distribution's package manager can be added asynchronously
169 later (see L{fetch_candidates}).
170 @param master_feed: feed containing the <package-implementation> elements
171 @type master_feed: L{model.ZeroInstallFeed}
172 @rtype: L{model.ZeroInstallFeed}"""
174 feed
= model
.ZeroInstallFeed(None)
175 feed
.url
= 'distribution:' + master_feed
.url
177 for item
, item_attrs
in master_feed
.get_package_impls(self
):
178 package
= item_attrs
.get('package', None)
180 raise model
.InvalidInterface(_("Missing 'package' attribute on %s") % item
)
184 def factory(id, only_if_missing
= False, installed
= True):
185 assert id.startswith('package:')
186 if id in feed
.implementations
:
189 logger
.warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
190 impl
= model
.DistributionImplementation(feed
, id, self
, item
)
191 feed
.implementations
[id] = impl
192 new_impls
.append(impl
)
194 impl
.installed
= installed
195 impl
.metadata
= item_attrs
197 if 'run' not in impl
.commands
:
198 item_main
= item_attrs
.get('main', None)
200 if item_main
.startswith('/'):
201 impl
.main
= item_main
203 raise model
.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
205 impl
.upstream_stability
= model
.packaged
209 self
.get_package_info(package
, factory
)
211 for impl
in new_impls
:
212 self
.fixup(package
, impl
)
214 self
.installed_fixup(impl
)
216 if master_feed
.url
== 'http://repo.roscidus.com/python/python' and all(not impl
.installed
for impl
in feed
.implementations
.values()):
217 # Hack: we can support Python on platforms with unsupported package managers
218 # by adding the implementation of Python running us now to the list.
219 python_version
= '.'.join([str(v
) for v
in sys
.version_info
if isinstance(v
, int)])
220 impl_id
= 'package:host:python:' + python_version
221 assert impl_id
not in feed
.implementations
222 impl
= model
.DistributionImplementation(feed
, impl_id
, self
)
223 impl
.installed
= True
224 impl
.version
= model
.parse_version(python_version
)
225 impl
.main
= sys
.executable
226 impl
.upstream_stability
= model
.packaged
227 impl
.machine
= host_machine
# (hopefully)
228 feed
.implementations
[impl_id
] = impl
232 def fetch_candidates(self
, master_feed
):
233 """Collect information about versions we could install using
234 the distribution's package manager. On success, the distribution
235 feed in iface_cache is updated.
236 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
237 if self
.packagekit
.available
:
238 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
239 return self
.packagekit
.fetch_candidates(package_names
)
242 def packagekit(self
):
243 """For use by subclasses.
244 @rtype: L{packagekit.PackageKit}"""
245 if not self
._packagekit
:
246 from zeroinstall
.injector
import packagekit
247 self
._packagekit
= packagekit
.PackageKit()
248 return self
._packagekit
250 def fixup(self
, package
, impl
):
251 """Some packages require special handling (e.g. Java). This is called for each
252 package that was added by L{get_package_info} after it returns. The default
254 @param package: the name of the package
255 @param impl: the constructed implementation"""
258 def installed_fixup(self
, impl
):
259 """Called when an installed package is added (after L{fixup}), or when installation
260 completes. This is useful to fix up the main value.
261 @type impl: L{DistributionImplementation}
265 class WindowsDistribution(Distribution
):
266 def get_package_info(self
, package
, factory
):
267 def _is_64bit_windows():
269 from win32process
import IsWow64Process
270 if p
== 'win64' or (p
== 'win32' and IsWow64Process()): return True
271 elif p
== 'win32': return False
272 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
274 def _read_hklm_reg(key_name
, value_name
):
275 from win32api
import RegOpenKeyEx
, RegQueryValueEx
, RegCloseKey
276 from win32con
import HKEY_LOCAL_MACHINE
, KEY_READ
277 KEY_WOW64_64KEY
= 0x0100
278 KEY_WOW64_32KEY
= 0x0200
279 if _is_64bit_windows():
281 key32
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ | KEY_WOW64_32KEY
)
282 (value32
, _
) = RegQueryValueEx(key32
, value_name
)
287 key64
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ | KEY_WOW64_64KEY
)
288 (value64
, _
) = RegQueryValueEx(key64
, value_name
)
294 key32
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ
)
295 (value32
, _
) = RegQueryValueEx(key32
, value_name
)
300 return (value32
, value64
)
302 def find_java(part
, win_version
, zero_version
):
303 reg_path
= r
"SOFTWARE\JavaSoft\{part}\{win_version}".format(part
= part
, win_version
= win_version
)
304 (java32_home
, java64_home
) = _read_hklm_reg(reg_path
, "JavaHome")
306 for (home
, arch
) in [(java32_home
, 'i486'),
307 (java64_home
, 'x86_64')]:
308 if os
.path
.isfile(home
+ r
"\bin\java.exe"):
309 impl
= factory('package:windows:%s:%s:%s' % (package
, zero_version
, arch
))
311 impl
.version
= model
.parse_version(zero_version
)
312 impl
.upstream_stability
= model
.packaged
313 impl
.main
= home
+ r
"\bin\java.exe"
315 if package
== 'openjdk-6-jre':
316 find_java("Java Runtime Environment", "1.6", '6')
317 elif package
== 'openjdk-6-jdk':
318 find_java("Java Development Kit", "1.6", '6')
319 elif package
== 'openjdk-7-jre':
320 find_java("Java Runtime Environment", "1.7", '7')
321 elif package
== 'openjdk-7-jdk':
322 find_java("Java Development Kit", "1.7", '7')
324 def get_score(self
, disto_name
):
325 return int(disto_name
== 'Windows')
327 class DarwinDistribution(Distribution
):
328 def get_package_info(self
, package
, factory
):
329 def java_home(version
, arch
):
330 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
331 child
= subprocess
.Popen(["/usr/libexec/java_home", "--failfast", "--version", version
, "--arch", arch
],
332 stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
333 home
= child
.stdout
.read().strip()
338 def find_java(part
, jvm_version
, zero_version
):
339 for arch
in ['i386', 'x86_64']:
340 home
= java_home(jvm_version
, arch
)
341 if os
.path
.isfile(home
+ "/bin/java"):
342 impl
= factory('package:darwin:%s:%s:%s' % (package
, zero_version
, arch
))
344 impl
.version
= model
.parse_version(zero_version
)
345 impl
.upstream_stability
= model
.packaged
346 impl
.main
= home
+ "/bin/java"
348 if package
== 'openjdk-6-jre':
349 find_java("Java Runtime Environment", "1.6", '6')
350 elif package
== 'openjdk-6-jdk':
351 find_java("Java Development Kit", "1.6", '6')
352 elif package
== 'openjdk-7-jre':
353 find_java("Java Runtime Environment", "1.7", '7')
354 elif package
== 'openjdk-7-jdk':
355 find_java("Java Development Kit", "1.7", '7')
357 def get_score(self
, disto_name
):
358 return int(disto_name
== 'Darwin')
360 class CachedDistribution(Distribution
):
361 """For distributions where querying the package database is slow (e.g. requires running
362 an external command), we cache the results.
364 @deprecated: use Cache instead
367 def __init__(self
, db_status_file
):
368 """@param db_status_file: update the cache when the timestamp of this file changes"""
369 self
._status
_details
= os
.stat(db_status_file
)
372 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
373 namespaces
.config_prog
)
377 except Exception as ex
:
378 logger
.info(_("Failed to load distribution database cache (%s). Regenerating..."), ex
)
380 self
.generate_cache()
382 except Exception as ex
:
383 logger
.warn(_("Failed to regenerate distribution database cache: %s"), ex
)
385 def _load_cache(self
):
386 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
387 Throws an exception if the cache should be (re)created."""
388 with
open(os
.path
.join(self
.cache_dir
, self
.cache_leaf
), 'rt') as stream
:
393 name
, value
= line
.split(': ')
394 if name
== 'mtime' and int(value
) != int(self
._status
_details
.st_mtime
):
395 raise Exception(_("Modification time of package database file has changed"))
396 if name
== 'size' and int(value
) != self
._status
_details
.st_size
:
397 raise Exception(_("Size of package database file has changed"))
398 if name
== 'version':
399 cache_version
= int(value
)
401 raise Exception(_('Invalid cache format (bad header)'))
403 if cache_version
is None:
404 raise Exception(_('Old cache format'))
406 versions
= self
.versions
408 package
, version
, zi_arch
= line
[:-1].split('\t')
409 versionarch
= (version
, intern(zi_arch
))
410 if package
not in versions
:
411 versions
[package
] = [versionarch
]
413 versions
[package
].append(versionarch
)
415 def _write_cache(self
, cache
):
416 #cache.sort() # Might be useful later; currently we don't care
418 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'zeroinstall-cache-tmp',
419 dir = self
.cache_dir
)
421 stream
= os
.fdopen(fd
, 'wt')
422 stream
.write('version: 2\n')
423 stream
.write('mtime: %d\n' % int(self
._status
_details
.st_mtime
))
424 stream
.write('size: %d\n' % self
._status
_details
.st_size
)
427 stream
.write(line
+ '\n')
430 portable_rename(tmpname
,
431 os
.path
.join(self
.cache_dir
,
437 # Maps machine type names used in packages to their Zero Install versions
438 # (updates to this might require changing the reverse Java mapping)
439 _canonical_machine
= {
454 host_machine
= arch
.canonicalize_machine(platform
.uname()[4])
455 def canonical_machine(package_machine
):
456 machine
= _canonical_machine
.get(package_machine
, None)
458 # Safe default if we can't understand the arch
462 class DebianDistribution(Distribution
):
463 """A dpkg-based distribution."""
465 cache_leaf
= 'dpkg-status.cache'
467 def __init__(self
, dpkg_status
):
468 self
.dpkg_cache
= Cache('dpkg-status.cache', dpkg_status
, 2)
471 def _query_installed_package(self
, package
):
472 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
473 child
= subprocess
.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package
],
474 stdout
= subprocess
.PIPE
, stderr
= null
,
475 universal_newlines
= True) # Needed for Python 3
477 stdout
, stderr
= child
.communicate()
479 for line
in stdout
.split('\n'):
480 if not line
: continue
481 version
, debarch
, status
= line
.split('\t', 2)
482 if not status
.endswith(' installed'): continue
483 clean_version
= try_cleanup_distro_version(version
)
484 if debarch
.find("-") != -1:
485 debarch
= debarch
.split("-")[-1]
487 return '%s\t%s' % (clean_version
, canonical_machine(debarch
.strip()))
489 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
493 def get_package_info(self
, package
, factory
):
494 # Add any already-installed package...
495 installed_cached_info
= self
._get
_dpkg
_info
(package
)
497 if installed_cached_info
!= '-':
498 installed_version
, machine
= installed_cached_info
.split('\t')
499 impl
= factory('package:deb:%s:%s:%s' % (package
, installed_version
, machine
))
500 impl
.version
= model
.parse_version(installed_version
)
502 impl
.machine
= machine
504 installed_version
= None
506 # Add any uninstalled candidates (note: only one of these two methods will add anything)
509 self
.packagekit
.get_candidates(package
, factory
, 'package:deb')
512 cached
= self
.apt_cache
.get(package
, None)
514 candidate_version
= cached
['version']
515 candidate_arch
= cached
['arch']
516 if candidate_version
and candidate_version
!= installed_version
:
517 impl
= factory('package:deb:%s:%s:%s' % (package
, candidate_version
, candidate_arch
), installed
= False)
518 impl
.version
= model
.parse_version(candidate_version
)
519 if candidate_arch
!= '*':
520 impl
.machine
= candidate_arch
521 def install(handler
):
522 raise model
.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
523 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
524 "use that to install it.") % package
)
525 impl
.download_sources
.append(model
.DistributionSource(package
, cached
['size'], install
, needs_confirmation
= False))
527 def fixup(self
, package
, impl
):
528 if impl
.id.startswith('package:deb:openjdk-6-jre:') or \
529 impl
.id.startswith('package:deb:openjdk-7-jre:'):
530 # Debian marks all Java versions as pre-releases
531 # See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=685276
532 impl
.version
= model
.parse_version(impl
.get_version().replace('-pre', '.'))
534 def installed_fixup(self
, impl
):
535 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
536 if impl
.id.startswith('package:deb:openjdk-6-jre:'):
537 java_version
= '6-openjdk'
538 elif impl
.id.startswith('package:deb:openjdk-7-jre:'):
539 java_version
= '7-openjdk'
543 if impl
.machine
== 'x86_64':
546 java_arch
= impl
.machine
548 java_bin
= '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version
, java_arch
)
549 if not os
.path
.exists(java_bin
):
550 # Try without the arch...
551 java_bin
= '/usr/lib/jvm/java-%s/jre/bin/java' % java_version
552 if not os
.path
.exists(java_bin
):
553 logger
.info("Java binary not found (%s)", java_bin
)
554 if impl
.main
is None:
555 java_bin
= '/usr/bin/java'
559 impl
.commands
["run"] = model
.Command(qdom
.Element(namespaces
.XMLNS_IFACE
, 'command',
560 {'path': java_bin
, 'name': 'run'}), None)
562 def get_score(self
, disto_name
):
563 return int(disto_name
== 'Debian')
565 def _get_dpkg_info(self
, package
):
566 installed_cached_info
= self
.dpkg_cache
.get(package
)
567 if installed_cached_info
== None:
568 installed_cached_info
= self
._query
_installed
_package
(package
)
569 self
.dpkg_cache
.put(package
, installed_cached_info
)
571 return installed_cached_info
573 def fetch_candidates(self
, master_feed
):
574 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
576 if self
.packagekit
.available
:
577 return self
.packagekit
.fetch_candidates(package_names
)
579 # No PackageKit. Use apt-cache directly.
580 for package
in package_names
:
581 # Check to see whether we could get a newer version using apt-get
583 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
584 child
= subprocess
.Popen(['apt-cache', 'show', '--no-all-versions', '--', package
], stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
587 arch
= version
= size
= None
588 for line
in child
.stdout
:
590 if line
.startswith('Version: '):
592 version
= try_cleanup_distro_version(version
)
593 elif line
.startswith('Architecture: '):
594 arch
= canonical_machine(line
[14:].strip())
595 elif line
.startswith('Size: '):
596 size
= int(line
[6:].strip())
598 cached
= {'version': version
, 'arch': arch
, 'size': size
}
603 except Exception as ex
:
604 logger
.warn("'apt-cache show %s' failed: %s", package
, ex
)
606 # (multi-arch support? can there be multiple candidates?)
607 self
.apt_cache
[package
] = cached
609 class RPMDistribution(CachedDistribution
):
610 """An RPM-based distribution."""
612 cache_leaf
= 'rpm-status.cache'
614 def generate_cache(self
):
617 child
= subprocess
.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
618 stdout
= subprocess
.PIPE
, universal_newlines
= True)
619 for line
in child
.stdout
:
620 package
, version
, rpmarch
= line
.split('\t', 2)
621 if package
== 'gpg-pubkey':
623 zi_arch
= canonical_machine(rpmarch
.strip())
624 clean_version
= try_cleanup_distro_version(version
)
626 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
628 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
630 self
._write
_cache
(cache
)
634 def get_package_info(self
, package
, factory
):
635 # Add installed versions...
636 versions
= self
.versions
.get(package
, [])
638 for version
, machine
in versions
:
639 impl
= factory('package:rpm:%s:%s:%s' % (package
, version
, machine
))
640 impl
.version
= model
.parse_version(version
)
642 impl
.machine
= machine
644 # Add any uninstalled candidates found by PackageKit
645 self
.packagekit
.get_candidates(package
, factory
, 'package:rpm')
647 def installed_fixup(self
, impl
):
648 # OpenSUSE uses _, Fedora uses .
649 impl_id
= impl
.id.replace('_', '.')
651 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
653 if impl_id
.startswith('package:rpm:java-1.6.0-openjdk:'):
654 java_version
= '1.6.0-openjdk'
655 elif impl_id
.startswith('package:rpm:java-1.7.0-openjdk:'):
656 java_version
= '1.7.0-openjdk'
660 # On Fedora, unlike Debian, the arch is x86_64, not amd64
662 java_bin
= '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version
, impl
.machine
)
663 if not os
.path
.exists(java_bin
):
664 # Try without the arch...
665 java_bin
= '/usr/lib/jvm/jre-%s/bin/java' % java_version
666 if not os
.path
.exists(java_bin
):
667 logger
.info("Java binary not found (%s)", java_bin
)
668 if impl
.main
is None:
669 java_bin
= '/usr/bin/java'
673 impl
.commands
["run"] = model
.Command(qdom
.Element(namespaces
.XMLNS_IFACE
, 'command',
674 {'path': java_bin
, 'name': 'run'}), None)
676 def fixup(self
, package
, impl
):
677 # OpenSUSE uses _, Fedora uses .
678 package
= package
.replace('_', '.')
680 if package
in ('java-1.6.0-openjdk', 'java-1.7.0-openjdk',
681 'java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'):
682 if impl
.version
[0][0] == 1:
683 # OpenSUSE uses 1.6 to mean 6
684 del impl
.version
[0][0]
686 def get_score(self
, disto_name
):
687 return int(disto_name
== 'RPM')
689 class SlackDistribution(Distribution
):
690 """A Slack-based distribution."""
692 def __init__(self
, packages_dir
):
693 self
._packages
_dir
= packages_dir
695 def get_package_info(self
, package
, factory
):
696 # Add installed versions...
697 for entry
in os
.listdir(self
._packages
_dir
):
698 name
, version
, arch
, build
= entry
.rsplit('-', 3)
700 zi_arch
= canonical_machine(arch
)
701 clean_version
= try_cleanup_distro_version("%s-%s" % (version
, build
))
702 if not clean_version
:
703 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': name
})
706 impl
= factory('package:slack:%s:%s:%s' % \
707 (package
, clean_version
, zi_arch
))
708 impl
.version
= model
.parse_version(clean_version
)
710 impl
.machine
= zi_arch
712 # Add any uninstalled candidates found by PackageKit
713 self
.packagekit
.get_candidates(package
, factory
, 'package:slack')
715 def get_score(self
, disto_name
):
716 return int(disto_name
== 'Slack')
718 class ArchDistribution(Distribution
):
719 """An Arch Linux distribution."""
721 def __init__(self
, packages_dir
):
722 self
._packages
_dir
= os
.path
.join(packages_dir
, "local")
724 def get_package_info(self
, package
, factory
):
725 # Add installed versions...
726 for entry
in os
.listdir(self
._packages
_dir
):
727 name
, version
, build
= entry
.rsplit('-', 2)
730 with
open(os
.path
.join(self
._packages
_dir
, entry
, "desc"), 'rt') as stream
:
732 if line
== "%ARCH%\n":
738 zi_arch
= canonical_machine(arch
)
739 clean_version
= try_cleanup_distro_version("%s-%s" % (version
, build
))
740 if not clean_version
:
741 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': name
})
744 impl
= factory('package:arch:%s:%s:%s' % \
745 (package
, clean_version
, zi_arch
))
746 impl
.version
= model
.parse_version(clean_version
)
748 impl
.machine
= zi_arch
750 # Add any uninstalled candidates found by PackageKit
751 self
.packagekit
.get_candidates(package
, factory
, 'package:arch')
753 def get_score(self
, disto_name
):
754 return int(disto_name
== 'Arch')
756 class GentooDistribution(Distribution
):
758 def __init__(self
, pkgdir
):
759 self
._pkgdir
= pkgdir
761 def get_package_info(self
, package
, factory
):
762 # Add installed versions...
763 _version_start_reqexp
= '-[0-9]'
765 if package
.count('/') != 1: return
767 category
, leafname
= package
.split('/')
768 category_dir
= os
.path
.join(self
._pkgdir
, category
)
769 match_prefix
= leafname
+ '-'
771 if not os
.path
.isdir(category_dir
): return
773 for filename
in os
.listdir(category_dir
):
774 if filename
.startswith(match_prefix
) and filename
[len(match_prefix
)].isdigit():
775 with
open(os
.path
.join(category_dir
, filename
, 'PF'), 'rt') as stream
:
776 name
= stream
.readline().strip()
778 match
= re
.search(_version_start_reqexp
, name
)
780 logger
.warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name
})
783 version
= try_cleanup_distro_version(name
[match
.start() + 1:])
785 if category
== 'app-emulation' and name
.startswith('emul-'):
786 __
, __
, machine
, __
= name
.split('-', 3)
788 with
open(os
.path
.join(category_dir
, filename
, 'CHOST'), 'rt') as stream
:
789 machine
, __
= stream
.readline().split('-', 1)
790 machine
= arch
.canonicalize_machine(machine
)
792 impl
= factory('package:gentoo:%s:%s:%s' % \
793 (package
, version
, machine
))
794 impl
.version
= model
.parse_version(version
)
795 impl
.machine
= machine
797 # Add any uninstalled candidates found by PackageKit
798 self
.packagekit
.get_candidates(package
, factory
, 'package:gentoo')
800 def get_score(self
, disto_name
):
801 return int(disto_name
== 'Gentoo')
803 class PortsDistribution(Distribution
):
805 def __init__(self
, pkgdir
):
806 self
._pkgdir
= pkgdir
808 def get_package_info(self
, package
, factory
):
809 _name_version_regexp
= '^(.+)-([^-]+)$'
811 nameversion
= re
.compile(_name_version_regexp
)
812 for pkgname
in os
.listdir(self
._pkgdir
):
813 pkgdir
= os
.path
.join(self
._pkgdir
, pkgname
)
814 if not os
.path
.isdir(pkgdir
): continue
816 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
818 match
= nameversion
.search(pkgname
)
820 logger
.warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname
})
823 name
= match
.group(1)
826 version
= try_cleanup_distro_version(match
.group(2))
828 machine
= host_machine
830 impl
= factory('package:ports:%s:%s:%s' % \
831 (package
, version
, machine
))
832 impl
.version
= model
.parse_version(version
)
833 impl
.machine
= machine
835 def get_score(self
, disto_name
):
836 return int(disto_name
== 'Ports')
838 class MacPortsDistribution(CachedDistribution
):
839 def __init__(self
, db_status_file
):
840 super(MacPortsDistribution
, self
).__init
__(db_status_file
)
841 self
.darwin
= DarwinDistribution()
843 cache_leaf
= 'macports-status.cache'
845 def generate_cache(self
):
848 child
= subprocess
.Popen(["port", "-v", "installed"],
849 stdout
= subprocess
.PIPE
, universal_newlines
= True)
850 for line
in child
.stdout
:
851 if not line
.startswith(" "):
853 if line
.strip().count(" ") > 1:
854 package
, version
, extra
= line
.split(None, 2)
856 package
, version
= line
.split()
858 if not extra
.startswith("(active)"):
860 version
= version
.lstrip('@')
861 version
= re
.sub(r
"\+.*", "", version
) # strip variants
863 clean_version
= try_cleanup_distro_version(version
)
865 match
= re
.match(r
" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra
)
867 platform
, major
, archs
= match
.groups()
868 for arch
in archs
.split():
869 zi_arch
= canonical_machine(arch
)
870 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
872 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
874 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
875 self
._write
_cache
(cache
)
879 def get_package_info(self
, package
, factory
):
880 self
.darwin
.get_package_info(package
, factory
)
882 # Add installed versions...
883 versions
= self
.versions
.get(package
, [])
885 for version
, machine
in versions
:
886 impl
= factory('package:macports:%s:%s:%s' % (package
, version
, machine
))
887 impl
.version
= model
.parse_version(version
)
889 impl
.machine
= machine
891 def get_score(self
, disto_name
):
892 # We support both sources of packages.
893 # In theory, we should route 'Darwin' package names to DarwinDistribution, and
894 # Mac Ports names to MacPortsDistribution. But since we only use Darwin for Java,
895 # having one object handle both is OK.
896 return int(disto_name
in ('Darwin', 'MacPorts'))
898 class CygwinDistribution(CachedDistribution
):
899 """A Cygwin-based distribution."""
901 cache_leaf
= 'cygcheck-status.cache'
903 def generate_cache(self
):
906 zi_arch
= canonical_machine(arch
)
907 for line
in os
.popen("cygcheck -c -d"):
908 if line
== "Cygwin Package Information\r\n":
912 package
, version
= line
.split()
913 if package
== "Package" and version
== "Version":
915 clean_version
= try_cleanup_distro_version(version
)
917 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
919 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
921 self
._write
_cache
(cache
)
923 def get_package_info(self
, package
, factory
):
924 # Add installed versions...
925 versions
= self
.versions
.get(package
, [])
927 for version
, machine
in versions
:
928 impl
= factory('package:cygwin:%s:%s:%s' % (package
, version
, machine
))
929 impl
.version
= model
.parse_version(version
)
931 impl
.machine
= machine
933 def get_score(self
, disto_name
):
934 return int(disto_name
== 'Cygwin')
937 _host_distribution
= None
938 def get_host_distribution():
939 """Get a Distribution suitable for the host operating system.
940 Calling this twice will return the same object.
941 @rtype: L{Distribution}"""
942 global _host_distribution
943 if not _host_distribution
:
944 dpkg_db_status
= '/var/lib/dpkg/status'
945 rpm_db_packages
= '/var/lib/rpm/Packages'
946 _slack_db
= '/var/log/packages'
947 _arch_db
= '/var/lib/pacman'
948 _pkg_db
= '/var/db/pkg'
949 _macports_db
= '/opt/local/var/macports/registry/registry.db'
950 _cygwin_log
= '/var/log/setup.log'
952 if sys
.prefix
== "/sw":
953 dpkg_db_status
= os
.path
.join(sys
.prefix
, dpkg_db_status
)
954 rpm_db_packages
= os
.path
.join(sys
.prefix
, rpm_db_packages
)
957 _host_distribution
= WindowsDistribution()
958 elif os
.path
.isdir(_pkg_db
):
959 if sys
.platform
.startswith("linux"):
960 _host_distribution
= GentooDistribution(_pkg_db
)
961 elif sys
.platform
.startswith("freebsd"):
962 _host_distribution
= PortsDistribution(_pkg_db
)
963 elif os
.path
.isfile(_macports_db
) \
964 and sys
.prefix
.startswith("/opt/local"):
965 _host_distribution
= MacPortsDistribution(_macports_db
)
966 elif os
.path
.isfile(_cygwin_log
) and sys
.platform
== "cygwin":
967 _host_distribution
= CygwinDistribution(_cygwin_log
)
968 elif os
.access(dpkg_db_status
, os
.R_OK
) \
969 and os
.path
.getsize(dpkg_db_status
) > 0:
970 _host_distribution
= DebianDistribution(dpkg_db_status
)
971 elif os
.path
.isfile(rpm_db_packages
):
972 _host_distribution
= RPMDistribution(rpm_db_packages
)
973 elif os
.path
.isdir(_slack_db
):
974 _host_distribution
= SlackDistribution(_slack_db
)
975 elif os
.path
.isdir(_arch_db
):
976 _host_distribution
= ArchDistribution(_arch_db
)
977 elif sys
.platform
== "darwin":
978 _host_distribution
= DarwinDistribution()
980 _host_distribution
= Distribution()
982 return _host_distribution