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 # Debian marks all Java versions as pre-releases
526 # See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=685276
527 impl
.version
= model
.parse_version(impl
.get_version().replace('-pre', '.'))
529 if impl
.machine
== 'x86_64':
532 java_arch
= impl
.machine
534 java_bin
= '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version
, java_arch
)
535 if not os
.path
.exists(java_bin
):
536 # Try without the arch...
537 java_bin
= '/usr/lib/jvm/java-%s/jre/bin/java' % java_version
538 if not os
.path
.exists(java_bin
):
539 logger
.info("Java binary not found (%s)", java_bin
)
540 if impl
.main
is None:
541 java_bin
= '/usr/bin/java'
545 impl
.commands
["run"] = model
.Command(qdom
.Element(namespaces
.XMLNS_IFACE
, 'command',
546 {'path': java_bin
, 'name': 'run'}), None)
548 def get_score(self
, disto_name
):
549 return int(disto_name
== 'Debian')
551 def _get_dpkg_info(self
, package
):
552 installed_cached_info
= self
.dpkg_cache
.get(package
)
553 if installed_cached_info
== None:
554 installed_cached_info
= self
._query
_installed
_package
(package
)
555 self
.dpkg_cache
.put(package
, installed_cached_info
)
557 return installed_cached_info
559 def fetch_candidates(self
, master_feed
):
560 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
562 if self
.packagekit
.available
:
563 return self
.packagekit
.fetch_candidates(package_names
)
565 # No PackageKit. Use apt-cache directly.
566 for package
in package_names
:
567 # Check to see whether we could get a newer version using apt-get
569 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
570 child
= subprocess
.Popen(['apt-cache', 'show', '--no-all-versions', '--', package
], stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
573 arch
= version
= size
= None
574 for line
in child
.stdout
:
576 if line
.startswith('Version: '):
578 version
= try_cleanup_distro_version(version
)
579 elif line
.startswith('Architecture: '):
580 arch
= canonical_machine(line
[14:].strip())
581 elif line
.startswith('Size: '):
582 size
= int(line
[6:].strip())
584 cached
= {'version': version
, 'arch': arch
, 'size': size
}
589 except Exception as ex
:
590 logger
.warn("'apt-cache show %s' failed: %s", package
, ex
)
592 # (multi-arch support? can there be multiple candidates?)
593 self
.apt_cache
[package
] = cached
595 class RPMDistribution(CachedDistribution
):
596 """An RPM-based distribution."""
598 cache_leaf
= 'rpm-status.cache'
600 def generate_cache(self
):
603 child
= subprocess
.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
604 stdout
= subprocess
.PIPE
, universal_newlines
= True)
605 for line
in child
.stdout
:
606 package
, version
, rpmarch
= line
.split('\t', 2)
607 if package
== 'gpg-pubkey':
609 zi_arch
= canonical_machine(rpmarch
.strip())
610 clean_version
= try_cleanup_distro_version(version
)
612 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
614 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
616 self
._write
_cache
(cache
)
620 def get_package_info(self
, package
, factory
):
621 # Add installed versions...
622 versions
= self
.versions
.get(package
, [])
624 for version
, machine
in versions
:
625 impl
= factory('package:rpm:%s:%s:%s' % (package
, version
, machine
))
626 impl
.version
= model
.parse_version(version
)
628 impl
.machine
= machine
630 # Add any uninstalled candidates found by PackageKit
631 self
.packagekit
.get_candidates(package
, factory
, 'package:rpm')
633 def fixup(self
, package
, impl
):
634 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
636 # OpenSUSE uses _, Fedora uses .
637 package
= package
.replace('_', '.')
639 if package
== 'java-1.6.0-openjdk':
640 java_version
= '1.6.0-openjdk'
641 elif package
== 'java-1.7.0-openjdk':
642 java_version
= '1.7.0-openjdk'
643 elif package
in ('java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'):
644 if impl
.version
[0][0] == 1:
645 # OpenSUSE uses 1.6 to mean 6
646 del impl
.version
[0][0]
651 if impl
.version
[0][0] == 1:
652 # OpenSUSE uses 1.6 to mean 6
653 del impl
.version
[0][0]
655 # On Fedora, unlike Debian, the arch is x86_64, not amd64
657 java_bin
= '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version
, impl
.machine
)
658 if not os
.path
.exists(java_bin
):
659 # Try without the arch...
660 java_bin
= '/usr/lib/jvm/jre-%s/bin/java' % java_version
661 if not os
.path
.exists(java_bin
):
662 logger
.info("Java binary not found (%s)", java_bin
)
663 if impl
.main
is None:
664 java_bin
= '/usr/bin/java'
668 impl
.commands
["run"] = model
.Command(qdom
.Element(namespaces
.XMLNS_IFACE
, 'command',
669 {'path': java_bin
, 'name': 'run'}), None)
671 def get_score(self
, disto_name
):
672 return int(disto_name
== 'RPM')
674 class SlackDistribution(Distribution
):
675 """A Slack-based distribution."""
677 def __init__(self
, packages_dir
):
678 self
._packages
_dir
= packages_dir
680 def get_package_info(self
, package
, factory
):
681 # Add installed versions...
682 for entry
in os
.listdir(self
._packages
_dir
):
683 name
, version
, arch
, build
= entry
.rsplit('-', 3)
685 zi_arch
= canonical_machine(arch
)
686 clean_version
= try_cleanup_distro_version("%s-%s" % (version
, build
))
687 if not clean_version
:
688 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': name
})
691 impl
= factory('package:slack:%s:%s:%s' % \
692 (package
, clean_version
, zi_arch
))
693 impl
.version
= model
.parse_version(clean_version
)
695 impl
.machine
= zi_arch
697 # Add any uninstalled candidates found by PackageKit
698 self
.packagekit
.get_candidates(package
, factory
, 'package:slack')
700 def get_score(self
, disto_name
):
701 return int(disto_name
== 'Slack')
703 class ArchDistribution(Distribution
):
704 """An Arch Linux distribution."""
706 def __init__(self
, packages_dir
):
707 self
._packages
_dir
= os
.path
.join(packages_dir
, "local")
709 def get_package_info(self
, package
, factory
):
710 # Add installed versions...
711 for entry
in os
.listdir(self
._packages
_dir
):
712 name
, version
, build
= entry
.rsplit('-', 2)
715 with
open(os
.path
.join(self
._packages
_dir
, entry
, "desc"), 'rt') as stream
:
717 if line
== "%ARCH%\n":
723 zi_arch
= canonical_machine(arch
)
724 clean_version
= try_cleanup_distro_version("%s-%s" % (version
, build
))
725 if not clean_version
:
726 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': name
})
729 impl
= factory('package:arch:%s:%s:%s' % \
730 (package
, clean_version
, zi_arch
))
731 impl
.version
= model
.parse_version(clean_version
)
733 impl
.machine
= zi_arch
735 # Add any uninstalled candidates found by PackageKit
736 self
.packagekit
.get_candidates(package
, factory
, 'package:arch')
738 def get_score(self
, disto_name
):
739 return int(disto_name
== 'Arch')
741 class GentooDistribution(Distribution
):
743 def __init__(self
, pkgdir
):
744 self
._pkgdir
= pkgdir
746 def get_package_info(self
, package
, factory
):
747 # Add installed versions...
748 _version_start_reqexp
= '-[0-9]'
750 if package
.count('/') != 1: return
752 category
, leafname
= package
.split('/')
753 category_dir
= os
.path
.join(self
._pkgdir
, category
)
754 match_prefix
= leafname
+ '-'
756 if not os
.path
.isdir(category_dir
): return
758 for filename
in os
.listdir(category_dir
):
759 if filename
.startswith(match_prefix
) and filename
[len(match_prefix
)].isdigit():
760 with
open(os
.path
.join(category_dir
, filename
, 'PF'), 'rt') as stream
:
761 name
= stream
.readline().strip()
763 match
= re
.search(_version_start_reqexp
, name
)
765 logger
.warn(_('Cannot parse version from Gentoo package named "%(name)s"'), {'name': name
})
768 version
= try_cleanup_distro_version(name
[match
.start() + 1:])
770 if category
== 'app-emulation' and name
.startswith('emul-'):
771 __
, __
, machine
, __
= name
.split('-', 3)
773 with
open(os
.path
.join(category_dir
, filename
, 'CHOST'), 'rt') as stream
:
774 machine
, __
= stream
.readline().split('-', 1)
775 machine
= arch
.canonicalize_machine(machine
)
777 impl
= factory('package:gentoo:%s:%s:%s' % \
778 (package
, version
, machine
))
779 impl
.version
= model
.parse_version(version
)
780 impl
.machine
= machine
782 # Add any uninstalled candidates found by PackageKit
783 self
.packagekit
.get_candidates(package
, factory
, 'package:gentoo')
785 def get_score(self
, disto_name
):
786 return int(disto_name
== 'Gentoo')
788 class PortsDistribution(Distribution
):
790 def __init__(self
, pkgdir
):
791 self
._pkgdir
= pkgdir
793 def get_package_info(self
, package
, factory
):
794 _name_version_regexp
= '^(.+)-([^-]+)$'
796 nameversion
= re
.compile(_name_version_regexp
)
797 for pkgname
in os
.listdir(self
._pkgdir
):
798 pkgdir
= os
.path
.join(self
._pkgdir
, pkgname
)
799 if not os
.path
.isdir(pkgdir
): continue
801 #contents = open(os.path.join(pkgdir, '+CONTENTS')).readline().strip()
803 match
= nameversion
.search(pkgname
)
805 logger
.warn(_('Cannot parse version from Ports package named "%(pkgname)s"'), {'pkgname': pkgname
})
808 name
= match
.group(1)
811 version
= try_cleanup_distro_version(match
.group(2))
813 machine
= host_machine
815 impl
= factory('package:ports:%s:%s:%s' % \
816 (package
, version
, machine
))
817 impl
.version
= model
.parse_version(version
)
818 impl
.machine
= machine
820 def get_score(self
, disto_name
):
821 return int(disto_name
== 'Ports')
823 class MacPortsDistribution(CachedDistribution
):
825 cache_leaf
= 'macports-status.cache'
827 def generate_cache(self
):
830 child
= subprocess
.Popen(["port", "-v", "installed"],
831 stdout
= subprocess
.PIPE
, universal_newlines
= True)
832 for line
in child
.stdout
:
833 if not line
.startswith(" "):
835 if line
.strip().count(" ") > 1:
836 package
, version
, extra
= line
.split(None, 2)
838 package
, version
= line
.split()
840 if not extra
.startswith("(active)"):
842 version
= version
.lstrip('@')
843 version
= re
.sub(r
"\+.*", "", version
) # strip variants
845 clean_version
= try_cleanup_distro_version(version
)
847 match
= re
.match(r
" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra
)
849 platform
, major
, archs
= match
.groups()
850 for arch
in archs
.split():
851 zi_arch
= canonical_machine(arch
)
852 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
854 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
856 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
857 self
._write
_cache
(cache
)
861 def get_package_info(self
, package
, factory
):
862 # Add installed versions...
863 versions
= self
.versions
.get(package
, [])
865 for version
, machine
in versions
:
866 impl
= factory('package:macports:%s:%s:%s' % (package
, version
, machine
))
867 impl
.version
= model
.parse_version(version
)
869 impl
.machine
= machine
871 def get_score(self
, disto_name
):
872 return int(disto_name
== 'MacPorts')
874 class CygwinDistribution(CachedDistribution
):
875 """A Cygwin-based distribution."""
877 cache_leaf
= 'cygcheck-status.cache'
879 def generate_cache(self
):
882 zi_arch
= canonical_machine(arch
)
883 for line
in os
.popen("cygcheck -c -d"):
884 if line
== "Cygwin Package Information\r\n":
888 package
, version
= line
.split()
889 if package
== "Package" and version
== "Version":
891 clean_version
= try_cleanup_distro_version(version
)
893 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
895 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
897 self
._write
_cache
(cache
)
899 def get_package_info(self
, package
, factory
):
900 # Add installed versions...
901 versions
= self
.versions
.get(package
, [])
903 for version
, machine
in versions
:
904 impl
= factory('package:cygwin:%s:%s:%s' % (package
, version
, machine
))
905 impl
.version
= model
.parse_version(version
)
907 impl
.machine
= machine
909 def get_score(self
, disto_name
):
910 return int(disto_name
== 'Cygwin')
913 _host_distribution
= None
914 def get_host_distribution():
915 """Get a Distribution suitable for the host operating system.
916 Calling this twice will return the same object.
917 @rtype: L{Distribution}"""
918 global _host_distribution
919 if not _host_distribution
:
920 dpkg_db_status
= '/var/lib/dpkg/status'
921 rpm_db_packages
= '/var/lib/rpm/Packages'
922 _slack_db
= '/var/log/packages'
923 _arch_db
= '/var/lib/pacman'
924 _pkg_db
= '/var/db/pkg'
925 _macports_db
= '/opt/local/var/macports/registry/registry.db'
926 _cygwin_log
= '/var/log/setup.log'
928 if sys
.prefix
== "/sw":
929 dpkg_db_status
= os
.path
.join(sys
.prefix
, dpkg_db_status
)
930 rpm_db_packages
= os
.path
.join(sys
.prefix
, rpm_db_packages
)
933 _host_distribution
= WindowsDistribution()
934 elif os
.path
.isdir(_pkg_db
):
935 if sys
.platform
.startswith("linux"):
936 _host_distribution
= GentooDistribution(_pkg_db
)
937 elif sys
.platform
.startswith("freebsd"):
938 _host_distribution
= PortsDistribution(_pkg_db
)
939 elif os
.path
.isfile(_macports_db
) \
940 and sys
.prefix
.startswith("/opt/local"):
941 _host_distribution
= MacPortsDistribution(_macports_db
)
942 elif os
.path
.isfile(_cygwin_log
) and sys
.platform
== "cygwin":
943 _host_distribution
= CygwinDistribution(_cygwin_log
)
944 elif os
.access(dpkg_db_status
, os
.R_OK
) \
945 and os
.path
.getsize(dpkg_db_status
) > 0:
946 _host_distribution
= DebianDistribution(dpkg_db_status
)
947 elif os
.path
.isfile(rpm_db_packages
):
948 _host_distribution
= RPMDistribution(rpm_db_packages
)
949 elif os
.path
.isdir(_slack_db
):
950 _host_distribution
= SlackDistribution(_slack_db
)
951 elif os
.path
.isdir(_arch_db
):
952 _host_distribution
= ArchDistribution(_arch_db
)
953 elif sys
.platform
== "darwin":
954 _host_distribution
= DarwinDistribution()
956 _host_distribution
= Distribution()
958 return _host_distribution