2 Integration with native distribution package managers.
6 # Copyright (C) 2007, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
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
22 match
= re
.match(_version_regexp
, version
)
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.
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")
40 @param factory: function for creating new DistributionImplementation objects from IDs
41 @type factory: str -> L{model.DistributionImplementation}
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
)
54 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
)
59 info("Failed to load dpkg cache (%s). Regenerating...", ex
)
64 warn("Failed to regenerate dpkg cache: %s", ex
)
67 stream
= file(os
.path
.join(self
.cache_dir
, self
.cache_leaf
))
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")
79 cache_version
= int(value
)
81 raise Exception('Invalid cache format (bad header)')
83 if cache_version
is None:
84 raise Exception('Old cache format')
86 versions
= self
.versions
88 package
, version
, zi_arch
= line
[:-1].split('\t')
89 versions
[package
] = (version
, intern(zi_arch
))
91 def generate_cache(self
):
94 for line
in os
.popen("dpkg-query -W --showformat='${Package}\t${Version}\t${Architecture}\n'"):
95 package
, version
, debarch
= line
.split('\t', 2)
97 # Debian's 'epoch' system
98 version
= version
.split(':', 1)[1]
99 if debarch
== 'amd64\n':
103 clean_version
= try_cleanup_distro_version(version
)
105 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
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
112 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'dpkg-cache-tmp', dir = self
.cache_dir
)
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
)
120 stream
.write(line
+ '\n')
124 os
.path
.join(self
.cache_dir
,
130 def get_package_info(self
, package
, factory
):
132 version
, machine
= self
.versions
[package
]
136 impl
= factory('package:deb:%s:%s' % (package
, version
))
137 impl
.version
= model
.parse_version(version
)
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
)
150 self
.cache_dir
=basedir
.save_cache_path(namespaces
.config_site
,
151 namespaces
.config_prog
)
155 except Exception, ex
:
156 info("Failed to load cache (%s). Regenerating...",
159 self
.generate_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
))
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
)
181 raise Exception('Invalid cache format (bad header)')
183 if cache_version
is None:
184 raise Exception('Old cache format')
186 versions
= self
.versions
188 package
, version
, zi_arch
= line
[:-1].split('\t')
189 versions
[package
] = (version
, intern(zi_arch
))
191 def generate_cache(self
):
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':
198 if rpmarch
== 'amd64\n':
200 elif rpmarch
== 'noarch\n' or rpmarch
== "(none)\n":
203 zi_arch
= rpmarch
.strip()
204 clean_version
= try_cleanup_distro_version(version
)
206 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
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
213 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'rpm-cache-tmp',
214 dir = self
.cache_dir
)
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
)
222 stream
.write(line
+ '\n')
226 os
.path
.join(self
.cache_dir
,
232 def get_package_info(self
, package
, factory
):
234 version
, machine
= self
.versions
[package
]
238 impl
= factory('package:rpm:%s:%s' % (package
, version
))
239 impl
.version
= model
.parse_version(version
)
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
)
258 _host_distribution
= Distribution()
260 return _host_distribution