Don't create .desktop filenames with spaces; xdg-desktop-menu gets confused
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / distro.py
blob840887a874aed408b4057baa22af13f14e6e0b69
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_dir):
49 self.db_dir = db_dir
50 dpkg_status = db_dir + '/status'
51 self.status_details = os.stat(self.db_dir + '/status')
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(self.cache_dir + '/dpkg-status.cache')
69 for line in stream:
70 if line == '\n':
71 break
72 name, value = line.split(': ')
73 if name == 'mtime' and int(value) != int(self.status_details.st_mtime):
74 raise Exception("Modification time of dpkg status file has changed")
75 if name == 'size' and int(value) != self.status_details.st_size:
76 raise Exception("Size of dpkg status file has changed")
77 else:
78 raise Exception('Invalid cache format (bad header)')
80 versions = self.versions
81 for line in stream:
82 package, version = line[:-1].split('\t')
83 versions[package] = version
85 def generate_cache(self):
86 cache = []
88 for line in os.popen("dpkg-query -W"):
89 package, version = line.split('\t', 1)
90 if ':' in version:
91 # Debian's 'epoch' system
92 version = version.split(':', 1)[1]
93 clean_version = try_cleanup_distro_version(version)
94 if clean_version:
95 cache.append('%s\t%s' % (package, clean_version))
96 else:
97 warn("Can't parse distribution version '%s' for package '%s'", version, package)
99 cache.sort() # Might be useful later; currently we don't care
101 import tempfile
102 fd, tmpname = tempfile.mkstemp(prefix = 'dpkg-cache-tmp', dir = self.cache_dir)
103 try:
104 stream = os.fdopen(fd, 'wb')
105 stream.write('mtime: %d\n' % int(self.status_details.st_mtime))
106 stream.write('size: %d\n' % self.status_details.st_size)
107 stream.write('\n')
108 for line in cache:
109 stream.write(line + '\n')
110 stream.close()
112 os.rename(tmpname, self.cache_dir + '/dpkg-status.cache')
113 except:
114 os.unlink(tmpname)
115 raise
117 def get_package_info(self, package, factory):
118 try:
119 version = self.versions[package]
120 except KeyError:
121 return
123 impl = factory('package:deb:%s:%s' % (package, version))
124 impl.version = model.parse_version(version)
126 class RPMDistribution(Distribution):
127 """An RPM-based distribution."""
129 cache_leaf = 'rpm-status.cache'
131 def __init__(self, db_dir):
132 self.db_dir = db_dir
133 pkg_status = os.path.join(db_dir, 'Packages')
134 self.status_details = os.stat(pkg_status)
136 self.versions = {}
137 self.cache_dir=basedir.save_cache_path(namespaces.config_site,
138 namespaces.config_prog)
140 try:
141 self.load_cache()
142 except Exception, ex:
143 info("Failed to load cache (%s). Regenerating...",
145 try:
146 self.generate_cache()
147 self.load_cache()
148 except Exception, ex:
149 warn("Failed to regenerate cache: %s", ex)
151 def load_cache(self):
152 stream = file(os.path.join(self.cache_dir, self.cache_leaf))
154 for line in stream:
155 if line == '\n':
156 break
157 name, value = line.split(': ')
158 if name == 'mtime' and (int(value) !=
159 int(self.status_details.st_mtime)):
160 raise Exception("Modification time of rpm status file has changed")
161 if name == 'size' and (int(value) !=
162 self.status_details.st_size):
163 raise Exception("Size of rpm status file has changed")
164 else:
165 raise Exception('Invalid cache format (bad header)')
167 versions = self.versions
168 for line in stream:
169 package, version = line[:-1].split('\t')
170 versions[package] = version
172 def __parse_rpm_name(self, line):
173 """Some samples we have to cope with (from SuSE 10.2):
174 mp3blaster-3.2.0-0.pm0
175 fuse-2.5.2-2.pm.0
176 gpg-pubkey-1abd1afb-450ef738
177 a52dec-0.7.4-3.pm.1
178 glibc-html-2.5-25
179 gnome-backgrounds-2.16.1-14
180 gnome-icon-theme-2.16.0.1-12
181 opensuse-quickstart_en-10.2-9
182 susehelp_en-2006.06.20-25
183 yast2-schema-2.14.2-3"""
185 parts=line.strip().split('-')
186 if len(parts)==2:
187 return parts[0], try_cleanup_distro_version(parts[1])
189 elif len(parts)<2:
190 return None, None
192 package='-'.join(parts[:-2])
193 version=parts[-2]
194 mod=parts[-1]
196 return package, try_cleanup_distro_version(version+'-'+mod)
198 def generate_cache(self):
199 cache = []
201 for line in os.popen("rpm -qa"):
202 package, version = self.__parse_rpm_name(line)
203 if package and version:
204 cache.append('%s\t%s' % (package, version))
206 cache.sort() # Might be useful later; currently we don't care
208 import tempfile
209 fd, tmpname = tempfile.mkstemp(prefix = 'rpm-cache-tmp',
210 dir = self.cache_dir)
211 try:
212 stream = os.fdopen(fd, 'wb')
213 stream.write('mtime: %d\n' % int(self.status_details.st_mtime))
214 stream.write('size: %d\n' % self.status_details.st_size)
215 stream.write('\n')
216 for line in cache:
217 stream.write(line + '\n')
218 stream.close()
220 os.rename(tmpname,
221 os.path.join(self.cache_dir,
222 self.cache_leaf))
223 except:
224 os.unlink(tmpname)
225 raise
227 def get_package_info(self, package, factory):
228 try:
229 version = self.versions[package]
230 except KeyError:
231 return
233 impl = factory('package:rpm:%s:%s' % (package, version))
234 impl.version = model.parse_version(version)
236 _host_distribution = None
237 def get_host_distribution():
238 """Get a Distribution suitable for the host operating system.
239 Calling this twice will return the same object.
240 @rtype: L{Distribution}"""
241 global _host_distribution
242 if not _host_distribution:
243 _dpkg_db_dir = '/var/lib/dpkg'
244 _rpm_db_dir = '/var/lib/rpm'
246 if os.access(_dpkg_db_dir, os.R_OK | os.X_OK):
247 _host_distribution = DebianDistribution(_dpkg_db_dir)
248 elif os.path.isdir(_rpm_db_dir):
249 _host_distribution = RPMDistribution(_rpm_db_dir)
250 else:
251 _host_distribution = Distribution()
253 return _host_distribution