2 Integration with native distribution package managers.
6 # Copyright (C) 2009, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
9 from zeroinstall
import _
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
28 match
= re
.match(_version_regexp
, version
)
30 version
, revision
= match
.groups()
34 return '%s-%s' % (version
, revision
[2:])
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.
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")
50 @param factory: function for creating new DistributionImplementation objects from IDs
51 @type factory: str -> L{model.DistributionImplementation}
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.
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
)
66 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
67 namespaces
.config_prog
)
72 info(_("Failed to load distribution database cache (%s). Regenerating..."), 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
))
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"))
94 cache_version
= int(value
)
96 raise Exception(_('Invalid cache format (bad header)'))
98 if cache_version
is None:
99 raise Exception(_('Old cache format'))
101 versions
= self
.versions
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
]
108 versions
[package
].append(versionarch
)
110 def _write_cache(self
, cache
):
111 #cache.sort() # Might be useful later; currently we don't care
113 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'zeroinstall-cache-tmp',
114 dir = self
.cache_dir
)
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
)
122 stream
.write(line
+ '\n')
126 os
.path
.join(self
.cache_dir
,
132 class DebianDistribution(CachedDistribution
):
133 """A dpkg-based distribution."""
135 cache_leaf
= 'dpkg-status.cache'
137 def generate_cache(self
):
140 for line
in os
.popen("dpkg-query -W --showformat='${Package}\t${Version}\t${Architecture}\n'"):
141 package
, version
, debarch
= line
.split('\t', 2)
143 # Debian's 'epoch' system
144 version
= version
.split(':', 1)[1]
145 if debarch
== 'amd64\n':
149 clean_version
= try_cleanup_distro_version(version
)
151 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
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
):
159 version
, machine
= self
.versions
[package
][0]
163 impl
= factory('package:deb:%s:%s' % (package
, version
))
164 impl
.version
= model
.parse_version(version
)
166 impl
.machine
= machine
168 class RPMDistribution(CachedDistribution
):
169 """An RPM-based distribution."""
171 cache_leaf
= 'rpm-status.cache'
173 def generate_cache(self
):
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':
180 if rpmarch
== 'amd64\n':
182 elif rpmarch
== 'noarch\n' or rpmarch
== "(none)\n":
185 zi_arch
= rpmarch
.strip()
186 clean_version
= try_cleanup_distro_version(version
)
188 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
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
):
196 versions
= self
.versions
[package
]
200 for version
, machine
in versions
:
201 impl
= factory('package:rpm:%s:%s:%s' % (package
, version
, machine
))
202 impl
.version
= model
.parse_version(version
)
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
)
226 warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name
})
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
)
255 _host_distribution
= Distribution()
257 return _host_distribution