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 _
, logger
10 import os
, platform
, re
, subprocess
, sys
11 from zeroinstall
.injector
import namespaces
, model
, arch
, qdom
12 from zeroinstall
.support
import basedir
, portable_rename
, intern
14 _dotted_ints
= '[0-9]+(?:\.[0-9]+)*'
16 # This matches a version number that would be a valid Zero Install version without modification
17 _zeroinstall_regexp
= '(?:%s)(?:-(?:pre|rc|post|)(?:%s))*' % (_dotted_ints
, _dotted_ints
)
19 # This matches the interesting bits of distribution version numbers
20 # (first matching group is for Java-style 6b17 syntax, or "major")
21 _version_regexp
= '(?:[a-z])?({ints}b)?({zero})(-r{ints})?'.format(zero
= _zeroinstall_regexp
, ints
= _dotted_ints
)
23 # We try to do updates atomically without locking, but we don't worry too much about
24 # duplicate entries or being a little out of sync with the on-disk copy.
26 def __init__(self
, cache_leaf
, source
, format
):
27 """Maintain a cache file (e.g. ~/.cache/0install.net/injector/$name).
28 If the size or mtime of $source has changed, or the cache
29 format version if different, reset the cache first."""
30 self
.cache_leaf
= cache_leaf
33 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
34 namespaces
.config_prog
)
35 self
.cached_for
= {} # Attributes of source when cache was created
38 except Exception as ex
:
39 logger
.info(_("Failed to load cache (%s). Flushing..."), ex
)
45 info
= os
.stat(self
.source
)
46 mtime
= int(info
.st_mtime
)
48 except Exception as ex
:
49 logger
.warn("Failed to stat %s: %s", self
.source
, ex
)
53 tmp
= tempfile
.NamedTemporaryFile(mode
= 'wt', dir = self
.cache_dir
, delete
= False)
54 tmp
.write("mtime=%d\nsize=%d\nformat=%d\n\n" % (mtime
, size
, self
.format
))
56 portable_rename(tmp
.name
, os
.path
.join(self
.cache_dir
, self
.cache_leaf
))
60 # Populate self.cache from our saved cache file.
61 # Throws an exception if the cache doesn't exist or has the wrong format.
62 def _load_cache(self
):
63 self
.cache
= cache
= {}
64 with
open(os
.path
.join(self
.cache_dir
, self
.cache_leaf
)) as stream
:
69 key
, value
= line
.split('=', 1)
70 if key
in ('mtime', 'size', 'format'):
71 self
.cached_for
[key
] = int(value
)
76 key
, value
= line
.split('=', 1)
77 cache
[key
] = value
[:-1]
79 # Check the source file hasn't changed since we created the cache
80 def _check_valid(self
):
81 info
= os
.stat(self
.source
)
82 if self
.cached_for
['mtime'] != int(info
.st_mtime
):
83 raise Exception("Modification time of %s has changed" % self
.source
)
84 if self
.cached_for
['size'] != info
.st_size
:
85 raise Exception("Size of %s has changed" % self
.source
)
86 if self
.cached_for
.get('format', None) != self
.format
:
87 raise Exception("Format of cache has changed")
92 except Exception as ex
:
93 logger
.info(_("Cache needs to be refreshed: %s"), ex
)
97 return self
.cache
.get(key
, None)
99 def put(self
, key
, value
):
100 cache_path
= os
.path
.join(self
.cache_dir
, self
.cache_leaf
)
101 self
.cache
[key
] = value
103 with
open(cache_path
, 'a') as stream
:
104 stream
.write('%s=%s\n' % (key
, value
))
105 except Exception as ex
:
106 logger
.warn("Failed to write to cache %s: %s=%s: %s", cache_path
, key
, value
, ex
)
108 def try_cleanup_distro_version(version
):
109 """Try to turn a distribution version string into one readable by Zero Install.
110 We do this by stripping off anything we can't parse.
111 @return: the part we understood, or None if we couldn't parse anything
114 version
= version
.split(':')[1] # Skip 'epoch'
115 version
= version
.replace('_', '-')
117 version
, suffix
= version
.split('~', 1)
118 suffix
= '-pre' + try_cleanup_distro_version(suffix
)
121 match
= re
.match(_version_regexp
, version
)
123 major
, version
, revision
= match
.groups()
124 if major
is not None:
125 version
= major
[:-1] + '.' + version
126 if revision
is not None:
127 version
= '%s-%s' % (version
, revision
[2:])
128 return version
+ suffix
131 class Distribution(object):
132 """Represents a distribution with which we can integrate.
133 Sub-classes should specialise this to integrate with the package managers of
134 particular distributions. This base class ignores the native package manager.
139 def get_package_info(self
, package
, factory
):
140 """Get information about the given package.
141 Add zero or more implementations using the factory (typically at most two
142 will be added; the currently installed version and the latest available).
143 @param package: package name (e.g. "gimp")
145 @param factory: function for creating new DistributionImplementation objects from IDs
146 @type factory: str -> L{model.DistributionImplementation}
150 def get_score(self
, distribution
):
151 """Indicate how closely the host distribution matches this one.
152 The <package-implementation> with the highest score is passed
153 to L{Distribution.get_package_info}. If several elements get
154 the same score, get_package_info is called for all of them.
155 @param distribution: a distribution name
156 @type distribution: str
157 @return: an integer, or -1 if there is no match at all
162 def get_feed(self
, master_feed
):
163 """Generate a feed containing information about distribution packages.
164 This should immediately return a feed containing an implementation for the
165 package if it's already installed. Information about versions that could be
166 installed using the distribution's package manager can be added asynchronously
167 later (see L{fetch_candidates}).
168 @param master_feed: feed containing the <package-implementation> elements
169 @type master_feed: L{model.ZeroInstallFeed}
170 @rtype: L{model.ZeroInstallFeed}"""
172 feed
= model
.ZeroInstallFeed(None)
173 feed
.url
= 'distribution:' + master_feed
.url
175 for item
, item_attrs
in master_feed
.get_package_impls(self
):
176 package
= item_attrs
.get('package', None)
178 raise model
.InvalidInterface(_("Missing 'package' attribute on %s") % item
)
182 def factory(id, only_if_missing
= False, installed
= True):
183 assert id.startswith('package:')
184 if id in feed
.implementations
:
187 logger
.warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
188 impl
= model
.DistributionImplementation(feed
, id, self
, item
)
189 feed
.implementations
[id] = impl
190 new_impls
.append(impl
)
192 impl
.installed
= installed
193 impl
.metadata
= item_attrs
195 if 'run' not in impl
.commands
:
196 item_main
= item_attrs
.get('main', None)
198 if item_main
.startswith('/'):
199 impl
.main
= item_main
201 raise model
.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
203 impl
.upstream_stability
= model
.packaged
207 self
.get_package_info(package
, factory
)
209 for impl
in new_impls
:
210 self
.fixup(package
, impl
)
212 if master_feed
.url
== 'http://repo.roscidus.com/python/python' and all(not impl
.installed
for impl
in feed
.implementations
.values()):
213 # Hack: we can support Python on platforms with unsupported package managers
214 # by adding the implementation of Python running us now to the list.
215 python_version
= '.'.join([str(v
) for v
in sys
.version_info
if isinstance(v
, int)])
216 impl_id
= 'package:host:python:' + python_version
217 assert impl_id
not in feed
.implementations
218 impl
= model
.DistributionImplementation(feed
, impl_id
, self
)
219 impl
.installed
= True
220 impl
.version
= model
.parse_version(python_version
)
221 impl
.main
= sys
.executable
222 impl
.upstream_stability
= model
.packaged
223 impl
.machine
= host_machine
# (hopefully)
224 feed
.implementations
[impl_id
] = impl
228 def fetch_candidates(self
, master_feed
):
229 """Collect information about versions we could install using
230 the distribution's package manager. On success, the distribution
231 feed in iface_cache is updated.
232 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
233 if self
.packagekit
.available
:
234 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
235 return self
.packagekit
.fetch_candidates(package_names
)
238 def packagekit(self
):
239 """For use by subclasses.
240 @rtype: L{packagekit.PackageKit}"""
241 if not self
._packagekit
:
242 from zeroinstall
.injector
import packagekit
243 self
._packagekit
= packagekit
.PackageKit()
244 return self
._packagekit
246 def fixup(self
, package
, impl
):
247 """Some packages require special handling (e.g. Java). This is called for each
248 package that was added by L{get_package_info} after it returns. The default
250 @param package: the name of the package
251 @param impl: the constructed implementation"""
254 class WindowsDistribution(Distribution
):
255 def get_package_info(self
, package
, factory
):
256 def _is_64bit_windows():
258 from win32process
import IsWow64Process
259 if p
== 'win64' or (p
== 'win32' and IsWow64Process()): return True
260 elif p
== 'win32': return False
261 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
263 def _read_hklm_reg(key_name
, value_name
):
264 from win32api
import RegOpenKeyEx
, RegQueryValueEx
, RegCloseKey
265 from win32con
import HKEY_LOCAL_MACHINE
, KEY_READ
266 KEY_WOW64_64KEY
= 0x0100
267 KEY_WOW64_32KEY
= 0x0200
268 if _is_64bit_windows():
270 key32
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ | KEY_WOW64_32KEY
)
271 (value32
, _
) = RegQueryValueEx(key32
, value_name
)
276 key64
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ | KEY_WOW64_64KEY
)
277 (value64
, _
) = RegQueryValueEx(key64
, value_name
)
283 key32
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ
)
284 (value32
, _
) = RegQueryValueEx(key32
, value_name
)
289 return (value32
, value64
)
291 def find_java(part
, win_version
, zero_version
):
292 reg_path
= r
"SOFTWARE\JavaSoft\{part}\{win_version}".format(part
= part
, win_version
= win_version
)
293 (java32_home
, java64_home
) = _read_hklm_reg(reg_path
, "JavaHome")
295 for (home
, arch
) in [(java32_home
, 'i486'),
296 (java64_home
, 'x86_64')]:
297 if os
.path
.isfile(home
+ r
"\bin\java.exe"):
298 impl
= factory('package:windows:%s:%s:%s' % (package
, zero_version
, arch
))
300 impl
.version
= model
.parse_version(zero_version
)
301 impl
.upstream_stability
= model
.packaged
302 impl
.main
= home
+ r
"\bin\java.exe"
304 if package
== 'openjdk-6-jre':
305 find_java("Java Runtime Environment", "1.6", '6')
306 elif package
== 'openjdk-6-jdk':
307 find_java("Java Development Kit", "1.6", '6')
308 elif package
== 'openjdk-7-jre':
309 find_java("Java Runtime Environment", "1.7", '7')
310 elif package
== 'openjdk-7-jdk':
311 find_java("Java Development Kit", "1.7", '7')
313 def get_score(self
, disto_name
):
314 return int(disto_name
== 'Windows')
316 class DarwinDistribution(Distribution
):
317 def get_package_info(self
, package
, factory
):
318 def java_home(version
, arch
):
319 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
320 child
= subprocess
.Popen(["/usr/libexec/java_home", "--failfast", "--version", version
, "--arch", arch
],
321 stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
322 home
= child
.stdout
.read().strip()
327 def find_java(part
, jvm_version
, zero_version
):
328 for arch
in ['i386', 'x86_64']:
329 home
= java_home(jvm_version
, arch
)
330 if os
.path
.isfile(home
+ "/bin/java"):
331 impl
= factory('package:darwin:%s:%s:%s' % (package
, zero_version
, arch
))
333 impl
.version
= model
.parse_version(zero_version
)
334 impl
.upstream_stability
= model
.packaged
335 impl
.main
= home
+ "/bin/java"
337 if package
== 'openjdk-6-jre':
338 find_java("Java Runtime Environment", "1.6", '6')
339 elif package
== 'openjdk-6-jdk':
340 find_java("Java Development Kit", "1.6", '6')
341 elif package
== 'openjdk-7-jre':
342 find_java("Java Runtime Environment", "1.7", '7')
343 elif package
== 'openjdk-7-jdk':
344 find_java("Java Development Kit", "1.7", '7')
346 def get_score(self
, disto_name
):
347 return int(disto_name
== 'Darwin')
349 class CachedDistribution(Distribution
):
350 """For distributions where querying the package database is slow (e.g. requires running
351 an external command), we cache the results.
353 @deprecated: use Cache instead
356 def __init__(self
, db_status_file
):
357 """@param db_status_file: update the cache when the timestamp of this file changes"""
358 self
._status
_details
= os
.stat(db_status_file
)
361 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
362 namespaces
.config_prog
)
366 except Exception as ex
:
367 logger
.info(_("Failed to load distribution database cache (%s). Regenerating..."), ex
)
369 self
.generate_cache()
371 except Exception as ex
:
372 logger
.warn(_("Failed to regenerate distribution database cache: %s"), ex
)
374 def _load_cache(self
):
375 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
376 Throws an exception if the cache should be (re)created."""
377 with
open(os
.path
.join(self
.cache_dir
, self
.cache_leaf
), 'rt') as stream
:
382 name
, value
= line
.split(': ')
383 if name
== 'mtime' and int(value
) != int(self
._status
_details
.st_mtime
):
384 raise Exception(_("Modification time of package database file has changed"))
385 if name
== 'size' and int(value
) != self
._status
_details
.st_size
:
386 raise Exception(_("Size of package database file has changed"))
387 if name
== 'version':
388 cache_version
= int(value
)
390 raise Exception(_('Invalid cache format (bad header)'))
392 if cache_version
is None:
393 raise Exception(_('Old cache format'))
395 versions
= self
.versions
397 package
, version
, zi_arch
= line
[:-1].split('\t')
398 versionarch
= (version
, intern(zi_arch
))
399 if package
not in versions
:
400 versions
[package
] = [versionarch
]
402 versions
[package
].append(versionarch
)
404 def _write_cache(self
, cache
):
405 #cache.sort() # Might be useful later; currently we don't care
407 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'zeroinstall-cache-tmp',
408 dir = self
.cache_dir
)
410 stream
= os
.fdopen(fd
, 'wt')
411 stream
.write('version: 2\n')
412 stream
.write('mtime: %d\n' % int(self
._status
_details
.st_mtime
))
413 stream
.write('size: %d\n' % self
._status
_details
.st_size
)
416 stream
.write(line
+ '\n')
419 portable_rename(tmpname
,
420 os
.path
.join(self
.cache_dir
,
426 # Maps machine type names used in packages to their Zero Install versions
427 # (updates to this might require changing the reverse Java mapping)
428 _canonical_machine
= {
443 host_machine
= arch
.canonicalize_machine(platform
.uname()[4])
444 def canonical_machine(package_machine
):
445 machine
= _canonical_machine
.get(package_machine
, None)
447 # Safe default if we can't understand the arch
451 class DebianDistribution(Distribution
):
452 """A dpkg-based distribution."""
454 cache_leaf
= 'dpkg-status.cache'
456 def __init__(self
, dpkg_status
):
457 self
.dpkg_cache
= Cache('dpkg-status.cache', dpkg_status
, 2)
460 def _query_installed_package(self
, package
):
461 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
462 child
= subprocess
.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package
],
463 stdout
= subprocess
.PIPE
, stderr
= null
,
464 universal_newlines
= True) # Needed for Python 3
466 stdout
, stderr
= child
.communicate()
468 for line
in stdout
.split('\n'):
469 if not line
: continue
470 version
, debarch
, status
= line
.split('\t', 2)
471 if not status
.endswith(' installed'): continue
472 clean_version
= try_cleanup_distro_version(version
)
473 if debarch
.find("-") != -1:
474 debarch
= debarch
.split("-")[-1]
476 return '%s\t%s' % (clean_version
, canonical_machine(debarch
.strip()))
478 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
482 def get_package_info(self
, package
, factory
):
483 # Add any already-installed package...
484 installed_cached_info
= self
._get
_dpkg
_info
(package
)
486 if installed_cached_info
!= '-':
487 installed_version
, machine
= installed_cached_info
.split('\t')
488 impl
= factory('package:deb:%s:%s:%s' % (package
, installed_version
, machine
))
489 impl
.version
= model
.parse_version(installed_version
)
491 impl
.machine
= machine
493 installed_version
= None
495 # Add any uninstalled candidates (note: only one of these two methods will add anything)
498 self
.packagekit
.get_candidates(package
, factory
, 'package:deb')
501 cached
= self
.apt_cache
.get(package
, None)
503 candidate_version
= cached
['version']
504 candidate_arch
= cached
['arch']
505 if candidate_version
and candidate_version
!= installed_version
:
506 impl
= factory('package:deb:%s:%s:%s' % (package
, candidate_version
, candidate_arch
), installed
= False)
507 impl
.version
= model
.parse_version(candidate_version
)
508 if candidate_arch
!= '*':
509 impl
.machine
= candidate_arch
510 def install(handler
):
511 raise model
.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
512 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
513 "use that to install it.") % package
)
514 impl
.download_sources
.append(model
.DistributionSource(package
, cached
['size'], install
, needs_confirmation
= False))
516 def fixup(self
, package
, impl
):
517 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
518 if package
== 'openjdk-6-jre':
519 java_version
= '6-openjdk'
520 elif package
== 'openjdk-7-jre':
521 java_version
= '7-openjdk'
525 if impl
.machine
== 'x86_64':
528 java_arch
= impl
.machine
530 java_bin
= '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version
, java_arch
)
531 if not os
.path
.exists(java_bin
):
532 # Try without the arch...
533 java_bin
= '/usr/lib/jvm/java-%s/jre/bin/java' % java_version
534 if not os
.path
.exists(java_bin
):
535 logger
.info("Java binary not found (%s)", java_bin
)
536 if impl
.main
is None:
537 java_bin
= '/usr/bin/java'
541 impl
.commands
["run"] = model
.Command(qdom
.Element(namespaces
.XMLNS_IFACE
, 'command',
542 {'path': java_bin
, 'name': 'run'}), None)
544 def get_score(self
, disto_name
):
545 return int(disto_name
== 'Debian')
547 def _get_dpkg_info(self
, package
):
548 installed_cached_info
= self
.dpkg_cache
.get(package
)
549 if installed_cached_info
== None:
550 installed_cached_info
= self
._query
_installed
_package
(package
)
551 self
.dpkg_cache
.put(package
, installed_cached_info
)
553 return installed_cached_info
555 def fetch_candidates(self
, master_feed
):
556 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
558 if self
.packagekit
.available
:
559 return self
.packagekit
.fetch_candidates(package_names
)
561 # No PackageKit. Use apt-cache directly.
562 for package
in package_names
:
563 # Check to see whether we could get a newer version using apt-get
565 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
566 child
= subprocess
.Popen(['apt-cache', 'show', '--no-all-versions', '--', package
], stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
569 arch
= version
= size
= None
570 for line
in child
.stdout
:
572 if line
.startswith('Version: '):
574 version
= try_cleanup_distro_version(version
)
575 elif line
.startswith('Architecture: '):
576 arch
= canonical_machine(line
[14:].strip())
577 elif line
.startswith('Size: '):
578 size
= int(line
[6:].strip())
580 cached
= {'version': version
, 'arch': arch
, 'size': size
}
585 except Exception as ex
:
586 logger
.warn("'apt-cache show %s' failed: %s", package
, ex
)
588 # (multi-arch support? can there be multiple candidates?)
589 self
.apt_cache
[package
] = cached
591 class RPMDistribution(CachedDistribution
):
592 """An RPM-based distribution."""
594 cache_leaf
= 'rpm-status.cache'
596 def generate_cache(self
):
599 child
= subprocess
.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
600 stdout
= subprocess
.PIPE
, universal_newlines
= True)
601 for line
in child
.stdout
:
602 package
, version
, rpmarch
= line
.split('\t', 2)
603 if package
== 'gpg-pubkey':
605 zi_arch
= canonical_machine(rpmarch
.strip())
606 clean_version
= try_cleanup_distro_version(version
)
608 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
610 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
612 self
._write
_cache
(cache
)
616 def get_package_info(self
, package
, factory
):
617 # Add installed versions...
618 versions
= self
.versions
.get(package
, [])
620 for version
, machine
in versions
:
621 impl
= factory('package:rpm:%s:%s:%s' % (package
, version
, machine
))
622 impl
.version
= model
.parse_version(version
)
624 impl
.machine
= machine
626 # Add any uninstalled candidates found by PackageKit
627 self
.packagekit
.get_candidates(package
, factory
, 'package:rpm')
629 def fixup(self
, package
, impl
):
630 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
632 # OpenSUSE uses _, Fedora uses .
633 package
= package
.replace('_', '.')
635 if package
== 'java-1.6.0-openjdk':
636 java_version
= '1.6.0-openjdk'
637 elif package
== 'java-1.7.0-openjdk':
638 java_version
= '1.7.0-openjdk'
639 elif package
in ('java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'):
640 if impl
.version
[0][0] == 1:
641 # OpenSUSE uses 1.6 to mean 6
642 del impl
.version
[0][0]
647 if impl
.version
[0][0] == 1:
648 # OpenSUSE uses 1.6 to mean 6
649 del impl
.version
[0][0]
651 # On Fedora, unlike Debian, the arch is x86_64, not amd64
653 java_bin
= '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version
, impl
.machine
)
654 if not os
.path
.exists(java_bin
):
655 # Try without the arch...
656 java_bin
= '/usr/lib/jvm/jre-%s/bin/java' % java_version
657 if not os
.path
.exists(java_bin
):
658 logger
.info("Java binary not found (%s)", java_bin
)
659 if impl
.main
is None:
660 java_bin
= '/usr/bin/java'
664 impl
.commands
["run"] = model
.Command(qdom
.Element(namespaces
.XMLNS_IFACE
, 'command',
665 {'path': java_bin
, 'name': 'run'}), None)
667 def get_score(self
, disto_name
):
668 return int(disto_name
== 'RPM')
670 class SlackDistribution(Distribution
):
671 """A Slack-based distribution."""
673 def __init__(self
, packages_dir
):
674 self
._packages
_dir
= packages_dir
676 def get_package_info(self
, package
, factory
):
677 # Add installed versions...
678 for entry
in os
.listdir(self
._packages
_dir
):
679 name
, version
, arch
, build
= entry
.rsplit('-', 3)
681 zi_arch
= canonical_machine(arch
)
682 clean_version
= try_cleanup_distro_version("%s-%s" % (version
, build
))
683 if not clean_version
:
684 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': name
})
687 impl
= factory('package:slack:%s:%s:%s' % \
688 (package
, clean_version
, zi_arch
))
689 impl
.version
= model
.parse_version(clean_version
)
691 impl
.machine
= zi_arch
693 # Add any uninstalled candidates found by PackageKit
694 self
.packagekit
.get_candidates(package
, factory
, 'package:slack')
696 def get_score(self
, disto_name
):
697 return int(disto_name
== 'Slack')
699 class ArchDistribution(Distribution
):
700 """An Arch Linux distribution."""
702 def __init__(self
, packages_dir
):
703 self
._packages
_dir
= os
.path
.join(packages_dir
, "local")
705 def get_package_info(self
, package
, factory
):
706 # Add installed versions...
707 for entry
in os
.listdir(self
._packages
_dir
):
708 name
, version
, build
= entry
.rsplit('-', 2)
711 with
open(os
.path
.join(self
._packages
_dir
, entry
, "desc"), 'rt') as stream
:
713 if line
== "%ARCH%\n":
719 zi_arch
= canonical_machine(arch
)
720 clean_version
= try_cleanup_distro_version("%s-%s" % (version
, build
))
721 if not clean_version
:
722 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': name
})
725 impl
= factory('package:arch:%s:%s:%s' % \
726 (package
, clean_version
, zi_arch
))
727 impl
.version
= model
.parse_version(clean_version
)
729 impl
.machine
= zi_arch
731 # Add any uninstalled candidates found by PackageKit
732 self
.packagekit
.get_candidates(package
, factory
, 'package:arch')
734 def get_score(self
, disto_name
):
735 return int(disto_name
== 'Arch')
737 class GentooDistribution(Distribution
):
739 def __init__(self
, pkgdir
):
740 self
._pkgdir
= pkgdir
742 def get_package_info(self
, package
, factory
):
743 # Add installed versions...
744 _version_start_reqexp
= '-[0-9]'
746 if package
.count('/') != 1: return
748 category
, leafname
= package
.split('/')
749 category_dir
= os
.path
.join(self
._pkgdir
, category
)
750 match_prefix
= leafname
+ '-'
752 if not os
.path
.isdir(category_dir
): return
754 for filename
in os
.listdir(category_dir
):
755 if filename
.startswith(match_prefix
) and filename
[len(match_prefix
)].isdigit():
756 with
open(os
.path
.join(category_dir
, filename
, 'PF'), 'rt') as stream
:
757 name
= stream
.readline().strip()
759 match
= re
.search(_version_start_reqexp
, name
)
761 logger
.warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name
})
764 version
= try_cleanup_distro_version(name
[match
.start() + 1:])
766 if category
== 'app-emulation' and name
.startswith('emul-'):
767 __
, __
, machine
, __
= name
.split('-', 3)
769 with
open(os
.path
.join(category_dir
, filename
, 'CHOST'), 'rt') as stream
:
770 machine
, __
= stream
.readline().split('-', 1)
771 machine
= arch
.canonicalize_machine(machine
)
773 impl
= factory('package:gentoo:%s:%s:%s' % \
774 (package
, version
, machine
))
775 impl
.version
= model
.parse_version(version
)
776 impl
.machine
= machine
778 # Add any uninstalled candidates found by PackageKit
779 self
.packagekit
.get_candidates(package
, factory
, 'package:gentoo')
781 def get_score(self
, disto_name
):
782 return int(disto_name
== 'Gentoo')
784 class PortsDistribution(Distribution
):
786 def __init__(self
, pkgdir
):
787 self
._pkgdir
= pkgdir
789 def get_package_info(self
, package
, factory
):
790 _name_version_regexp
= '^(.+)-([^-]+)$'
792 nameversion
= re
.compile(_name_version_regexp
)
793 for pkgname
in os
.listdir(self
._pkgdir
):
794 pkgdir
= os
.path
.join(self
._pkgdir
, pkgname
)
795 if not os
.path
.isdir(pkgdir
): continue
797 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
799 match
= nameversion
.search(pkgname
)
801 logger
.warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname
})
804 name
= match
.group(1)
807 version
= try_cleanup_distro_version(match
.group(2))
809 machine
= host_machine
811 impl
= factory('package:ports:%s:%s:%s' % \
812 (package
, version
, machine
))
813 impl
.version
= model
.parse_version(version
)
814 impl
.machine
= machine
816 def get_score(self
, disto_name
):
817 return int(disto_name
== 'Ports')
819 class MacPortsDistribution(CachedDistribution
):
821 cache_leaf
= 'macports-status.cache'
823 def generate_cache(self
):
826 child
= subprocess
.Popen(["port", "-v", "installed"],
827 stdout
= subprocess
.PIPE
, universal_newlines
= True)
828 for line
in child
.stdout
:
829 if not line
.startswith(" "):
831 if line
.strip().count(" ") > 1:
832 package
, version
, extra
= line
.split(None, 2)
834 package
, version
= line
.split()
836 if not extra
.startswith("(active)"):
838 version
= version
.lstrip('@')
839 version
= re
.sub(r
"\+.*", "", version
) # strip variants
841 clean_version
= try_cleanup_distro_version(version
)
843 match
= re
.match(r
" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra
)
845 platform
, major
, archs
= match
.groups()
846 for arch
in archs
.split():
847 zi_arch
= canonical_machine(arch
)
848 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
850 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
852 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
853 self
._write
_cache
(cache
)
857 def get_package_info(self
, package
, factory
):
858 # Add installed versions...
859 versions
= self
.versions
.get(package
, [])
861 for version
, machine
in versions
:
862 impl
= factory('package:macports:%s:%s:%s' % (package
, version
, machine
))
863 impl
.version
= model
.parse_version(version
)
865 impl
.machine
= machine
867 def get_score(self
, disto_name
):
868 return int(disto_name
== 'MacPorts')
870 class CygwinDistribution(CachedDistribution
):
871 """A Cygwin-based distribution."""
873 cache_leaf
= 'cygcheck-status.cache'
875 def generate_cache(self
):
878 zi_arch
= canonical_machine(arch
)
879 for line
in os
.popen("cygcheck -c -d"):
880 if line
== "Cygwin Package Information\r\n":
884 package
, version
= line
.split()
885 if package
== "Package" and version
== "Version":
887 clean_version
= try_cleanup_distro_version(version
)
889 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
891 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
893 self
._write
_cache
(cache
)
895 def get_package_info(self
, package
, factory
):
896 # Add installed versions...
897 versions
= self
.versions
.get(package
, [])
899 for version
, machine
in versions
:
900 impl
= factory('package:cygwin:%s:%s:%s' % (package
, version
, machine
))
901 impl
.version
= model
.parse_version(version
)
903 impl
.machine
= machine
905 def get_score(self
, disto_name
):
906 return int(disto_name
== 'Cygwin')
909 _host_distribution
= None
910 def get_host_distribution():
911 """Get a Distribution suitable for the host operating system.
912 Calling this twice will return the same object.
913 @rtype: L{Distribution}"""
914 global _host_distribution
915 if not _host_distribution
:
916 dpkg_db_status
= '/var/lib/dpkg/status'
917 rpm_db_packages
= '/var/lib/rpm/Packages'
918 _slack_db
= '/var/log/packages'
919 _arch_db
= '/var/lib/pacman'
920 _pkg_db
= '/var/db/pkg'
921 _macports_db
= '/opt/local/var/macports/registry/registry.db'
922 _cygwin_log
= '/var/log/setup.log'
924 if sys
.prefix
== "/sw":
925 dpkg_db_status
= os
.path
.join(sys
.prefix
, dpkg_db_status
)
926 rpm_db_packages
= os
.path
.join(sys
.prefix
, rpm_db_packages
)
929 _host_distribution
= WindowsDistribution()
930 elif os
.path
.isdir(_pkg_db
):
931 if sys
.platform
.startswith("linux"):
932 _host_distribution
= GentooDistribution(_pkg_db
)
933 elif sys
.platform
.startswith("freebsd"):
934 _host_distribution
= PortsDistribution(_pkg_db
)
935 elif os
.path
.isfile(_macports_db
) \
936 and sys
.prefix
.startswith("/opt/local"):
937 _host_distribution
= MacPortsDistribution(_macports_db
)
938 elif os
.path
.isfile(_cygwin_log
) and sys
.platform
== "cygwin":
939 _host_distribution
= CygwinDistribution(_cygwin_log
)
940 elif os
.access(dpkg_db_status
, os
.R_OK
) \
941 and os
.path
.getsize(dpkg_db_status
) > 0:
942 _host_distribution
= DebianDistribution(dpkg_db_status
)
943 elif os
.path
.isfile(rpm_db_packages
):
944 _host_distribution
= RPMDistribution(rpm_db_packages
)
945 elif os
.path
.isdir(_slack_db
):
946 _host_distribution
= SlackDistribution(_slack_db
)
947 elif os
.path
.isdir(_arch_db
):
948 _host_distribution
= ArchDistribution(_arch_db
)
949 elif sys
.platform
== "darwin":
950 _host_distribution
= DarwinDistribution()
952 _host_distribution
= Distribution()
954 return _host_distribution