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 if suffix
.startswith('pre'):
120 suffix
= '-pre' + (try_cleanup_distro_version(suffix
) or '')
123 match
= re
.match(_version_regexp
, version
)
125 major
, version
, revision
= match
.groups()
126 if major
is not None:
127 version
= major
[:-1] + '.' + version
128 if revision
is not None:
129 version
= '%s-%s' % (version
, revision
[2:])
130 return version
+ suffix
133 class Distribution(object):
134 """Represents a distribution with which we can integrate.
135 Sub-classes should specialise this to integrate with the package managers of
136 particular distributions. This base class ignores the native package manager.
141 def get_package_info(self
, package
, factory
):
142 """Get information about the given package.
143 Add zero or more implementations using the factory (typically at most two
144 will be added; the currently installed version and the latest available).
145 @param package: package name (e.g. "gimp")
147 @param factory: function for creating new DistributionImplementation objects from IDs
148 @type factory: str -> L{model.DistributionImplementation}
152 def get_score(self
, distribution
):
153 """Indicate how closely the host distribution matches this one.
154 The <package-implementation> with the highest score is passed
155 to L{Distribution.get_package_info}. If several elements get
156 the same score, get_package_info is called for all of them.
157 @param distribution: a distribution name
158 @type distribution: str
159 @return: an integer, or -1 if there is no match at all
164 def get_feed(self
, master_feed
):
165 """Generate a feed containing information about distribution packages.
166 This should immediately return a feed containing an implementation for the
167 package if it's already installed. Information about versions that could be
168 installed using the distribution's package manager can be added asynchronously
169 later (see L{fetch_candidates}).
170 @param master_feed: feed containing the <package-implementation> elements
171 @type master_feed: L{model.ZeroInstallFeed}
172 @rtype: L{model.ZeroInstallFeed}"""
174 feed
= model
.ZeroInstallFeed(None)
175 feed
.url
= 'distribution:' + master_feed
.url
177 for item
, item_attrs
in master_feed
.get_package_impls(self
):
178 package
= item_attrs
.get('package', None)
180 raise model
.InvalidInterface(_("Missing 'package' attribute on %s") % item
)
184 def factory(id, only_if_missing
= False, installed
= True):
185 assert id.startswith('package:')
186 if id in feed
.implementations
:
189 logger
.warn(_("Duplicate ID '%s' for DistributionImplementation"), id)
190 impl
= model
.DistributionImplementation(feed
, id, self
, item
)
191 feed
.implementations
[id] = impl
192 new_impls
.append(impl
)
194 impl
.installed
= installed
195 impl
.metadata
= item_attrs
197 if 'run' not in impl
.commands
:
198 item_main
= item_attrs
.get('main', None)
200 if item_main
.startswith('/'):
201 impl
.main
= item_main
203 raise model
.InvalidInterface(_("'main' attribute must be absolute, but '%s' doesn't start with '/'!") %
205 impl
.upstream_stability
= model
.packaged
209 self
.get_package_info(package
, factory
)
211 for impl
in new_impls
:
212 self
.fixup(package
, impl
)
214 self
.installed_fixup(impl
)
217 def fetch_candidates(self
, master_feed
):
218 """Collect information about versions we could install using
219 the distribution's package manager. On success, the distribution
220 feed in iface_cache is updated.
221 @return: a L{tasks.Blocker} if the task is in progress, or None if not"""
222 if self
.packagekit
.available
:
223 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
224 return self
.packagekit
.fetch_candidates(package_names
)
227 def packagekit(self
):
228 """For use by subclasses.
229 @rtype: L{packagekit.PackageKit}"""
230 if not self
._packagekit
:
231 from zeroinstall
.injector
import packagekit
232 self
._packagekit
= packagekit
.PackageKit()
233 return self
._packagekit
235 def fixup(self
, package
, impl
):
236 """Some packages require special handling (e.g. Java). This is called for each
237 package that was added by L{get_package_info} after it returns. The default
239 @param package: the name of the package
240 @param impl: the constructed implementation"""
243 def installed_fixup(self
, impl
):
244 """Called when an installed package is added (after L{fixup}), or when installation
245 completes. This is useful to fix up the main value.
246 @type impl: L{DistributionImplementation}
250 class WindowsDistribution(Distribution
):
251 def get_package_info(self
, package
, factory
):
252 def _is_64bit_windows():
254 from win32process
import IsWow64Process
255 if p
== 'win64' or (p
== 'win32' and IsWow64Process()): return True
256 elif p
== 'win32': return False
257 else: raise Exception(_("WindowsDistribution may only be used on the Windows platform"))
259 def _read_hklm_reg(key_name
, value_name
):
260 from win32api
import RegOpenKeyEx
, RegQueryValueEx
, RegCloseKey
261 from win32con
import HKEY_LOCAL_MACHINE
, KEY_READ
262 KEY_WOW64_64KEY
= 0x0100
263 KEY_WOW64_32KEY
= 0x0200
264 if _is_64bit_windows():
266 key32
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ | KEY_WOW64_32KEY
)
267 (value32
, _
) = RegQueryValueEx(key32
, value_name
)
272 key64
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ | KEY_WOW64_64KEY
)
273 (value64
, _
) = RegQueryValueEx(key64
, value_name
)
279 key32
= RegOpenKeyEx(HKEY_LOCAL_MACHINE
, key_name
, 0, KEY_READ
)
280 (value32
, _
) = RegQueryValueEx(key32
, value_name
)
285 return (value32
, value64
)
287 def find_java(part
, win_version
, zero_version
):
288 reg_path
= r
"SOFTWARE\JavaSoft\{part}\{win_version}".format(part
= part
, win_version
= win_version
)
289 (java32_home
, java64_home
) = _read_hklm_reg(reg_path
, "JavaHome")
291 for (home
, arch
) in [(java32_home
, 'i486'),
292 (java64_home
, 'x86_64')]:
293 if os
.path
.isfile(home
+ r
"\bin\java.exe"):
294 impl
= factory('package:windows:%s:%s:%s' % (package
, zero_version
, arch
))
296 impl
.version
= model
.parse_version(zero_version
)
297 impl
.upstream_stability
= model
.packaged
298 impl
.main
= home
+ r
"\bin\java.exe"
300 if package
== 'openjdk-6-jre':
301 find_java("Java Runtime Environment", "1.6", '6')
302 elif package
== 'openjdk-6-jdk':
303 find_java("Java Development Kit", "1.6", '6')
304 elif package
== 'openjdk-7-jre':
305 find_java("Java Runtime Environment", "1.7", '7')
306 elif package
== 'openjdk-7-jdk':
307 find_java("Java Development Kit", "1.7", '7')
309 def get_score(self
, disto_name
):
310 return int(disto_name
== 'Windows')
312 class DarwinDistribution(Distribution
):
313 def get_package_info(self
, package
, factory
):
314 def java_home(version
, arch
):
315 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
316 child
= subprocess
.Popen(["/usr/libexec/java_home", "--failfast", "--version", version
, "--arch", arch
],
317 stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
318 home
= child
.stdout
.read().strip()
323 def find_java(part
, jvm_version
, zero_version
):
324 for arch
in ['i386', 'x86_64']:
325 home
= java_home(jvm_version
, arch
)
326 if os
.path
.isfile(home
+ "/bin/java"):
327 impl
= factory('package:darwin:%s:%s:%s' % (package
, zero_version
, arch
))
329 impl
.version
= model
.parse_version(zero_version
)
330 impl
.upstream_stability
= model
.packaged
331 impl
.main
= home
+ "/bin/java"
333 if package
== 'openjdk-6-jre':
334 find_java("Java Runtime Environment", "1.6", '6')
335 elif package
== 'openjdk-6-jdk':
336 find_java("Java Development Kit", "1.6", '6')
337 elif package
== 'openjdk-7-jre':
338 find_java("Java Runtime Environment", "1.7", '7')
339 elif package
== 'openjdk-7-jdk':
340 find_java("Java Development Kit", "1.7", '7')
342 def get_score(self
, disto_name
):
343 return int(disto_name
== 'Darwin')
345 class CachedDistribution(Distribution
):
346 """For distributions where querying the package database is slow (e.g. requires running
347 an external command), we cache the results.
349 @deprecated: use Cache instead
352 def __init__(self
, db_status_file
):
353 """@param db_status_file: update the cache when the timestamp of this file changes"""
354 self
._status
_details
= os
.stat(db_status_file
)
357 self
.cache_dir
= basedir
.save_cache_path(namespaces
.config_site
,
358 namespaces
.config_prog
)
362 except Exception as ex
:
363 logger
.info(_("Failed to load distribution database cache (%s). Regenerating..."), ex
)
365 self
.generate_cache()
367 except Exception as ex
:
368 logger
.warn(_("Failed to regenerate distribution database cache: %s"), ex
)
370 def _load_cache(self
):
371 """Load {cache_leaf} cache file into self.versions if it is available and up-to-date.
372 Throws an exception if the cache should be (re)created."""
373 with
open(os
.path
.join(self
.cache_dir
, self
.cache_leaf
), 'rt') as stream
:
378 name
, value
= line
.split(': ')
379 if name
== 'mtime' and int(value
) != int(self
._status
_details
.st_mtime
):
380 raise Exception(_("Modification time of package database file has changed"))
381 if name
== 'size' and int(value
) != self
._status
_details
.st_size
:
382 raise Exception(_("Size of package database file has changed"))
383 if name
== 'version':
384 cache_version
= int(value
)
386 raise Exception(_('Invalid cache format (bad header)'))
388 if cache_version
is None:
389 raise Exception(_('Old cache format'))
391 versions
= self
.versions
393 package
, version
, zi_arch
= line
[:-1].split('\t')
394 versionarch
= (version
, intern(zi_arch
))
395 if package
not in versions
:
396 versions
[package
] = [versionarch
]
398 versions
[package
].append(versionarch
)
400 def _write_cache(self
, cache
):
401 #cache.sort() # Might be useful later; currently we don't care
403 fd
, tmpname
= tempfile
.mkstemp(prefix
= 'zeroinstall-cache-tmp',
404 dir = self
.cache_dir
)
406 stream
= os
.fdopen(fd
, 'wt')
407 stream
.write('version: 2\n')
408 stream
.write('mtime: %d\n' % int(self
._status
_details
.st_mtime
))
409 stream
.write('size: %d\n' % self
._status
_details
.st_size
)
412 stream
.write(line
+ '\n')
415 portable_rename(tmpname
,
416 os
.path
.join(self
.cache_dir
,
422 # Maps machine type names used in packages to their Zero Install versions
423 # (updates to this might require changing the reverse Java mapping)
424 _canonical_machine
= {
439 host_machine
= arch
.canonicalize_machine(platform
.uname()[4])
440 def canonical_machine(package_machine
):
441 machine
= _canonical_machine
.get(package_machine
, None)
443 # Safe default if we can't understand the arch
447 class DebianDistribution(Distribution
):
448 """A dpkg-based distribution."""
450 cache_leaf
= 'dpkg-status.cache'
452 def __init__(self
, dpkg_status
):
453 self
.dpkg_cache
= Cache('dpkg-status.cache', dpkg_status
, 2)
456 def _query_installed_package(self
, package
):
457 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
458 child
= subprocess
.Popen(["dpkg-query", "-W", "--showformat=${Version}\t${Architecture}\t${Status}\n", "--", package
],
459 stdout
= subprocess
.PIPE
, stderr
= null
,
460 universal_newlines
= True) # Needed for Python 3
462 stdout
, stderr
= child
.communicate()
464 for line
in stdout
.split('\n'):
465 if not line
: continue
466 version
, debarch
, status
= line
.split('\t', 2)
467 if not status
.endswith(' installed'): continue
468 clean_version
= try_cleanup_distro_version(version
)
469 if debarch
.find("-") != -1:
470 debarch
= debarch
.split("-")[-1]
472 return '%s\t%s' % (clean_version
, canonical_machine(debarch
.strip()))
474 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
478 def get_package_info(self
, package
, factory
):
479 # Add any already-installed package...
480 installed_cached_info
= self
._get
_dpkg
_info
(package
)
482 if installed_cached_info
!= '-':
483 installed_version
, machine
= installed_cached_info
.split('\t')
484 impl
= factory('package:deb:%s:%s:%s' % (package
, installed_version
, machine
))
485 impl
.version
= model
.parse_version(installed_version
)
487 impl
.machine
= machine
489 installed_version
= None
491 # Add any uninstalled candidates (note: only one of these two methods will add anything)
494 self
.packagekit
.get_candidates(package
, factory
, 'package:deb')
497 cached
= self
.apt_cache
.get(package
, None)
499 candidate_version
= cached
['version']
500 candidate_arch
= cached
['arch']
501 if candidate_version
and candidate_version
!= installed_version
:
502 impl
= factory('package:deb:%s:%s:%s' % (package
, candidate_version
, candidate_arch
), installed
= False)
503 impl
.version
= model
.parse_version(candidate_version
)
504 if candidate_arch
!= '*':
505 impl
.machine
= candidate_arch
506 def install(handler
):
507 raise model
.SafeException(_("This program depends on '%s', which is a package that is available through your distribution. "
508 "Please install it manually using your distribution's tools and try again. Or, install 'packagekit' and I can "
509 "use that to install it.") % package
)
510 impl
.download_sources
.append(model
.DistributionSource(package
, cached
['size'], install
, needs_confirmation
= False))
512 def fixup(self
, package
, impl
):
513 if impl
.id.startswith('package:deb:openjdk-6-jre:') or \
514 impl
.id.startswith('package:deb:openjdk-7-jre:'):
515 # Debian marks all Java versions as pre-releases
516 # See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=685276
517 impl
.version
= model
.parse_version(impl
.get_version().replace('-pre', '.'))
519 def installed_fixup(self
, impl
):
520 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
521 if impl
.id.startswith('package:deb:openjdk-6-jre:'):
522 java_version
= '6-openjdk'
523 elif impl
.id.startswith('package:deb:openjdk-7-jre:'):
524 java_version
= '7-openjdk'
528 if impl
.machine
== 'x86_64':
531 java_arch
= impl
.machine
533 java_bin
= '/usr/lib/jvm/java-%s-%s/jre/bin/java' % (java_version
, java_arch
)
534 if not os
.path
.exists(java_bin
):
535 # Try without the arch...
536 java_bin
= '/usr/lib/jvm/java-%s/jre/bin/java' % java_version
537 if not os
.path
.exists(java_bin
):
538 logger
.info("Java binary not found (%s)", java_bin
)
539 if impl
.main
is None:
540 java_bin
= '/usr/bin/java'
544 impl
.commands
["run"] = model
.Command(qdom
.Element(namespaces
.XMLNS_IFACE
, 'command',
545 {'path': java_bin
, 'name': 'run'}), None)
547 def get_score(self
, disto_name
):
548 return int(disto_name
== 'Debian')
550 def _get_dpkg_info(self
, package
):
551 installed_cached_info
= self
.dpkg_cache
.get(package
)
552 if installed_cached_info
== None:
553 installed_cached_info
= self
._query
_installed
_package
(package
)
554 self
.dpkg_cache
.put(package
, installed_cached_info
)
556 return installed_cached_info
558 def fetch_candidates(self
, master_feed
):
559 package_names
= [item
.getAttribute("package") for item
, item_attrs
in master_feed
.get_package_impls(self
)]
561 if self
.packagekit
.available
:
562 return self
.packagekit
.fetch_candidates(package_names
)
564 # No PackageKit. Use apt-cache directly.
565 for package
in package_names
:
566 # Check to see whether we could get a newer version using apt-get
568 null
= os
.open(os
.devnull
, os
.O_WRONLY
)
569 child
= subprocess
.Popen(['apt-cache', 'show', '--no-all-versions', '--', package
], stdout
= subprocess
.PIPE
, stderr
= null
, universal_newlines
= True)
572 arch
= version
= size
= None
573 for line
in child
.stdout
:
575 if line
.startswith('Version: '):
577 version
= try_cleanup_distro_version(version
)
578 elif line
.startswith('Architecture: '):
579 arch
= canonical_machine(line
[14:].strip())
580 elif line
.startswith('Size: '):
581 size
= int(line
[6:].strip())
583 cached
= {'version': version
, 'arch': arch
, 'size': size
}
588 except Exception as ex
:
589 logger
.warn("'apt-cache show %s' failed: %s", package
, ex
)
591 # (multi-arch support? can there be multiple candidates?)
592 self
.apt_cache
[package
] = cached
594 class RPMDistribution(CachedDistribution
):
595 """An RPM-based distribution."""
597 cache_leaf
= 'rpm-status.cache'
599 def generate_cache(self
):
602 child
= subprocess
.Popen(["rpm", "-qa", "--qf=%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n"],
603 stdout
= subprocess
.PIPE
, universal_newlines
= True)
604 for line
in child
.stdout
:
605 package
, version
, rpmarch
= line
.split('\t', 2)
606 if package
== 'gpg-pubkey':
608 zi_arch
= canonical_machine(rpmarch
.strip())
609 clean_version
= try_cleanup_distro_version(version
)
611 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
613 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
615 self
._write
_cache
(cache
)
619 def get_package_info(self
, package
, factory
):
620 # Add installed versions...
621 versions
= self
.versions
.get(package
, [])
623 for version
, machine
in versions
:
624 impl
= factory('package:rpm:%s:%s:%s' % (package
, version
, machine
))
625 impl
.version
= model
.parse_version(version
)
627 impl
.machine
= machine
629 # Add any uninstalled candidates found by PackageKit
630 self
.packagekit
.get_candidates(package
, factory
, 'package:rpm')
632 def installed_fixup(self
, impl
):
633 # OpenSUSE uses _, Fedora uses .
634 impl_id
= impl
.id.replace('_', '.')
636 # Hack: If we added any Java implementations, find the corresponding JAVA_HOME...
638 if impl_id
.startswith('package:rpm:java-1.6.0-openjdk:'):
639 java_version
= '1.6.0-openjdk'
640 elif impl_id
.startswith('package:rpm:java-1.7.0-openjdk:'):
641 java_version
= '1.7.0-openjdk'
645 # On Fedora, unlike Debian, the arch is x86_64, not amd64
647 java_bin
= '/usr/lib/jvm/jre-%s.%s/bin/java' % (java_version
, impl
.machine
)
648 if not os
.path
.exists(java_bin
):
649 # Try without the arch...
650 java_bin
= '/usr/lib/jvm/jre-%s/bin/java' % java_version
651 if not os
.path
.exists(java_bin
):
652 logger
.info("Java binary not found (%s)", java_bin
)
653 if impl
.main
is None:
654 java_bin
= '/usr/bin/java'
658 impl
.commands
["run"] = model
.Command(qdom
.Element(namespaces
.XMLNS_IFACE
, 'command',
659 {'path': java_bin
, 'name': 'run'}), None)
661 def fixup(self
, package
, impl
):
662 # OpenSUSE uses _, Fedora uses .
663 package
= package
.replace('_', '.')
665 if package
in ('java-1.6.0-openjdk', 'java-1.7.0-openjdk',
666 'java-1.6.0-openjdk-devel', 'java-1.7.0-openjdk-devel'):
667 if impl
.version
[0][0] == 1:
668 # OpenSUSE uses 1.6 to mean 6
669 del impl
.version
[0][0]
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
):
824 def __init__(self
, db_status_file
):
825 super(MacPortsDistribution
, self
).__init
__(db_status_file
)
826 self
.darwin
= DarwinDistribution()
828 cache_leaf
= 'macports-status.cache'
830 def generate_cache(self
):
833 child
= subprocess
.Popen(["port", "-v", "installed"],
834 stdout
= subprocess
.PIPE
, universal_newlines
= True)
835 for line
in child
.stdout
:
836 if not line
.startswith(" "):
838 if line
.strip().count(" ") > 1:
839 package
, version
, extra
= line
.split(None, 2)
841 package
, version
= line
.split()
843 if not extra
.startswith("(active)"):
845 version
= version
.lstrip('@')
846 version
= re
.sub(r
"\+.*", "", version
) # strip variants
848 clean_version
= try_cleanup_distro_version(version
)
850 match
= re
.match(r
" platform='([^' ]*)( \d+)?' archs='([^']*)'", extra
)
852 platform
, major
, archs
= match
.groups()
853 for arch
in archs
.split():
854 zi_arch
= canonical_machine(arch
)
855 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
857 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
859 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
860 self
._write
_cache
(cache
)
864 def get_package_info(self
, package
, factory
):
865 self
.darwin
.get_package_info(package
, factory
)
867 # Add installed versions...
868 versions
= self
.versions
.get(package
, [])
870 for version
, machine
in versions
:
871 impl
= factory('package:macports:%s:%s:%s' % (package
, version
, machine
))
872 impl
.version
= model
.parse_version(version
)
874 impl
.machine
= machine
876 def get_score(self
, disto_name
):
877 # We support both sources of packages.
878 # In theory, we should route 'Darwin' package names to DarwinDistribution, and
879 # Mac Ports names to MacPortsDistribution. But since we only use Darwin for Java,
880 # having one object handle both is OK.
881 return int(disto_name
in ('Darwin', 'MacPorts'))
883 class CygwinDistribution(CachedDistribution
):
884 """A Cygwin-based distribution."""
886 cache_leaf
= 'cygcheck-status.cache'
888 def generate_cache(self
):
891 zi_arch
= canonical_machine(arch
)
892 for line
in os
.popen("cygcheck -c -d"):
893 if line
== "Cygwin Package Information\r\n":
897 package
, version
= line
.split()
898 if package
== "Package" and version
== "Version":
900 clean_version
= try_cleanup_distro_version(version
)
902 cache
.append('%s\t%s\t%s' % (package
, clean_version
, zi_arch
))
904 logger
.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version
, 'package': package
})
906 self
._write
_cache
(cache
)
908 def get_package_info(self
, package
, factory
):
909 # Add installed versions...
910 versions
= self
.versions
.get(package
, [])
912 for version
, machine
in versions
:
913 impl
= factory('package:cygwin:%s:%s:%s' % (package
, version
, machine
))
914 impl
.version
= model
.parse_version(version
)
916 impl
.machine
= machine
918 def get_score(self
, disto_name
):
919 return int(disto_name
== 'Cygwin')
922 _host_distribution
= None
923 def get_host_distribution():
924 """Get a Distribution suitable for the host operating system.
925 Calling this twice will return the same object.
926 @rtype: L{Distribution}"""
927 global _host_distribution
928 if not _host_distribution
:
929 dpkg_db_status
= '/var/lib/dpkg/status'
930 rpm_db_packages
= '/var/lib/rpm/Packages'
931 _slack_db
= '/var/log/packages'
932 _arch_db
= '/var/lib/pacman'
933 _pkg_db
= '/var/db/pkg'
934 _macports_db
= '/opt/local/var/macports/registry/registry.db'
935 _cygwin_log
= '/var/log/setup.log'
937 if sys
.prefix
== "/sw":
938 dpkg_db_status
= os
.path
.join(sys
.prefix
, dpkg_db_status
)
939 rpm_db_packages
= os
.path
.join(sys
.prefix
, rpm_db_packages
)
942 _host_distribution
= WindowsDistribution()
943 elif os
.path
.isdir(_pkg_db
):
944 if sys
.platform
.startswith("linux"):
945 _host_distribution
= GentooDistribution(_pkg_db
)
946 elif sys
.platform
.startswith("freebsd"):
947 _host_distribution
= PortsDistribution(_pkg_db
)
948 elif os
.path
.isfile(_macports_db
) \
949 and sys
.prefix
.startswith("/opt/local"):
950 _host_distribution
= MacPortsDistribution(_macports_db
)
951 elif os
.path
.isfile(_cygwin_log
) and sys
.platform
== "cygwin":
952 _host_distribution
= CygwinDistribution(_cygwin_log
)
953 elif os
.access(dpkg_db_status
, os
.R_OK
) \
954 and os
.path
.getsize(dpkg_db_status
) > 0:
955 _host_distribution
= DebianDistribution(dpkg_db_status
)
956 elif os
.path
.isfile(rpm_db_packages
):
957 _host_distribution
= RPMDistribution(rpm_db_packages
)
958 elif os
.path
.isdir(_slack_db
):
959 _host_distribution
= SlackDistribution(_slack_db
)
960 elif os
.path
.isdir(_arch_db
):
961 _host_distribution
= ArchDistribution(_arch_db
)
962 elif sys
.platform
== "darwin":
963 _host_distribution
= DarwinDistribution()
965 _host_distribution
= Distribution()
967 return _host_distribution