Allow multiple versions of each distribution package
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / distro.py
blobe7b96c80eef91e36bde8695d7bf4cf418a1ce0ec
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, re
11 from logging import warn, info
12 from zeroinstall.injector import namespaces, model
13 from zeroinstall.support import basedir
15 _dotted_ints = '[0-9]+(\.[0-9]+)*'
16 _version_regexp = '(%s)(-(pre|rc|post|)%s)*' % (_dotted_ints, _dotted_ints)
18 def try_cleanup_distro_version(version):
19 """Try to turn a distribution version string into one readable by Zero Install.
20 We do this by stripping off anything we can't parse.
21 @return: the part we understood, or None if we couldn't parse anything
22 @rtype: str"""
23 match = re.match(_version_regexp, version)
24 if match:
25 return match.group(0)
26 return None
28 class Distribution(object):
29 """Represents a distribution with which we can integrate.
30 Sub-classes should specialise this to integrate with the package managers of
31 particular distributions. This base class ignores the native package manager.
32 @since: 0.28
33 """
35 def get_package_info(self, package, factory):
36 """Get information about the given package.
37 Add zero or more implementations using the factory (typically at most two
38 will be added; the currently installed version and the latest available).
39 @param package: package name (e.g. "gimp")
40 @type package: str
41 @param factory: function for creating new DistributionImplementation objects from IDs
42 @type factory: str -> L{model.DistributionImplementation}
43 """
44 return
46 class CachedDistribution(Distribution):
47 """For distributions where querying the package database is slow (e.g. requires running
48 an external command), we cache the results.
49 @since: 0.39
50 """
52 def __init__(self, db_status_file):
53 """@param db_status_file: update the cache when the timestamp of this file changes"""
54 self._status_details = os.stat(db_status_file)
56 self.versions = {}
57 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
58 namespaces.config_prog)
60 try:
61 self._load_cache()
62 except Exception, ex:
63 info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
64 try:
65 self.generate_cache()
66 self._load_cache()
67 except Exception, ex:
68 warn(_("Failed to regenerate distribution database cache: %s"), ex)
70 def _load_cache(self):
71 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
72 Throws an exception if the cache should be (re)created."""
73 stream = file(os.path.join(self.cache_dir, self.cache_leaf))
75 cache_version = None
76 for line in stream:
77 if line == '\n':
78 break
79 name, value = line.split(': ')
80 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
81 raise Exception(_("Modification time of package database file has changed"))
82 if name == 'size' and int(value) != self._status_details.st_size:
83 raise Exception(_("Size of package database file has changed"))
84 if name == 'version':
85 cache_version = int(value)
86 else:
87 raise Exception(_('Invalid cache format (bad header)'))
89 if cache_version is None:
90 raise Exception(_('Old cache format'))
92 versions = self.versions
93 for line in stream:
94 package, version, zi_arch = line[:-1].split('\t')
95 versionarch = (version, intern(zi_arch))
96 if package not in versions:
97 versions[package] = [versionarch]
98 else:
99 versions[package].append(versionarch)
101 def _write_cache(self, cache):
102 #cache.sort() # Might be useful later; currently we don't care
103 import tempfile
104 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
105 dir = self.cache_dir)
106 try:
107 stream = os.fdopen(fd, 'wb')
108 stream.write('version: 2\n')
109 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
110 stream.write('size: %d\n' % self._status_details.st_size)
111 stream.write('\n')
112 for line in cache:
113 stream.write(line + '\n')
114 stream.close()
116 os.rename(tmpname,
117 os.path.join(self.cache_dir,
118 self.cache_leaf))
119 except:
120 os.unlink(tmpname)
121 raise
123 class DebianDistribution(CachedDistribution):
124 """A dpkg-based distribution."""
126 cache_leaf = 'dpkg-status.cache'
128 def generate_cache(self):
129 cache = []
131 for line in os.popen("dpkg-query -W --showformat='${Package}\t${Version}\t${Architecture}\n'"):
132 package, version, debarch = line.split('\t', 2)
133 if ':' in version:
134 # Debian's 'epoch' system
135 version = version.split(':', 1)[1]
136 if debarch == 'amd64\n':
137 zi_arch = 'x86_64'
138 else:
139 zi_arch = '*'
140 clean_version = try_cleanup_distro_version(version)
141 if clean_version:
142 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
143 else:
144 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
146 self._write_cache(cache)
148 def get_package_info(self, package, factory):
149 try:
150 version, machine = self.versions[package][0]
151 except KeyError:
152 return
154 impl = factory('package:deb:%s:%s' % (package, version))
155 impl.version = model.parse_version(version)
156 if machine != '*':
157 impl.machine = machine
159 class RPMDistribution(CachedDistribution):
160 """An RPM-based distribution."""
162 cache_leaf = 'rpm-status.cache'
164 def generate_cache(self):
165 cache = []
167 for line in os.popen("rpm -qa --qf='%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n'"):
168 package, version, rpmarch = line.split('\t', 2)
169 if package == 'gpg-pubkey':
170 continue
171 if rpmarch == 'amd64\n':
172 zi_arch = 'x86_64'
173 elif rpmarch == 'noarch\n' or rpmarch == "(none)\n":
174 zi_arch = '*'
175 else:
176 zi_arch = rpmarch.strip()
177 clean_version = try_cleanup_distro_version(version)
178 if clean_version:
179 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
180 else:
181 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
183 self._write_cache(cache)
185 def get_package_info(self, package, factory):
186 try:
187 versions = self.versions[package]
188 except KeyError:
189 return
191 for version, machine in versions:
192 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
193 impl.version = model.parse_version(version)
194 if machine != '*':
195 impl.machine = machine
197 _host_distribution = None
198 def get_host_distribution():
199 """Get a Distribution suitable for the host operating system.
200 Calling this twice will return the same object.
201 @rtype: L{Distribution}"""
202 global _host_distribution
203 if not _host_distribution:
204 _dpkg_db_status = '/var/lib/dpkg/status'
205 _rpm_db = '/var/lib/rpm/Packages'
207 if os.access(_dpkg_db_status, os.R_OK):
208 _host_distribution = DebianDistribution(_dpkg_db_status)
209 elif os.path.isfile(_rpm_db):
210 _host_distribution = RPMDistribution(_rpm_db)
211 else:
212 _host_distribution = Distribution()
214 return _host_distribution