Don't interpret wildchars in Gentoo package names
[zeroinstall/solver.git] / zeroinstall / injector / distro.py
blob54ebdeabca17a9f70f264cc4afba9648bb6bba7b
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, glob
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]+)*'
17 # This matches a version number that would be a valid Zero Install version without modification
18 _zeroinstall_regexp = '(?:%s)(?:-(?:pre|rc|post|)(?:%s))*' % (_dotted_ints, _dotted_ints)
20 # This matches the interesting bits of distribution version numbers
21 _version_regexp = '(%s)(-r%s)?' % (_zeroinstall_regexp, _dotted_ints)
23 def try_cleanup_distro_version(version):
24 """Try to turn a distribution version string into one readable by Zero Install.
25 We do this by stripping off anything we can't parse.
26 @return: the part we understood, or None if we couldn't parse anything
27 @rtype: str"""
28 match = re.match(_version_regexp, version)
29 if match:
30 version, revision = match.groups()
31 if revision is None:
32 return version
33 else:
34 return '%s-%s' % (version, revision[2:])
35 return None
37 class Distribution(object):
38 """Represents a distribution with which we can integrate.
39 Sub-classes should specialise this to integrate with the package managers of
40 particular distributions. This base class ignores the native package manager.
41 @since: 0.28
42 """
44 def get_package_info(self, package, factory):
45 """Get information about the given package.
46 Add zero or more implementations using the factory (typically at most two
47 will be added; the currently installed version and the latest available).
48 @param package: package name (e.g. "gimp")
49 @type package: str
50 @param factory: function for creating new DistributionImplementation objects from IDs
51 @type factory: str -> L{model.DistributionImplementation}
52 """
53 return
55 class CachedDistribution(Distribution):
56 """For distributions where querying the package database is slow (e.g. requires running
57 an external command), we cache the results.
58 @since: 0.39
59 """
61 def __init__(self, db_status_file):
62 """@param db_status_file: update the cache when the timestamp of this file changes"""
63 self._status_details = os.stat(db_status_file)
65 self.versions = {}
66 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
67 namespaces.config_prog)
69 try:
70 self._load_cache()
71 except Exception, ex:
72 info(_("Failed to load distribution database cache (%s). Regenerating..."), ex)
73 try:
74 self.generate_cache()
75 self._load_cache()
76 except Exception, ex:
77 warn(_("Failed to regenerate distribution database cache: %s"), ex)
79 def _load_cache(self):
80 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
81 Throws an exception if the cache should be (re)created."""
82 stream = file(os.path.join(self.cache_dir, self.cache_leaf))
84 cache_version = None
85 for line in stream:
86 if line == '\n':
87 break
88 name, value = line.split(': ')
89 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
90 raise Exception(_("Modification time of package database file has changed"))
91 if name == 'size' and int(value) != self._status_details.st_size:
92 raise Exception(_("Size of package database file has changed"))
93 if name == 'version':
94 cache_version = int(value)
95 else:
96 raise Exception(_('Invalid cache format (bad header)'))
98 if cache_version is None:
99 raise Exception(_('Old cache format'))
101 versions = self.versions
102 for line in stream:
103 package, version, zi_arch = line[:-1].split('\t')
104 versionarch = (version, intern(zi_arch))
105 if package not in versions:
106 versions[package] = [versionarch]
107 else:
108 versions[package].append(versionarch)
110 def _write_cache(self, cache):
111 #cache.sort() # Might be useful later; currently we don't care
112 import tempfile
113 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
114 dir = self.cache_dir)
115 try:
116 stream = os.fdopen(fd, 'wb')
117 stream.write('version: 2\n')
118 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
119 stream.write('size: %d\n' % self._status_details.st_size)
120 stream.write('\n')
121 for line in cache:
122 stream.write(line + '\n')
123 stream.close()
125 os.rename(tmpname,
126 os.path.join(self.cache_dir,
127 self.cache_leaf))
128 except:
129 os.unlink(tmpname)
130 raise
132 class DebianDistribution(CachedDistribution):
133 """A dpkg-based distribution."""
135 cache_leaf = 'dpkg-status.cache'
137 def generate_cache(self):
138 cache = []
140 for line in os.popen("dpkg-query -W --showformat='${Package}\t${Version}\t${Architecture}\n'"):
141 package, version, debarch = line.split('\t', 2)
142 if ':' in version:
143 # Debian's 'epoch' system
144 version = version.split(':', 1)[1]
145 if debarch == 'amd64\n':
146 zi_arch = 'x86_64'
147 else:
148 zi_arch = '*'
149 clean_version = try_cleanup_distro_version(version)
150 if clean_version:
151 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
152 else:
153 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
155 self._write_cache(cache)
157 def get_package_info(self, package, factory):
158 try:
159 version, machine = self.versions[package][0]
160 except KeyError:
161 return
163 impl = factory('package:deb:%s:%s' % (package, version))
164 impl.version = model.parse_version(version)
165 if machine != '*':
166 impl.machine = machine
168 class RPMDistribution(CachedDistribution):
169 """An RPM-based distribution."""
171 cache_leaf = 'rpm-status.cache'
173 def generate_cache(self):
174 cache = []
176 for line in os.popen("rpm -qa --qf='%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n'"):
177 package, version, rpmarch = line.split('\t', 2)
178 if package == 'gpg-pubkey':
179 continue
180 if rpmarch == 'amd64\n':
181 zi_arch = 'x86_64'
182 elif rpmarch == 'noarch\n' or rpmarch == "(none)\n":
183 zi_arch = '*'
184 else:
185 zi_arch = rpmarch.strip()
186 clean_version = try_cleanup_distro_version(version)
187 if clean_version:
188 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
189 else:
190 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package})
192 self._write_cache(cache)
194 def get_package_info(self, package, factory):
195 try:
196 versions = self.versions[package]
197 except KeyError:
198 return
200 for version, machine in versions:
201 impl = factory('package:rpm:%s:%s:%s' % (package, version, machine))
202 impl.version = model.parse_version(version)
203 if machine != '*':
204 impl.machine = machine
206 class GentooDistribution(Distribution):
208 def __init__(self, pkgdir):
209 self._pkgdir = pkgdir
211 def get_package_info(self, package, factory):
212 _version_start_reqexp = '-[0-9]'
214 if package.count('/') != 1: return
216 category, leafname = package.split('/')
217 category_dir = os.path.join(self._pkgdir, category)
218 match_prefix = leafname + '-'
220 for filename in os.listdir(category_dir):
221 if filename.startswith(match_prefix) and filename[len(match_prefix)].isdigit():
222 name = file(os.path.join(category_dir, filename, 'PF')).readline().strip()
224 match = re.search(_version_start_reqexp, name)
225 if match is None:
226 warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name})
227 continue
228 else:
229 version = try_cleanup_distro_version(name[match.start() + 1:])
231 machine = file(os.path.join(category_dir, filename, 'CHOST')).readline().split('-')[0]
233 impl = factory('package:gentoo:%s:%s:%s' % \
234 (package, version, machine))
235 impl.version = model.parse_version(version)
237 _host_distribution = None
238 def get_host_distribution():
239 """Get a Distribution suitable for the host operating system.
240 Calling this twice will return the same object.
241 @rtype: L{Distribution}"""
242 global _host_distribution
243 if not _host_distribution:
244 _dpkg_db_status = '/var/lib/dpkg/status'
245 _rpm_db = '/var/lib/rpm/Packages'
246 _gentoo_db = '/var/db/pkg'
248 if os.path.isdir(_gentoo_db):
249 _host_distribution = GentooDistribution(_gentoo_db)
250 elif os.access(_dpkg_db_status, os.R_OK):
251 _host_distribution = DebianDistribution(_dpkg_db_status)
252 elif os.path.isfile(_rpm_db):
253 _host_distribution = RPMDistribution(_rpm_db)
254 else:
255 _host_distribution = Distribution()
257 return _host_distribution