Use architecture from RPM packages
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / distro.py
blobcb77178171de24d72e16749c1b2086a826a3e1c1
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 cache_leaf = 'dpkg-status.cache'
50 def __init__(self, db_status_file):
51 self.status_details = os.stat(db_status_file)
53 self.versions = {}
54 self.cache_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog)
56 try:
57 self.load_cache()
58 except Exception, ex:
59 info("Failed to load dpkg cache (%s). Regenerating...", ex)
60 try:
61 self.generate_cache()
62 self.load_cache()
63 except Exception, ex:
64 warn("Failed to regenerate dpkg cache: %s", ex)
66 def load_cache(self):
67 stream = file(os.path.join(self.cache_dir, self.cache_leaf))
69 cache_version = None
70 for line in stream:
71 if line == '\n':
72 break
73 name, value = line.split(': ')
74 if name == 'mtime' and int(value) != int(self.status_details.st_mtime):
75 raise Exception("Modification time of dpkg status file has changed")
76 if name == 'size' and int(value) != self.status_details.st_size:
77 raise Exception("Size of dpkg status file has changed")
78 if name == 'version':
79 cache_version = int(value)
80 else:
81 raise Exception('Invalid cache format (bad header)')
83 if cache_version is None:
84 raise Exception('Old cache format')
86 versions = self.versions
87 for line in stream:
88 package, version, zi_arch = line[:-1].split('\t')
89 versions[package] = (version, intern(zi_arch))
91 def generate_cache(self):
92 cache = []
94 for line in os.popen("dpkg-query -W --showformat='${Package}\t${Version}\t${Architecture}\n'"):
95 package, version, debarch = line.split('\t', 2)
96 if ':' in version:
97 # Debian's 'epoch' system
98 version = version.split(':', 1)[1]
99 if debarch == 'amd64\n':
100 zi_arch = 'x86_64'
101 else:
102 zi_arch = '*'
103 clean_version = try_cleanup_distro_version(version)
104 if clean_version:
105 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
106 else:
107 warn("Can't parse distribution version '%s' for package '%s'", version, package)
109 cache.sort() # Might be useful later; currently we don't care
111 import tempfile
112 fd, tmpname = tempfile.mkstemp(prefix = 'dpkg-cache-tmp', dir = self.cache_dir)
113 try:
114 stream = os.fdopen(fd, 'wb')
115 stream.write('version: 2\n')
116 stream.write('mtime: %d\n' % int(self.status_details.st_mtime))
117 stream.write('size: %d\n' % self.status_details.st_size)
118 stream.write('\n')
119 for line in cache:
120 stream.write(line + '\n')
121 stream.close()
123 os.rename(tmpname,
124 os.path.join(self.cache_dir,
125 self.cache_leaf))
126 except:
127 os.unlink(tmpname)
128 raise
130 def get_package_info(self, package, factory):
131 try:
132 version, machine = self.versions[package]
133 except KeyError:
134 return
136 impl = factory('package:deb:%s:%s' % (package, version))
137 impl.version = model.parse_version(version)
138 if machine != '*':
139 impl.machine = machine
141 class RPMDistribution(Distribution):
142 """An RPM-based distribution."""
144 cache_leaf = 'rpm-status.cache'
146 def __init__(self, packages_file):
147 self.status_details = os.stat(packages_file)
149 self.versions = {}
150 self.cache_dir=basedir.save_cache_path(namespaces.config_site,
151 namespaces.config_prog)
153 try:
154 self.load_cache()
155 except Exception, ex:
156 info("Failed to load cache (%s). Regenerating...",
158 try:
159 self.generate_cache()
160 self.load_cache()
161 except Exception, ex:
162 warn("Failed to regenerate cache: %s", ex)
164 def load_cache(self):
165 stream = file(os.path.join(self.cache_dir, self.cache_leaf))
167 cache_version = None
168 for line in stream:
169 if line == '\n':
170 break
171 name, value = line.split(': ')
172 if name == 'mtime' and (int(value) !=
173 int(self.status_details.st_mtime)):
174 raise Exception("Modification time of rpm status file has changed")
175 if name == 'size' and (int(value) !=
176 self.status_details.st_size):
177 raise Exception("Size of rpm status file has changed")
178 if name == 'version':
179 cache_version = int(value)
180 else:
181 raise Exception('Invalid cache format (bad header)')
183 if cache_version is None:
184 raise Exception('Old cache format')
186 versions = self.versions
187 for line in stream:
188 package, version, zi_arch = line[:-1].split('\t')
189 versions[package] = (version, intern(zi_arch))
191 def generate_cache(self):
192 cache = []
194 for line in os.popen("rpm -qa --qf='%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n'"):
195 package, version, rpmarch = line.split('\t', 2)
196 if package == 'gpg-pubkey':
197 continue
198 if rpmarch == 'amd64\n':
199 zi_arch = 'x86_64'
200 elif rpmarch == 'noarch\n' or rpmarch == "(none)\n":
201 zi_arch = '*'
202 else:
203 zi_arch = rpmarch.strip()
204 clean_version = try_cleanup_distro_version(version)
205 if clean_version:
206 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
207 else:
208 warn("Can't parse distribution version '%s' for package '%s'", version, package)
210 cache.sort() # Might be useful later; currently we don't care
212 import tempfile
213 fd, tmpname = tempfile.mkstemp(prefix = 'rpm-cache-tmp',
214 dir = self.cache_dir)
215 try:
216 stream = os.fdopen(fd, 'wb')
217 stream.write('version: 2\n')
218 stream.write('mtime: %d\n' % int(self.status_details.st_mtime))
219 stream.write('size: %d\n' % self.status_details.st_size)
220 stream.write('\n')
221 for line in cache:
222 stream.write(line + '\n')
223 stream.close()
225 os.rename(tmpname,
226 os.path.join(self.cache_dir,
227 self.cache_leaf))
228 except:
229 os.unlink(tmpname)
230 raise
232 def get_package_info(self, package, factory):
233 try:
234 version, machine = self.versions[package]
235 except KeyError:
236 return
238 impl = factory('package:rpm:%s:%s' % (package, version))
239 impl.version = model.parse_version(version)
240 if machine != '*':
241 impl.machine = machine
243 _host_distribution = None
244 def get_host_distribution():
245 """Get a Distribution suitable for the host operating system.
246 Calling this twice will return the same object.
247 @rtype: L{Distribution}"""
248 global _host_distribution
249 if not _host_distribution:
250 _dpkg_db_status = '/var/lib/dpkg/status'
251 _rpm_db = '/var/lib/rpm/Packages'
253 if os.access(_dpkg_db_status, os.R_OK):
254 _host_distribution = DebianDistribution(_dpkg_db_status)
255 elif os.path.isfile(_rpm_db):
256 _host_distribution = RPMDistribution(_rpm_db)
257 else:
258 _host_distribution = Distribution()
260 return _host_distribution