Removed used of deprecated os.open2
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / distro.py
blob245de34faf5d468d518c53b373209547a345b277
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 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 CachedDistribution(Distribution):
46 """For distributions where querying the package database is slow (e.g. requires running
47 an external command), we cache the results.
48 @since: 0.39
49 """
51 def __init__(self, db_status_file):
52 """@param db_status_file: update the cache when the timestamp of this file changes"""
53 self._status_details = os.stat(db_status_file)
55 self.versions = {}
56 self.cache_dir = basedir.save_cache_path(namespaces.config_site,
57 namespaces.config_prog)
59 try:
60 self._load_cache()
61 except Exception, ex:
62 info("Failed to load distribution database cache (%s). Regenerating...", ex)
63 try:
64 self.generate_cache()
65 self._load_cache()
66 except Exception, ex:
67 warn("Failed to regenerate distribution database cache: %s", ex)
69 def _load_cache(self):
70 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
71 Throws an exception if the cache should be (re)created."""
72 stream = file(os.path.join(self.cache_dir, self.cache_leaf))
74 cache_version = None
75 for line in stream:
76 if line == '\n':
77 break
78 name, value = line.split(': ')
79 if name == 'mtime' and int(value) != int(self._status_details.st_mtime):
80 raise Exception("Modification time of package database file has changed")
81 if name == 'size' and int(value) != self._status_details.st_size:
82 raise Exception("Size of package database file has changed")
83 if name == 'version':
84 cache_version = int(value)
85 else:
86 raise Exception('Invalid cache format (bad header)')
88 if cache_version is None:
89 raise Exception('Old cache format')
91 versions = self.versions
92 for line in stream:
93 package, version, zi_arch = line[:-1].split('\t')
94 versions[package] = (version, intern(zi_arch))
96 def _write_cache(self, cache):
97 #cache.sort() # Might be useful later; currently we don't care
98 import tempfile
99 fd, tmpname = tempfile.mkstemp(prefix = 'zeroinstall-cache-tmp',
100 dir = self.cache_dir)
101 try:
102 stream = os.fdopen(fd, 'wb')
103 stream.write('version: 2\n')
104 stream.write('mtime: %d\n' % int(self._status_details.st_mtime))
105 stream.write('size: %d\n' % self._status_details.st_size)
106 stream.write('\n')
107 for line in cache:
108 stream.write(line + '\n')
109 stream.close()
111 os.rename(tmpname,
112 os.path.join(self.cache_dir,
113 self.cache_leaf))
114 except:
115 os.unlink(tmpname)
116 raise
118 class DebianDistribution(CachedDistribution):
119 """A dpkg-based distribution."""
121 cache_leaf = 'dpkg-status.cache'
123 def generate_cache(self):
124 cache = []
126 for line in os.popen("dpkg-query -W --showformat='${Package}\t${Version}\t${Architecture}\n'"):
127 package, version, debarch = line.split('\t', 2)
128 if ':' in version:
129 # Debian's 'epoch' system
130 version = version.split(':', 1)[1]
131 if debarch == 'amd64\n':
132 zi_arch = 'x86_64'
133 else:
134 zi_arch = '*'
135 clean_version = try_cleanup_distro_version(version)
136 if clean_version:
137 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
138 else:
139 warn("Can't parse distribution version '%s' for package '%s'", version, package)
141 self._write_cache(cache)
143 def get_package_info(self, package, factory):
144 try:
145 version, machine = self.versions[package]
146 except KeyError:
147 return
149 impl = factory('package:deb:%s:%s' % (package, version))
150 impl.version = model.parse_version(version)
151 if machine != '*':
152 impl.machine = machine
154 class RPMDistribution(CachedDistribution):
155 """An RPM-based distribution."""
157 cache_leaf = 'rpm-status.cache'
159 def generate_cache(self):
160 cache = []
162 for line in os.popen("rpm -qa --qf='%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n'"):
163 package, version, rpmarch = line.split('\t', 2)
164 if package == 'gpg-pubkey':
165 continue
166 if rpmarch == 'amd64\n':
167 zi_arch = 'x86_64'
168 elif rpmarch == 'noarch\n' or rpmarch == "(none)\n":
169 zi_arch = '*'
170 else:
171 zi_arch = rpmarch.strip()
172 clean_version = try_cleanup_distro_version(version)
173 if clean_version:
174 cache.append('%s\t%s\t%s' % (package, clean_version, zi_arch))
175 else:
176 warn("Can't parse distribution version '%s' for package '%s'", version, package)
178 self._write_cache(cache)
180 def get_package_info(self, package, factory):
181 try:
182 version, machine = self.versions[package]
183 except KeyError:
184 return
186 impl = factory('package:rpm:%s:%s' % (package, version))
187 impl.version = model.parse_version(version)
188 if machine != '*':
189 impl.machine = machine
191 _host_distribution = None
192 def get_host_distribution():
193 """Get a Distribution suitable for the host operating system.
194 Calling this twice will return the same object.
195 @rtype: L{Distribution}"""
196 global _host_distribution
197 if not _host_distribution:
198 _dpkg_db_status = '/var/lib/dpkg/status'
199 _rpm_db = '/var/lib/rpm/Packages'
201 if os.access(_dpkg_db_status, os.R_OK):
202 _host_distribution = DebianDistribution(_dpkg_db_status)
203 elif os.path.isfile(_rpm_db):
204 _host_distribution = RPMDistribution(_rpm_db)
205 else:
206 _host_distribution = Distribution()
208 return _host_distribution