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.
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 CachedDistribution(Distribution
):
46 """For distributions where querying the package database is slow (e.g. requires running
47 an external command), we cache the results.
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
)
56 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
57 namespaces
.config_prog
)
62 info("Failed to load distribution database cache (%s). Regenerating...", 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
))
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")
84 cache_version
= int(value
)
86 raise Exception('Invalid cache format (bad header)')
88 if cache_version
is None:
89 raise Exception('Old cache format')
91 versions
= self
.versions
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
99 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'zeroinstall-cache-tmp',
100 dir = self
.cache_dir
)
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
)
108 stream
.write(line
+ '\n')
112 os
.path
.join(self
.cache_dir
,
118 class DebianDistribution(CachedDistribution
):
119 """A dpkg-based distribution."""
121 cache_leaf
= 'dpkg-status.cache'
123 def generate_cache(self
):
126 for line
in os
.popen("dpkg-query -W --showformat='${Package}\t${Version}\t${Architecture}\n'"):
127 package
, version
, debarch
= line
.split('\t', 2)
129 # Debian's 'epoch' system
130 version
= version
.split(':', 1)[1]
131 if debarch
== 'amd64\n':
135 clean_version
= try_cleanup_distro_version(version
)
137 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
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
):
145 version
, machine
= self
.versions
[package
]
149 impl
= factory('package:deb:%s:%s' % (package
, version
))
150 impl
.version
= model
.parse_version(version
)
152 impl
.machine
= machine
154 class RPMDistribution(CachedDistribution
):
155 """An RPM-based distribution."""
157 cache_leaf
= 'rpm-status.cache'
159 def generate_cache(self
):
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':
166 if rpmarch
== 'amd64\n':
168 elif rpmarch
== 'noarch\n' or rpmarch
== "(none)\n":
171 zi_arch
= rpmarch
.strip()
172 clean_version
= try_cleanup_distro_version(version
)
174 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
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
):
182 version
, machine
= self
.versions
[package
]
186 impl
= factory('package:rpm:%s:%s' % (package
, version
))
187 impl
.version
= model
.parse_version(version
)
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
)
206 _host_distribution
= Distribution()
208 return _host_distribution