Better check for Debian-style distribution
[zeroinstall.git] / zeroinstall / injector / distro.py
blob2852ad6f34e6885d1f22d1fb0b4e1fa4e17dc0dd
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, model
12 from zeroinstall.support import basedir
14 _dotted_ints = '[0-9]+(\.[0-9]+)*'
15 _version_regexp = '(%s)(-(pre|rc|post|)%s)*' % (_dotted_ints, _dotted_ints)
17 def try_cleanup_distro_version(version):
18 """Try to turn a distribution version string into one readable by Zero Install.
19 We do this by stripping off anything we can't parse.
20 @return: the part we understood, or None if we couldn't parse anything
21 @rtype: str"""
22 match = re.match(_version_regexp, version)
23 if match:
24 return match.group(0)
25 return None
27 class Distribution(object):
28 """Represents a distribution with which we can integrate.
29 Sub-classes should specialise this to integrate with the package managers of
30 particular distributions. This base class ignores the native package manager.
31 @since: 0.28
32 """
34 def get_package_info(self, package, factory):
35 """Get information about the given package.
36 Add zero or more implementations using the factory (typically at most two
37 will be added; the currently installed version and the latest available).
38 @param package: package name (e.g. "gimp")
39 @type package: str
40 @param factory: function for creating new DistributionImplementation objects from IDs
41 @type factory: str -> L{model.DistributionImplementation}
42 """
43 return
45 class DebianDistribution(Distribution):
46 """An dpkg-based distribution."""
48 def __init__(self, db_status_file):
49 self.status_details = os.stat(db_status_file)
51 self.versions = {}
52 self.cache_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog)
54 try:
55 self.load_cache()
56 except Exception, ex:
57 info("Failed to load dpkg cache (%s). Regenerating...", ex)
58 try:
59 self.generate_cache()
60 self.load_cache()
61 except Exception, ex:
62 warn("Failed to regenerate dpkg cache: %s", ex)
64 def load_cache(self):
65 stream = file(self.cache_dir + '/dpkg-status.cache')
67 cache_version = None
68 for line in stream:
69 if line == '\n':
70 break
71 name, value = line.split(': ')
72 if name == 'mtime' and int(value) != int(self.status_details.st_mtime):
73 raise Exception("Modification time of dpkg status file has changed")
74 if name == 'size' and int(value) != self.status_details.st_size:
75 raise Exception("Size of dpkg status file has changed")
76 if name == 'version':
77 cache_version = int(value)
78 else:
79 raise Exception('Invalid cache format (bad header)')
81 if cache_version is None:
82 raise Exception('Old cache format')
84 versions = self.versions
85 for line in stream:
86 package, version, zi_arch = line[:-1].split('\t')
87 versions[package] = (version, intern(zi_arch))
89 def generate_cache(self):
90 cache = []
92 for line in os.popen("dpkg-query -W -f='${Package}\t${Version}\t${Architecture}\n'"):
93 package, version, debarch = line.split('\t', 2)
94 if ':' in version:
95 # Debian's 'epoch' system
96 version = version.split(':', 1)[1]
97 if debarch == 'amd64\n':
98 zi_arch = 'x86_64'
99 else:
100 zi_arch = '*'
101 clean_version = try_cleanup_distro_version(version)
102 if clean_version:
103 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
104 else:
105 warn("Can't parse distribution version '%s' for package '%s'", version, package)
107 cache.sort() # Might be useful later; currently we don't care
109 import tempfile
110 fd, tmpname = tempfile.mkstemp(prefix = 'dpkg-cache-tmp', dir = self.cache_dir)
111 try:
112 stream = os.fdopen(fd, 'wb')
113 stream.write('version: 2\n')
114 stream.write('mtime: %d\n' % int(self.status_details.st_mtime))
115 stream.write('size: %d\n' % self.status_details.st_size)
116 stream.write('\n')
117 for line in cache:
118 stream.write(line + '\n')
119 stream.close()
121 os.rename(tmpname, self.cache_dir + '/dpkg-status.cache')
122 except:
123 os.unlink(tmpname)
124 raise
126 def get_package_info(self, package, factory):
127 try:
128 version, machine = self.versions[package]
129 except KeyError:
130 return
132 impl = factory('package:deb:%s:%s' % (package, version))
133 impl.version = model.parse_version(version)
134 if machine != '*':
135 impl.machine = machine
137 class RPMDistribution(Distribution):
138 """An RPM-based distribution."""
140 cache_leaf = 'rpm-status.cache'
142 def __init__(self, packages_file):
143 self.status_details = os.stat(packages_file)
145 self.versions = {}
146 self.cache_dir=basedir.save_cache_path(namespaces.config_site,
147 namespaces.config_prog)
149 try:
150 self.load_cache()
151 except Exception, ex:
152 info("Failed to load cache (%s). Regenerating...",
154 try:
155 self.generate_cache()
156 self.load_cache()
157 except Exception, ex:
158 warn("Failed to regenerate cache: %s", ex)
160 def load_cache(self):
161 stream = file(os.path.join(self.cache_dir, self.cache_leaf))
163 for line in stream:
164 if line == '\n':
165 break
166 name, value = line.split(': ')
167 if name == 'mtime' and (int(value) !=
168 int(self.status_details.st_mtime)):
169 raise Exception("Modification time of rpm status file has changed")
170 if name == 'size' and (int(value) !=
171 self.status_details.st_size):
172 raise Exception("Size of rpm status file has changed")
173 else:
174 raise Exception('Invalid cache format (bad header)')
176 versions = self.versions
177 for line in stream:
178 package, version = line[:-1].split('\t')
179 versions[package] = version
181 def __parse_rpm_name(self, line):
182 """Some samples we have to cope with (from SuSE 10.2):
183 mp3blaster-3.2.0-0.pm0
184 fuse-2.5.2-2.pm.0
185 gpg-pubkey-1abd1afb-450ef738
186 a52dec-0.7.4-3.pm.1
187 glibc-html-2.5-25
188 gnome-backgrounds-2.16.1-14
189 gnome-icon-theme-2.16.0.1-12
190 opensuse-quickstart_en-10.2-9
191 susehelp_en-2006.06.20-25
192 yast2-schema-2.14.2-3"""
194 parts=line.strip().split('-')
195 if len(parts)==2:
196 return parts[0], try_cleanup_distro_version(parts[1])
198 elif len(parts)<2:
199 return None, None
201 package='-'.join(parts[:-2])
202 version=parts[-2]
203 mod=parts[-1]
205 return package, try_cleanup_distro_version(version+'-'+mod)
207 def generate_cache(self):
208 cache = []
210 for line in os.popen("rpm -qa"):
211 package, version = self.__parse_rpm_name(line)
212 if package and version:
213 cache.append('%s\t%s' % (package, version))
215 cache.sort() # Might be useful later; currently we don't care
217 import tempfile
218 fd, tmpname = tempfile.mkstemp(prefix = 'rpm-cache-tmp',
219 dir = self.cache_dir)
220 try:
221 stream = os.fdopen(fd, 'wb')
222 stream.write('mtime: %d\n' % int(self.status_details.st_mtime))
223 stream.write('size: %d\n' % self.status_details.st_size)
224 stream.write('\n')
225 for line in cache:
226 stream.write(line + '\n')
227 stream.close()
229 os.rename(tmpname,
230 os.path.join(self.cache_dir,
231 self.cache_leaf))
232 except:
233 os.unlink(tmpname)
234 raise
236 def get_package_info(self, package, factory):
237 try:
238 version = self.versions[package]
239 except KeyError:
240 return
242 impl = factory('package:rpm:%s:%s' % (package, version))
243 impl.version = model.parse_version(version)
245 _host_distribution = None
246 def get_host_distribution():
247 """Get a Distribution suitable for the host operating system.
248 Calling this twice will return the same object.
249 @rtype: L{Distribution}"""
250 global _host_distribution
251 if not _host_distribution:
252 _dpkg_db_status = '/var/lib/dpkg/status'
253 _rpm_db = '/var/lib/rpm/Packages'
255 if os.access(_dpkg_db_status, os.R_OK):
256 _host_distribution = DebianDistribution(_dpkg_db_status)
257 elif os.path.isfile(_rpm_db):
258 _host_distribution = RPMDistribution(_rpm_db)
259 else:
260 _host_distribution = Distribution()
262 return _host_distribution