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 def __init__(self
, db_status_file
):
49 self
.status_details
= os
.stat(db_status_file
)
52 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
)
57 info("Failed to load dpkg cache (%s). Regenerating...", ex
)
62 warn("Failed to regenerate dpkg cache: %s", ex
)
65 stream
= file(self
.cache_dir
+ '/dpkg-status.cache')
71 name
, value
= line
.split(': ')
72 if name
== 'mtime' and int(value
) != int(self
.status_details
.st_mtime
):
73 raise Exception("Modification time of dpkg status file has changed")
74 if name
== 'size' and int(value
) != self
.status_details
.st_size
:
75 raise Exception("Size of dpkg status file has changed")
77 cache_version
= int(value
)
79 raise Exception('Invalid cache format (bad header)')
81 if cache_version
is None:
82 raise Exception('Old cache format')
84 versions
= self
.versions
86 package
, version
, zi_arch
= line
[:-1].split('\t')
87 versions
[package
] = (version
, intern(zi_arch
))
89 def generate_cache(self
):
92 for line
in os
.popen("dpkg-query -W -f='${Package}\t${Version}\t${Architecture}\n'"):
93 package
, version
, debarch
= line
.split('\t', 2)
95 # Debian's 'epoch' system
96 version
= version
.split(':', 1)[1]
97 if debarch
== 'amd64\n':
101 clean_version
= try_cleanup_distro_version(version
)
103 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
105 warn("Can't parse distribution version '%s' for package '%s'", version
, package
)
107 cache
.sort() # Might be useful later; currently we don't care
110 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'dpkg-cache-tmp', dir = self
.cache_dir
)
112 stream
= os
.fdopen(fd
, 'wb')
113 stream
.write('version: 2\n')
114 stream
.write('mtime: %d\n' % int(self
.status_details
.st_mtime
))
115 stream
.write('size: %d\n' % self
.status_details
.st_size
)
118 stream
.write(line
+ '\n')
121 os
.rename(tmpname
, self
.cache_dir
+ '/dpkg-status.cache')
126 def get_package_info(self
, package
, factory
):
128 version
, machine
= self
.versions
[package
]
132 impl
= factory('package:deb:%s:%s' % (package
, version
))
133 impl
.version
= model
.parse_version(version
)
135 impl
.machine
= machine
137 class RPMDistribution(Distribution
):
138 """An RPM-based distribution."""
140 cache_leaf
= 'rpm-status.cache'
142 def __init__(self
, packages_file
):
143 self
.status_details
= os
.stat(packages_file
)
146 self
.cache_dir
=basedir
.save_cache_path(namespaces
.config_site
,
147 namespaces
.config_prog
)
151 except Exception, ex
:
152 info("Failed to load cache (%s). Regenerating...",
155 self
.generate_cache()
157 except Exception, ex
:
158 warn("Failed to regenerate cache: %s", ex
)
160 def load_cache(self
):
161 stream
= file(os
.path
.join(self
.cache_dir
, self
.cache_leaf
))
166 name
, value
= line
.split(': ')
167 if name
== 'mtime' and (int(value
) !=
168 int(self
.status_details
.st_mtime
)):
169 raise Exception("Modification time of rpm status file has changed")
170 if name
== 'size' and (int(value
) !=
171 self
.status_details
.st_size
):
172 raise Exception("Size of rpm status file has changed")
174 raise Exception('Invalid cache format (bad header)')
176 versions
= self
.versions
178 package
, version
= line
[:-1].split('\t')
179 versions
[package
] = version
181 def __parse_rpm_name(self
, line
):
182 """Some samples we have to cope with (from SuSE 10.2):
183 mp3blaster-3.2.0-0.pm0
185 gpg-pubkey-1abd1afb-450ef738
188 gnome-backgrounds-2.16.1-14
189 gnome-icon-theme-2.16.0.1-12
190 opensuse-quickstart_en-10.2-9
191 susehelp_en-2006.06.20-25
192 yast2-schema-2.14.2-3"""
194 parts
=line
.strip().split('-')
196 return parts
[0], try_cleanup_distro_version(parts
[1])
201 package
='-'.join(parts
[:-2])
205 return package
, try_cleanup_distro_version(version
+'-'+mod
)
207 def generate_cache(self
):
210 for line
in os
.popen("rpm -qa"):
211 package
, version
= self
.__parse
_rpm
_name
(line
)
212 if package
and version
:
213 cache
.append('%s\t%s' % (package
, version
))
215 cache
.sort() # Might be useful later; currently we don't care
218 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'rpm-cache-tmp',
219 dir = self
.cache_dir
)
221 stream
= os
.fdopen(fd
, 'wb')
222 stream
.write('mtime: %d\n' % int(self
.status_details
.st_mtime
))
223 stream
.write('size: %d\n' % self
.status_details
.st_size
)
226 stream
.write(line
+ '\n')
230 os
.path
.join(self
.cache_dir
,
236 def get_package_info(self
, package
, factory
):
238 version
= self
.versions
[package
]
242 impl
= factory('package:rpm:%s:%s' % (package
, version
))
243 impl
.version
= model
.parse_version(version
)
245 _host_distribution
= None
246 def get_host_distribution():
247 """Get a Distribution suitable for the host operating system.
248 Calling this twice will return the same object.
249 @rtype: L{Distribution}"""
250 global _host_distribution
251 if not _host_distribution
:
252 _dpkg_db_status
= '/var/lib/dpkg/status'
253 _rpm_db
= '/var/lib/rpm/Packages'
255 if os
.access(_dpkg_db_status
, os
.R_OK
):
256 _host_distribution
= DebianDistribution(_dpkg_db_status
)
257 elif os
.path
.isfile(_rpm_db
):
258 _host_distribution
= RPMDistribution(_rpm_db
)
260 _host_distribution
= Distribution()
262 return _host_distribution