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 _
10 import os
, platform
, re
, subprocess
, sys
11 from logging
import warn
, info
12 from zeroinstall
.injector
import namespaces
, model
, arch
13 from zeroinstall
.support
import basedir
, portable_rename
, intern
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 # (first matching group is for Java-style 6b17 syntax, or "major")
22 _version_regexp
= '(?:[a-z])?({ints}b)?({zero})(-r{ints})?'.format(zero
= _zeroinstall_regexp
, ints
= _dotted_ints
)
24 # We try to do updates atomically without locking, but we don't worry too much about
25 # duplicate entries or being a little out of sync with the on-disk copy.
27 def __init__(self
, cache_leaf
, source
, format
):
28 """Maintain a cache file (e.g. ~/.cache/0install.net/injector/$name).
29 If the size or mtime of $source has changed, or the cache
30 format version if different, reset the cache first."""
31 self
.cache_leaf
= cache_leaf
34 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
35 namespaces
.config_prog
)
36 self
.cached_for
= {} # Attributes of source when cache was created
39 except Exception as ex
:
40 info(_("Failed to load cache (%s). Flushing..."), ex
)
46 info
= os
.stat(self
.source
)
47 mtime
= int(info
.st_mtime
)
49 except Exception as ex
:
50 warn("Failed to stat %s: %s", self
.source
, ex
)
54 tmp
= tempfile
.NamedTemporaryFile(mode
= 'wt', dir = self
.cache_dir
, delete
= False)
55 tmp
.write("mtime=%d\nsize=%d\nformat=%d\n\n" % (mtime
, size
, self
.format
))
57 portable_rename(tmp
.name
, os
.path
.join(self
.cache_dir
, self
.cache_leaf
))
61 # Populate self.cache from our saved cache file.
62 # Throws an exception if the cache doesn't exist or has the wrong format.
63 def _load_cache(self
):
64 self
.cache
= cache
= {}
65 with
open(os
.path
.join(self
.cache_dir
, self
.cache_leaf
)) as stream
:
70 key
, value
= line
.split('=', 1)
71 if key
in ('mtime', 'size', 'format'):
72 self
.cached_for
[key
] = int(value
)
77 key
, value
= line
.split('=', 1)
78 cache
[key
] = value
[:-1]
80 # Check the source file hasn't changed since we created the cache
81 def _check_valid(self
):
82 info
= os
.stat(self
.source
)
83 if self
.cached_for
['mtime'] != int(info
.st_mtime
):
84 raise Exception("Modification time of %s has changed" % self
.source
)
85 if self
.cached_for
['size'] != info
.st_size
:
86 raise Exception("Size of %s has changed" % self
.source
)
87 if self
.cached_for
.get('format', None) != self
.format
:
88 raise Exception("Format of cache has changed")
93 except Exception as ex
:
94 info(_("Cache needs to be refreshed: %s"), ex
)
98 return self
.cache
.get(key
, None)
100 def put(self
, key
, value
):
101 cache_path
= os
.path
.join(self
.cache_dir
, self
.cache_leaf
)
102 self
.cache
[key
] = value
104 with
open(cache_path
, 'a') as stream
:
105 stream
.write('%s=%s\n' % (key
, value
))
106 except Exception as ex
:
107 warn("Failed to write to cache %s: %s=%s: %s", cache_path
, key
, value
, ex
)
109 def try_cleanup_distro_version(version
):
110 """Try to turn a distribution version string into one readable by Zero Install.
111 We do this by stripping off anything we can't parse.
112 @return: the part we understood, or None if we couldn't parse anything
115 version
= version
.split(':')[1] # Skip 'epoch'
116 version
= version
.replace('_', '-')
117 match
= re
.match(_version_regexp
, version
)
119 major
, version
, revision
= match
.groups()
120 if major
is not None:
121 version
= major
[:-1] + '.' + version
125 return '%s-%s' % (version
, revision
[2:])
128 class Distribution(object):
129 """Represents a distribution with which we can integrate.
130 Sub-classes should specialise this to integrate with the package managers of
131 particular distributions. This base class ignores the native package manager.
136 def get_package_info(self
, package
, factory
):
137 """Get information about the given package.
138 Add zero or more implementations using the factory (typically at most two
139 will be added; the currently installed version and the latest available).
140 @param package: package name (e.g. "gimp")
142 @param factory: function for creating new DistributionImplementation objects from IDs
143 @type factory: str -> L{model.DistributionImplementation}
147 def get_score(self
, distribution
):
148 """Indicate how closely the host distribution matches this one.
149 The <package-implementation> with the highest score is passed
150 to L{Distribution.get_package_info}. If several elements get
151 the same score, get_package_info is called for all of them.
152 @param distribution: a distribution name
153 @type distribution: str
154 @return: an integer, or -1 if there is no match at all
159 def get_feed(self
, master_feed
):
160 """Generate a feed containing information about distribution packages.
161 This should immediately return a feed containing an implementation for the
162 package if it's already installed. Information about versions that could be
163 installed using the distribution's package manager can be added asynchronously
164 later (see L{fetch_candidates}).
165 @param master_feed: feed containing the <package-implementation> elements
166 @type master_feed: L{model.ZeroInstallFeed}
167 @rtype: L{model.ZeroInstallFeed}"""
169 feed
= model
.ZeroInstallFeed(None)
170 feed
.url
= 'distribution:' + master_feed
.url
172 for item
, item_attrs
in master_feed
.get_package_impls(self
):
173 package
= item_attrs
.get('package', None)
175 raise model
.InvalidInterface(_("Missing 'package' attribute on %s") % item
)
177 def factory(id, only_if_missing
= False, installed
= True):
178 assert id.startswith('package:')
179 if id in feed
.implementations
:
182 warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
183 impl
= model
.DistributionImplementation(feed
, id, self
, item
)
184 feed
.implementations
[id] = impl
186 impl
.installed
= installed
187 impl
.metadata
= item_attrs
189 if 'run' not in impl
.commands
:
190 item_main
= item_attrs
.get('main', None)
192 if item_main
.startswith('/'):
193 impl
.main
= item_main
195 raise model
.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
197 impl
.upstream_stability
= model
.packaged
201 self
.get_package_info(package
, factory
)
203 if master_feed
.url
== 'http://repo.roscidus.com/python/python' and all(not impl
.installed
for impl
in feed
.implementations
.values()):
204 # Hack: we can support Python on platforms with unsupported package managers
205 # by adding the implementation of Python running us now to the list.
206 python_version
= '.'.join([str(v
) for v
in sys
.version_info
if isinstance(v
, int)])
207 impl_id
= 'package:host:python:' + python_version
208 assert impl_id
not in feed
.implementations
209 impl
= model
.DistributionImplementation(feed
, impl_id
, self
)
210 impl
.installed
= True
211 impl
.version
= model
.parse_version(python_version
)
212 impl
.main
= sys
.executable
213 impl
.upstream_stability
= model
.packaged
214 impl
.machine
= host_machine
# (hopefully)
215 feed
.implementations
[impl_id
] = impl
219 def fetch_candidates(self
, master_feed
):
220 """Collect information about versions we could install using
221 the distribution's package manager. On success, the distribution
222 feed in iface_cache is updated.
223 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
224 if self
.packagekit
.available
:
225 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
226 return self
.packagekit
.fetch_candidates(package_names
)
229 def packagekit(self
):
230 """For use by subclasses.
231 @rtype: L{packagekit.PackageKit}"""
232 if not self
._packagekit
:
233 from zeroinstall
.injector
import packagekit
234 self
._packagekit
= packagekit
.PackageKit()
235 return self
._packagekit
237 class WindowsDistribution(Distribution
):
238 def get_package_info(self
, package
, factory
):
239 def _is_64bit_windows():
241 from win32process
import IsWow64Process
242 if p
== 'win64' or (p
== 'win32' and IsWow64Process()): return True
243 elif p
== 'win32': return False
244 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
246 def _read_hklm_reg(key_name
, value_name
):
247 from win32api
import RegOpenKeyEx
, RegQueryValueEx
, RegCloseKey
248 from win32con
import HKEY_LOCAL_MACHINE
, KEY_READ
249 KEY_WOW64_64KEY
= 0x0100
250 KEY_WOW64_32KEY
= 0x0200
251 if _is_64bit_windows():
253 key32
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ | KEY_WOW64_32KEY
)
254 (value32
, _
) = RegQueryValueEx(key32
, value_name
)
259 key64
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ | KEY_WOW64_64KEY
)
260 (value64
, _
) = RegQueryValueEx(key64
, value_name
)
266 key32
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ
)
267 (value32
, _
) = RegQueryValueEx(key32
, value_name
)
272 return (value32
, value64
)
274 def find_java(part
, win_version
, zero_version
):
275 reg_path
= r
"SOFTWARE\JavaSoft\{part}\{win_version}".format(part
= part
, win_version
= win_version
)
276 (java32_home
, java64_home
) = _read_hklm_reg(reg_path
, "JavaHome")
278 for (home
, arch
) in [(java32_home
, 'i486'),
279 (java64_home
, 'x86_64')]:
280 if os
.path
.isfile(home
+ r
"\bin\java.exe"):
281 impl
= factory('package:windows:%s:%s:%s' % (package
, zero_version
, arch
))
283 impl
.version
= model
.parse_version(zero_version
)
284 impl
.upstream_stability
= model
.packaged
285 impl
.main
= home
+ r
"\bin\java.exe"
287 if package
== 'openjdk-6-jre':
288 find_java("Java Runtime Environment", "1.6", '6')
289 elif package
== 'openjdk-6-jdk':
290 find_java("Java Development Kit", "1.6", '6')
291 elif package
== 'openjdk-7-jre':
292 find_java("Java Runtime Environment", "1.7", '7')
293 elif package
== 'openjdk-7-jdk':
294 find_java("Java Development Kit", "1.7", '7')
296 def get_score(self
, disto_name
):
297 return int(disto_name
== 'Windows')
299 class DarwinDistribution(Distribution
):
300 def get_package_info(self
, package
, factory
):
301 def java_home(version
, arch
):
302 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
303 child
= subprocess
.Popen(["/usr/libexec/java_home", "--failfast", "--version", version
, "--arch", arch
],
304 stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
305 home
= child
.stdout
.read().strip()
310 def find_java(part
, jvm_version
, zero_version
):
311 for arch
in ['i386', 'x86_64']:
312 home
= java_home(jvm_version
, arch
)
313 if os
.path
.isfile(home
+ "/bin/java"):
314 impl
= factory('package:darwin:%s:%s:%s' % (package
, zero_version
, arch
))
316 impl
.version
= model
.parse_version(zero_version
)
317 impl
.upstream_stability
= model
.packaged
318 impl
.main
= home
+ "/bin/java"
320 if package
== 'openjdk-6-jre':
321 find_java("Java Runtime Environment", "1.6", '6')
322 elif package
== 'openjdk-6-jdk':
323 find_java("Java Development Kit", "1.6", '6')
324 elif package
== 'openjdk-7-jre':
325 find_java("Java Runtime Environment", "1.7", '7')
326 elif package
== 'openjdk-7-jdk':
327 find_java("Java Development Kit", "1.7", '7')
329 def get_score(self
, disto_name
):
330 return int(disto_name
== 'Darwin')
332 class CachedDistribution(Distribution
):
333 """For distributions where querying the package database is slow (e.g. requires running
334 an external command), we cache the results.
336 @deprecated: use Cache instead
339 def __init__(self
, db_status_file
):
340 """@param db_status_file: update the cache when the timestamp of this file changes"""
341 self
._status
_details
= os
.stat(db_status_file
)
344 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
345 namespaces
.config_prog
)
349 except Exception as ex
:
350 info(_("Failed to load distribution database cache (%s). Regenerating..."), ex
)
352 self
.generate_cache()
354 except Exception as ex
:
355 warn(_("Failed to regenerate distribution database cache: %s"), ex
)
357 def _load_cache(self
):
358 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
359 Throws an exception if the cache should be (re)created."""
360 with
open(os
.path
.join(self
.cache_dir
, self
.cache_leaf
), 'rt') as stream
:
365 name
, value
= line
.split(': ')
366 if name
== 'mtime' and int(value
) != int(self
._status
_details
.st_mtime
):
367 raise Exception(_("Modification time of package database file has changed"))
368 if name
== 'size' and int(value
) != self
._status
_details
.st_size
:
369 raise Exception(_("Size of package database file has changed"))
370 if name
== 'version':
371 cache_version
= int(value
)
373 raise Exception(_('Invalid cache format (bad header)'))
375 if cache_version
is None:
376 raise Exception(_('Old cache format'))
378 versions
= self
.versions
380 package
, version
, zi_arch
= line
[:-1].split('\t')
381 versionarch
= (version
, intern(zi_arch
))
382 if package
not in versions
:
383 versions
[package
] = [versionarch
]
385 versions
[package
].append(versionarch
)
387 def _write_cache(self
, cache
):
388 #cache.sort() # Might be useful later; currently we don't care
390 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'zeroinstall-cache-tmp',
391 dir = self
.cache_dir
)
393 stream
= os
.fdopen(fd
, 'wt')
394 stream
.write('version: 2\n')
395 stream
.write('mtime: %d\n' % int(self
._status
_details
.st_mtime
))
396 stream
.write('size: %d\n' % self
._status
_details
.st_size
)
399 stream
.write(line
+ '\n')
402 portable_rename(tmpname
,
403 os
.path
.join(self
.cache_dir
,
409 # Maps machine type names used in packages to their Zero Install versions
410 _canonical_machine
= {
425 host_machine
= arch
.canonicalize_machine(platform
.uname()[4])
426 def canonical_machine(package_machine
):
427 machine
= _canonical_machine
.get(package_machine
, None)
429 # Safe default if we can't understand the arch
433 class DebianDistribution(Distribution
):
434 """A dpkg-based distribution."""
436 cache_leaf
= 'dpkg-status.cache'
438 def __init__(self
, dpkg_status
):
439 self
.dpkg_cache
= Cache('dpkg-status.cache', dpkg_status
, 2)
442 def _query_installed_package(self
, package
):
443 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
444 child
= subprocess
.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package
],
445 stdout
= subprocess
.PIPE
, stderr
= null
,
446 universal_newlines
= True) # Needed for Python 3
448 stdout
, stderr
= child
.communicate()
450 for line
in stdout
.split('\n'):
451 if not line
: continue
452 version
, debarch
, status
= line
.split('\t', 2)
453 if not status
.endswith(' installed'): continue
454 clean_version
= try_cleanup_distro_version(version
)
455 if debarch
.find("-") != -1:
456 debarch
= debarch
.split("-")[-1]
458 return '%s\t%s' % (clean_version
, canonical_machine(debarch
.strip()))
460 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
464 def get_package_info(self
, package
, factory
):
465 # Add any already-installed package...
466 installed_cached_info
= self
._get
_dpkg
_info
(package
)
468 if installed_cached_info
!= '-':
469 installed_version
, machine
= installed_cached_info
.split('\t')
470 impl
= factory('package:deb:%s:%s:%s' % (package
, installed_version
, machine
))
471 impl
.version
= model
.parse_version(installed_version
)
473 impl
.machine
= machine
475 installed_version
= None
477 # Add any uninstalled candidates (note: only one of these two methods will add anything)
480 self
.packagekit
.get_candidates(package
, factory
, 'package:deb')
483 cached
= self
.apt_cache
.get(package
, None)
485 candidate_version
= cached
['version']
486 candidate_arch
= cached
['arch']
487 if candidate_version
and candidate_version
!= installed_version
:
488 impl
= factory('package:deb:%s:%s:%s' % (package
, candidate_version
, candidate_arch
), installed
= False)
489 impl
.version
= model
.parse_version(candidate_version
)
490 if candidate_arch
!= '*':
491 impl
.machine
= candidate_arch
492 def install(handler
):
493 raise model
.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
494 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
495 "use that to install it.") % package
)
496 impl
.download_sources
.append(model
.DistributionSource(package
, cached
['size'], install
, needs_confirmation
= False))
498 def get_score(self
, disto_name
):
499 return int(disto_name
== 'Debian')
501 def _get_dpkg_info(self
, package
):
502 installed_cached_info
= self
.dpkg_cache
.get(package
)
503 if installed_cached_info
== None:
504 installed_cached_info
= self
._query
_installed
_package
(package
)
505 self
.dpkg_cache
.put(package
, installed_cached_info
)
507 return installed_cached_info
509 def fetch_candidates(self
, master_feed
):
510 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
512 if self
.packagekit
.available
:
513 return self
.packagekit
.fetch_candidates(package_names
)
515 # No PackageKit. Use apt-cache directly.
516 for package
in package_names
:
517 # Check to see whether we could get a newer version using apt-get
519 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
520 child
= subprocess
.Popen(['apt-cache', 'show', '--no-all-versions', '--', package
], stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
523 arch
= version
= size
= None
524 for line
in child
.stdout
:
526 if line
.startswith('Version: '):
528 version
= try_cleanup_distro_version(version
)
529 elif line
.startswith('Architecture: '):
530 arch
= canonical_machine(line
[14:].strip())
531 elif line
.startswith('Size: '):
532 size
= int(line
[6:].strip())
534 cached
= {'version': version
, 'arch': arch
, 'size': size
}
539 except Exception as ex
:
540 warn("'apt-cache show %s' failed: %s", package
, ex
)
542 # (multi-arch support? can there be multiple candidates?)
543 self
.apt_cache
[package
] = cached
545 class RPMDistribution(CachedDistribution
):
546 """An RPM-based distribution."""
548 cache_leaf
= 'rpm-status.cache'
550 def generate_cache(self
):
553 child
= subprocess
.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
554 stdout
= subprocess
.PIPE
, universal_newlines
= True)
555 for line
in child
.stdout
:
556 package
, version
, rpmarch
= line
.split('\t', 2)
557 if package
== 'gpg-pubkey':
559 zi_arch
= canonical_machine(rpmarch
.strip())
560 clean_version
= try_cleanup_distro_version(version
)
562 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
564 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
566 self
._write
_cache
(cache
)
570 def get_package_info(self
, package
, factory
):
571 # Add installed versions...
572 versions
= self
.versions
.get(package
, [])
574 for version
, machine
in versions
:
575 impl
= factory('package:rpm:%s:%s:%s' % (package
, version
, machine
))
576 impl
.version
= model
.parse_version(version
)
578 impl
.machine
= machine
580 # Add any uninstalled candidates found by PackageKit
581 self
.packagekit
.get_candidates(package
, factory
, 'package:rpm')
583 def get_score(self
, disto_name
):
584 return int(disto_name
== 'RPM')
586 class SlackDistribution(Distribution
):
587 """A Slack-based distribution."""
589 def __init__(self
, packages_dir
):
590 self
._packages
_dir
= packages_dir
592 def get_package_info(self
, package
, factory
):
593 # Add installed versions...
594 for entry
in os
.listdir(self
._packages
_dir
):
595 name
, version
, arch
, build
= entry
.rsplit('-', 3)
597 zi_arch
= canonical_machine(arch
)
598 clean_version
= try_cleanup_distro_version("%s-%s" % (version
, build
))
599 if not clean_version
:
600 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': name
})
603 impl
= factory('package:slack:%s:%s:%s' % \
604 (package
, clean_version
, zi_arch
))
605 impl
.version
= model
.parse_version(clean_version
)
607 impl
.machine
= zi_arch
609 # Add any uninstalled candidates found by PackageKit
610 self
.packagekit
.get_candidates(package
, factory
, 'package:slack')
612 def get_score(self
, disto_name
):
613 return int(disto_name
== 'Slack')
615 class ArchDistribution(Distribution
):
616 """An Arch Linux distribution."""
618 def __init__(self
, packages_dir
):
619 self
._packages
_dir
= os
.path
.join(packages_dir
, "local")
621 def get_package_info(self
, package
, factory
):
622 # Add installed versions...
623 for entry
in os
.listdir(self
._packages
_dir
):
624 name
, version
, build
= entry
.rsplit('-', 2)
627 with
open(os
.path
.join(self
._packages
_dir
, entry
, "desc"), 'rt') as stream
:
629 if line
== "%ARCH%\n":
635 zi_arch
= canonical_machine(arch
)
636 clean_version
= try_cleanup_distro_version("%s-%s" % (version
, build
))
637 if not clean_version
:
638 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': name
})
641 impl
= factory('package:arch:%s:%s:%s' % \
642 (package
, clean_version
, zi_arch
))
643 impl
.version
= model
.parse_version(clean_version
)
645 impl
.machine
= zi_arch
647 # Add any uninstalled candidates found by PackageKit
648 self
.packagekit
.get_candidates(package
, factory
, 'package:arch')
650 def get_score(self
, disto_name
):
651 return int(disto_name
== 'Arch')
653 class GentooDistribution(Distribution
):
655 def __init__(self
, pkgdir
):
656 self
._pkgdir
= pkgdir
658 def get_package_info(self
, package
, factory
):
659 # Add installed versions...
660 _version_start_reqexp
= '-[0-9]'
662 if package
.count('/') != 1: return
664 category
, leafname
= package
.split('/')
665 category_dir
= os
.path
.join(self
._pkgdir
, category
)
666 match_prefix
= leafname
+ '-'
668 if not os
.path
.isdir(category_dir
): return
670 for filename
in os
.listdir(category_dir
):
671 if filename
.startswith(match_prefix
) and filename
[len(match_prefix
)].isdigit():
672 with
open(os
.path
.join(category_dir
, filename
, 'PF'), 'rt') as stream
:
673 name
= stream
.readline().strip()
675 match
= re
.search(_version_start_reqexp
, name
)
677 warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name
})
680 version
= try_cleanup_distro_version(name
[match
.start() + 1:])
682 if category
== 'app-emulation' and name
.startswith('emul-'):
683 __
, __
, machine
, __
= name
.split('-', 3)
685 with
open(os
.path
.join(category_dir
, filename
, 'CHOST'), 'rt') as stream
:
686 machine
, __
= stream
.readline().split('-', 1)
687 machine
= arch
.canonicalize_machine(machine
)
689 impl
= factory('package:gentoo:%s:%s:%s' % \
690 (package
, version
, machine
))
691 impl
.version
= model
.parse_version(version
)
692 impl
.machine
= machine
694 # Add any uninstalled candidates found by PackageKit
695 self
.packagekit
.get_candidates(package
, factory
, 'package:gentoo')
697 def get_score(self
, disto_name
):
698 return int(disto_name
== 'Gentoo')
700 class PortsDistribution(Distribution
):
702 def __init__(self
, pkgdir
):
703 self
._pkgdir
= pkgdir
705 def get_package_info(self
, package
, factory
):
706 _name_version_regexp
= '^(.+)-([^-]+)$'
708 nameversion
= re
.compile(_name_version_regexp
)
709 for pkgname
in os
.listdir(self
._pkgdir
):
710 pkgdir
= os
.path
.join(self
._pkgdir
, pkgname
)
711 if not os
.path
.isdir(pkgdir
): continue
713 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
715 match
= nameversion
.search(pkgname
)
717 warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname
})
720 name
= match
.group(1)
723 version
= try_cleanup_distro_version(match
.group(2))
725 machine
= host_machine
727 impl
= factory('package:ports:%s:%s:%s' % \
728 (package
, version
, machine
))
729 impl
.version
= model
.parse_version(version
)
730 impl
.machine
= machine
732 def get_score(self
, disto_name
):
733 return int(disto_name
== 'Ports')
735 class MacPortsDistribution(CachedDistribution
):
737 cache_leaf
= 'macports-status.cache'
739 def generate_cache(self
):
742 child
= subprocess
.Popen(["port", "-v", "installed"],
743 stdout
= subprocess
.PIPE
, universal_newlines
= True)
744 for line
in child
.stdout
:
745 if not line
.startswith(" "):
747 if line
.strip().count(" ") > 1:
748 package
, version
, extra
= line
.split(None, 2)
750 package
, version
= line
.split()
752 if not extra
.startswith("(active)"):
754 version
= version
.lstrip('@')
755 version
= re
.sub(r
"\+.*", "", version
) # strip variants
757 clean_version
= try_cleanup_distro_version(version
)
759 match
= re
.match(r
" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra
)
761 platform
, major
, archs
= match
.groups()
762 for arch
in archs
.split():
763 zi_arch
= canonical_machine(arch
)
764 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
766 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
768 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
769 self
._write
_cache
(cache
)
773 def get_package_info(self
, package
, factory
):
774 # Add installed versions...
775 versions
= self
.versions
.get(package
, [])
777 for version
, machine
in versions
:
778 impl
= factory('package:macports:%s:%s:%s' % (package
, version
, machine
))
779 impl
.version
= model
.parse_version(version
)
781 impl
.machine
= machine
783 def get_score(self
, disto_name
):
784 return int(disto_name
== 'MacPorts')
786 class CygwinDistribution(CachedDistribution
):
787 """A Cygwin-based distribution."""
789 cache_leaf
= 'cygcheck-status.cache'
791 def generate_cache(self
):
794 zi_arch
= canonical_machine(arch
)
795 for line
in os
.popen("cygcheck -c -d"):
796 if line
== "Cygwin Package Information\r\n":
800 package
, version
= line
.split()
801 if package
== "Package" and version
== "Version":
803 clean_version
= try_cleanup_distro_version(version
)
805 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
807 warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
809 self
._write
_cache
(cache
)
811 def get_package_info(self
, package
, factory
):
812 # Add installed versions...
813 versions
= self
.versions
.get(package
, [])
815 for version
, machine
in versions
:
816 impl
= factory('package:cygwin:%s:%s:%s' % (package
, version
, machine
))
817 impl
.version
= model
.parse_version(version
)
819 impl
.machine
= machine
821 def get_score(self
, disto_name
):
822 return int(disto_name
== 'Cygwin')
825 _host_distribution
= None
826 def get_host_distribution():
827 """Get a Distribution suitable for the host operating system.
828 Calling this twice will return the same object.
829 @rtype: L{Distribution}"""
830 global _host_distribution
831 if not _host_distribution
:
832 dpkg_db_status
= '/var/lib/dpkg/status'
833 rpm_db_packages
= '/var/lib/rpm/Packages'
834 _slack_db
= '/var/log/packages'
835 _arch_db
= '/var/lib/pacman'
836 _pkg_db
= '/var/db/pkg'
837 _macports_db
= '/opt/local/var/macports/registry/registry.db'
838 _cygwin_log
= '/var/log/setup.log'
840 if sys
.prefix
== "/sw":
841 dpkg_db_status
= os
.path
.join(sys
.prefix
, dpkg_db_status
)
842 rpm_db_packages
= os
.path
.join(sys
.prefix
, rpm_db_packages
)
845 _host_distribution
= WindowsDistribution()
846 elif os
.path
.isdir(_pkg_db
):
847 if sys
.platform
.startswith("linux"):
848 _host_distribution
= GentooDistribution(_pkg_db
)
849 elif sys
.platform
.startswith("freebsd"):
850 _host_distribution
= PortsDistribution(_pkg_db
)
851 elif os
.path
.isfile(_macports_db
) \
852 and sys
.prefix
.startswith("/opt/local"):
853 _host_distribution
= MacPortsDistribution(_macports_db
)
854 elif os
.path
.isfile(_cygwin_log
) and sys
.platform
== "cygwin":
855 _host_distribution
= CygwinDistribution(_cygwin_log
)
856 elif os
.access(dpkg_db_status
, os
.R_OK
) \
857 and os
.path
.getsize(dpkg_db_status
) > 0:
858 _host_distribution
= DebianDistribution(dpkg_db_status
)
859 elif os
.path
.isfile(rpm_db_packages
):
860 _host_distribution
= RPMDistribution(rpm_db_packages
)
861 elif os
.path
.isdir(_slack_db
):
862 _host_distribution
= SlackDistribution(_slack_db
)
863 elif os
.path
.isdir(_arch_db
):
864 _host_distribution
= ArchDistribution(_arch_db
)
865 elif sys
.platform
== "darwin":
866 _host_distribution
= DarwinDistribution()
868 _host_distribution
= Distribution()
870 return _host_distribution