Don't try to read dpkg database unless its directory is accessible to us.
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / distro.py
blob24e6523fdd772357c2436957f2e24bdf900ee52a
1 """
2 Integration with native distribution package managers.
3 @since: 0.28
4 """
6 # Copyright (C) 2007, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
9 import os, re
10 from logging import warn, info
11 from zeroinstall.injector import namespaces, basedir, model
13 dotted_ints = '[0-9]+(\.[0-9]+)*'
14 version_regexp = '(%s)(-(pre|rc|post|)%s)*' % (dotted_ints, dotted_ints)
16 def try_cleanup_distro_version(version):
17 """Try to turn a distribution version string into one readable by Zero Install.
18 We do this by stripping off anything we can't parse.
19 @return: the part we understood, or None if we couldn't parse anything
20 @rtype: str"""
21 match = re.match(version_regexp, version)
22 if match:
23 return match.group(0)
24 return None
26 class Distribution(object):
27 """Represents a distribution with which we can integrate.
28 Sub-classes should specialise this to integrate with the package managers of
29 particular distributions. This base class ignores the native package manager.
30 @since: 0.28
31 """
33 def get_package_info(self, package, factory):
34 """Get information about the given package.
35 Add zero or more implementations using the factory (typically at most two
36 will be added; the currently installed version and the latest available).
37 @param package: package name (e.g. "gimp")
38 @type package: str
39 @param factory: function for creating new DistributionImplementation objects from IDs
40 @type factory: str -> L{model.DistributionImplementation}
41 """
42 return
44 class DebianDistribution(Distribution):
45 def __init__(self, db_dir):
46 self.db_dir = db_dir
47 dpkg_status = db_dir + '/status'
48 self.status_details = os.stat(self.db_dir + '/status')
50 self.versions = {}
51 self.cache_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog)
53 try:
54 self.load_cache()
55 except Exception, ex:
56 info("Failed to load dpkg cache (%s). Regenerating...", ex)
57 try:
58 self.generate_cache()
59 self.load_cache()
60 except Exception, ex:
61 warn("Failed to regenerate dpkg cache: %s", ex)
63 def load_cache(self):
64 stream = file(self.cache_dir + '/dpkg-status.cache')
66 for line in stream:
67 if line == '\n':
68 break
69 name, value = line.split(': ')
70 if name == 'mtime' and int(value) != int(self.status_details.st_mtime):
71 raise Exception("Modification time of dpkg status file has changed")
72 if name == 'size' and int(value) != self.status_details.st_size:
73 raise Exception("Size of dpkg status file has changed")
74 else:
75 raise Exception('Invalid cache format (bad header)')
77 versions = self.versions
78 for line in stream:
79 package, version = line[:-1].split('\t')
80 versions[package] = version
82 def generate_cache(self):
83 cache = []
85 for line in os.popen("dpkg-query -W"):
86 package, version = line.split('\t', 1)
87 if ':' in version:
88 # Debian's 'epoch' system
89 version = version.split(':', 1)[1]
90 clean_version = try_cleanup_distro_version(version)
91 if clean_version:
92 cache.append('%s\t%s' % (package, clean_version))
93 else:
94 warn("Can't parse distribution version '%s' for package '%s'", version, package)
96 cache.sort() # Might be useful later; currently we don't care
98 import tempfile
99 fd, tmpname = tempfile.mkstemp(prefix = 'dpkg-cache-tmp', dir = self.cache_dir)
100 try:
101 stream = os.fdopen(fd, 'wb')
102 stream.write('mtime: %d\n' % int(self.status_details.st_mtime))
103 stream.write('size: %d\n' % self.status_details.st_size)
104 stream.write('\n')
105 for line in cache:
106 stream.write(line + '\n')
107 stream.close()
109 os.rename(tmpname, self.cache_dir + '/dpkg-status.cache')
110 except:
111 os.unlink(tmpname)
112 raise
114 def get_package_info(self, package, factory):
115 try:
116 version = self.versions[package]
117 except KeyError:
118 return
120 impl = factory('package:deb:%s:%s' % (package, version))
121 impl.version = model.parse_version(version)
123 class RPMDistribution(Distribution):
124 cache_leaf = 'rpm-status.cache'
126 def __init__(self, db_dir):
127 self.db_dir = db_dir
128 pkg_status = os.path.join(db_dir, 'Packages')
129 self.status_details = os.stat(pkg_status)
131 self.versions = {}
132 self.cache_dir=basedir.save_cache_path(namespaces.config_site,
133 namespaces.config_prog)
135 try:
136 self.load_cache()
137 except Exception, ex:
138 info("Failed to load cache (%s). Regenerating...",
140 try:
141 self.generate_cache()
142 self.load_cache()
143 except Exception, ex:
144 warn("Failed to regenerate cache: %s", ex)
146 def load_cache(self):
147 stream = file(os.path.join(self.cache_dir, self.cache_leaf))
149 for line in stream:
150 if line == '\n':
151 break
152 name, value = line.split(': ')
153 if name == 'mtime' and (int(value) !=
154 int(self.status_details.st_mtime)):
155 raise Exception("Modification time of rpm status file has changed")
156 if name == 'size' and (int(value) !=
157 self.status_details.st_size):
158 raise Exception("Size of rpm status file has changed")
159 else:
160 raise Exception('Invalid cache format (bad header)')
162 versions = self.versions
163 for line in stream:
164 package, version = line[:-1].split('\t')
165 versions[package] = version
167 def __parse_rpm_name(self, line):
168 """Some samples we have to cope with (from SuSE 10.2):
169 mp3blaster-3.2.0-0.pm0
170 fuse-2.5.2-2.pm.0
171 gpg-pubkey-1abd1afb-450ef738
172 a52dec-0.7.4-3.pm.1
173 glibc-html-2.5-25
174 gnome-backgrounds-2.16.1-14
175 gnome-icon-theme-2.16.0.1-12
176 opensuse-quickstart_en-10.2-9
177 susehelp_en-2006.06.20-25
178 yast2-schema-2.14.2-3"""
180 parts=line.strip().split('-')
181 if len(parts)==2:
182 return parts[0], try_cleanup_distro_version(parts[1])
184 elif len(parts)<2:
185 return None, None
187 package='-'.join(parts[:-2])
188 version=parts[-2]
189 mod=parts[-1]
191 return package, try_cleanup_distro_version(version+'-'+mod)
193 def generate_cache(self):
194 cache = []
196 for line in os.popen("rpm -qa"):
197 package, version = self.__parse_rpm_name(line)
198 if package and version:
199 cache.append('%s\t%s' % (package, version))
201 cache.sort() # Might be useful later; currently we don't care
203 import tempfile
204 fd, tmpname = tempfile.mkstemp(prefix = 'rpm-cache-tmp',
205 dir = self.cache_dir)
206 try:
207 stream = os.fdopen(fd, 'wb')
208 stream.write('mtime: %d\n' % int(self.status_details.st_mtime))
209 stream.write('size: %d\n' % self.status_details.st_size)
210 stream.write('\n')
211 for line in cache:
212 stream.write(line + '\n')
213 stream.close()
215 os.rename(tmpname,
216 os.path.join(self.cache_dir,
217 self.cache_leaf))
218 except:
219 os.unlink(tmpname)
220 raise
222 def get_package_info(self, package, factory):
223 try:
224 version = self.versions[package]
225 except KeyError:
226 return
228 impl = factory('package:rpm:%s:%s' % (package, version))
229 impl.version = model.parse_version(version)
231 _host_distribution = None
232 def get_host_distribution():
233 global _host_distribution
234 if not _host_distribution:
235 _dpkg_db_dir = '/var/lib/dpkg'
236 _rpm_db_dir = '/var/lib/rpm'
238 if os.access(_dpkg_db_dir, os.R_OK | os.X_OK):
239 _host_distribution = DebianDistribution(_dpkg_db_dir)
240 elif os.path.isdir(_rpm_db_dir):
241 _host_distribution = RPMDistribution(_rpm_db_dir)
242 else:
243 _host_distribution = Distribution()
245 return _host_distribution