# (c) Copyright 2009-2012, 2015. CodeWeavers, Inc.
import os
import os.path
import re
import cxlog
import cxobjc
import cxutils
import distversion
# for localization
from cxutils import cxgettext as _
import bottlemanagement
import bottlequery
import c4profiles
import c4profilesmanager
import cddetector
import cxaiemedia
import cxdiag
import downloaddetector
#####
#
# Keep track of the installation SourceMedia and of their properties
#
#####
class SourceMedia(object):
"""Keeps track of installation wizard data associated to a potential
installation media.
"""
def __init__(self, installtask, path, label, show_untested_apps, device=''):
"""Source is the absolute path of the installation media. This normally
corresponds to the mount point of a CD or a directory, but it may also
point to a user-specified file.
The label is the string that should be used to describe that media.
"""
self.installtask = installtask
self.path = path
self.label = label
self.device = device
self._profile_ids = None
self.show_untested_apps = show_untested_apps
def _get_profile_ids(self):
"""Returns the list of profile ids that this media could correspond to
(in the form of a dictionary mapping the ids to the profile objects).
"""
if self._profile_ids is None:
if os.path.isdir(self.path):
self._profile_ids = cddetector.get_cd_profiles(self.path, self.installtask.profiles, self.show_untested_apps)
else:
self._profile_ids = {}
return self._profile_ids
profile_ids = property(_get_profile_ids)
#####
#
# Keep track of the target bottles and of their properties
#
#####
# Bottle categories
CAT_NONE = 0
CAT_RECOMMENDED = 1
CAT_COMPATIBLE = 2
CAT_INCOMPATIBLE = 3
# Reasons for an appid to be included the install
REASON_MAIN = 0
REASON_DEPENDENCY = 1
REASON_STEAM = 2
REASON_OVERRIDE = 3
# Dependency override types
OVERRIDE_EXCLUDE = 0
OVERRIDE_INCLUDE = 1
class TargetBottle(object):
#####
#
# Initialization
#
#####
def __init__(self, installtask, bottle):
self.installtask = installtask
self.bottle = bottle
if bottle:
self.bottlename = bottle.name
else:
self.bottlename = None
self._newtemplate = None
# The profile-related properties
# This should match check_cache()
self._profile = None
self._locale = None
self._category = CAT_NONE
self._installers = None
self._missing_media = None
self._missing_profiles = None
self._warnings = None
self._use_steam = False
self._dep_overrides = {}
self._sourcefile = None
self._sourcetype = None
self._reasons = None
def _gettemplate(self):
if self._newtemplate is None:
return self.bottle.bottletemplate
return self._newtemplate
template = property(_gettemplate)
#####
#
# Profile dependency analysis
#
#####
def _get_sourcetype(self):
# Possible values:
# dependency - this is a dependency of the main profile, so the
# sourcetype value should not be used (because the user has no
# opportunity to select the source until after the install starts, or
# maybe never).
# steam - installed from a steam id
# file - installed from a local or downloaded file
# cd - installed from a cd or other local directory
# unset - user has not yet selected an install source
if self.installtask.installWithSteam:
return 'steam'
if self.installtask.installerDownloadSource:
# We could report this as a "download", but it's probably bad for
# profiles to behave differently depending on whether you download
# the file or let CrossOver do it. When adding different
# distributors (bug 9162), we'll need to be careful to make sure the
# user can select the distributor for a local file.
return 'file'
if self.installtask.installerSource:
if os.path.isdir(self.installtask.installerSource):
return 'cd'
return 'file'
# We can't ever start an install with this, but if someone wants to
# set installnotes that apply only to file installs and not to cd
# installs, this will let them delay the notes until a source is
# chosen.
return 'unset'
def check_cache(self):
"""Checks whether the cached data is still relevant and throws it away
if not (typically because either the profile or the locale changed).
"""
# If further optimisation is needed we can check for the presence of
# useif or even scan its content before resetting the cache for a mere
# locale change.
if self._profile != self.installtask.profile or \
self._locale != self.installtask.realInstallerLocale or \
self._use_steam != self.installtask.installWithSteam or \
self._dep_overrides != self.installtask.dependency_overrides or \
self._sourcefile != self.installtask.installerSource or \
self._sourcetype != self._get_sourcetype():
self._profile = self.installtask.profile
self._use_steam = self.installtask.installWithSteam
self._dep_overrides = self.installtask.dependency_overrides.copy()
self._locale = self.installtask.realInstallerLocale
self._sourcefile = self.installtask.installerSource
self._sourcetype = self._get_sourcetype()
# Wipe everything out
self._category = CAT_NONE
self._installers = None
self._missing_media = None
self._missing_profiles = None
self._warnings = None
self._reasons = None
def analyzed(self):
"""Returns True if the bottle has been analyzed already and False
otherwise.
"""
self.check_cache()
return self._installers is not None
def _break_dependency_loops(self, appid, parents):
"""Detects, breaks and reports dependency loops."""
parents.add(appid)
to_remove = set()
for depid in self._installers[appid].pre_dependencies:
if depid in parents:
if not self.bottlename:
cxlog.warn("The dependency of %s on %s creates a loop in bottle template %s." % (cxlog.to_str(self.installtask.profiles[appid].name), cxlog.to_str(self.installtask.profiles[depid].name), self.template))
to_remove.add(depid)
elif depid in self._installers:
self._break_dependency_loops(depid, parents)
else:
# This dependency is either already installed, or it has no
# profile at all. Either way we should ignore it.
to_remove.add(depid)
if to_remove:
self._installers[appid].pre_dependencies -= to_remove
parents.remove(appid)
def _add_reason(self, appids, reason, other_appid=None):
for appid in appids:
if appid not in self._reasons:
self._reasons[appid] = (reason, other_appid)
def _is_predependency(self, dep_appid, appid):
"""Returns True if dep_appid must install before appid, must be called
after postdependencies are mapped to predependencies."""
todo = set([appid])
checked = set()
while todo:
check_appid = todo.pop()
if check_appid in checked or check_appid not in self._installers:
continue
checked.add(check_appid)
if dep_appid in self._installers[check_appid].pre_dependencies:
return True
todo.update(self._installers[check_appid].pre_dependencies)
return False
def analyze(self):
"""Analyzes the bottle so that we know all there is to know for
installation and about its suitability for the selected profile and
locale. This means:
- creating aggregated installer profiles for the selected profile and
all its dependencies,
- detecting and reporting any potential issue like dependency loops,
template incompatibilities, etc.
"""
if self.analyzed():
return True
if self.bottle:
if not self.bottle.get_installed_packages_ready():
# This bottle's installed applications list is not ready yet
# so we cannot perform the analysis. So return False so the
# caller knows he should try again later.
cxlog.log("analyze: %s is not ready" % cxlog.to_str(self.bottlename))
return False
installed_apps = self.bottle.installed_packages
else:
installed_apps = {}
profiles = self.installtask.profiles
self._installers = {}
self._missing_media = set()
self._missing_profiles = set()
self._warnings = []
self._reasons = {}
self.compatible = True
override_include_deps = set(appid for appid in self._dep_overrides if self._dep_overrides[appid] == OVERRIDE_INCLUDE)
incompatible_profiles = []
overrides = {}
dependencies = {}
todo = [self._profile.appid]
self._add_reason((self._profile.appid,), REASON_MAIN)
while todo or override_include_deps: # pylint: disable=R1702
if todo:
appid = todo.pop()
else:
appid = override_include_deps.pop()
self._add_reason((appid,), REASON_OVERRIDE)
if appid in self._installers or \
(appid != self._profile.appid and appid in installed_apps):
# We've done that one already. This check also lets us avoid
# infinite loops.
continue
if appid not in profiles or \
not profiles[appid].is_for_current_product:
# This dependency has no profile *at all* (not even a name),
# or it is for the wrong CrossOver product.
self._missing_profiles.add(appid)
continue
if self._dep_overrides.get(appid) == OVERRIDE_EXCLUDE:
continue
# Aggregate all the installer profile chunks into one installer
# profile.
installer = c4profiles.C4InstallerProfile()
nomatch = True
for inst_profile in profiles.installer_profiles(appid):
if inst_profile.appid is None:
use_if_props = self.get_use_if_properties(appid)
else:
use_if_props = self.get_use_if_properties(inst_profile.appid)
if inst_profile.use(use_if_props):
if inst_profile.appid is None or \
inst_profile.appid == appid:
installer.update(inst_profile)
nomatch = False
else:
# Remember the override information so we can apply it
# after we have built the main installer profile.
if inst_profile.appid not in overrides:
overrides[inst_profile.appid] = []
overrides[inst_profile.appid].append(inst_profile)
if inst_profile.appid in self._installers:
# We may have just discovered new dependencies for
# a profile we already handled.
todo.extend(inst_profile.pre_dependencies)
todo.extend(inst_profile.post_dependencies)
self._add_reason(inst_profile.pre_dependencies, REASON_DEPENDENCY, inst_profile.appid)
self._add_reason(inst_profile.post_dependencies, REASON_DEPENDENCY, inst_profile.appid)
else:
# Remember the extra dependencies for that profile
# so we take them into account if we run into it
# later on.
if inst_profile.appid not in dependencies:
dependencies[inst_profile.appid] = set()
dependencies[inst_profile.appid].update(inst_profile.pre_dependencies)
dependencies[inst_profile.appid].update(inst_profile.post_dependencies)
if nomatch:
# There is no installer profile so use the one for the unknown
# applications.
installer = profiles.unknown_installer().copy()
if self._use_steam and appid == self._profile.appid:
installer.pre_dependencies.add(u"com.codeweavers.c4.206")
todo.append(u"com.codeweavers.c4.206")
self._add_reason((u"com.codeweavers.c4.206",), REASON_STEAM)
installer.parent = profiles[appid]
self._installers[appid] = installer
# Check that bottle's template is compatible with this installer
if appid == self._profile.appid:
purpose = 'use'
else:
purpose = 'install'
if self.template not in profiles[appid].app_profile.bottle_types[purpose]:
incompatible_profiles.append(profiles[appid].name)
if appid != self._profile.appid and \
'virtual' not in profiles[appid].app_profile.flags and \
not cxaiemedia.get_builtin_installer(appid) and \
not profiles[appid].app_profile.download_urls:
if profiles[appid].app_profile.steamid:
# Use steam for dependency with steamid and no other install source
installer.pre_dependencies.add(u"com.codeweavers.c4.206")
todo.append(u"com.codeweavers.c4.206")
self._add_reason((u"com.codeweavers.c4.206",), REASON_DEPENDENCY, appid)
else:
# We cannot automatically download this dependency.
# The installation engine does not care, but still make a note
# of it for the GUI.
self._missing_media.add(appid)
# Schedule the dependencies for analysis
if appid not in dependencies:
dependencies[appid] = set()
dependencies[appid].update(installer.pre_dependencies)
dependencies[appid].update(installer.post_dependencies)
todo.extend(dependencies[appid])
self._add_reason(dependencies[appid], REASON_DEPENDENCY, appid)
if incompatible_profiles:
apps = ', '.join(sorted(incompatible_profiles))
if self.template in self._profile.app_profile.bottle_types['use']:
if not self.bottlename:
cxlog.warn("The %s template is compatible with %s but is incompatible with dependencies: %s" % (self.template, cxlog.to_str(self._profile.appid), cxlog.to_str(apps)))
else:
self.compatible = False
if self.bottlename:
self._warnings.append(_("You have selected the '%(bottlename)s' bottle, which is incompatible with %(applications)s.") %
{'bottlename': cxutils.html_escape(self.bottlename),
'applications': cxutils.html_escape(apps)})
else:
self._warnings.append(_("You have chosen to install in a new %(template)s bottle but it is incompatible with %(applications)s.") %
{'template': self.template,
'applications': cxutils.html_escape(apps)})
# Apply all the installer overrides so we don't have to worry about
# them later.
for appid, installer_profiles in overrides.iteritems():
if appid in self._installers:
for inst_profile in installer_profiles:
# Note that this may impact the pre_dependencies list, but
# we took precautions in the above loop so it won't
# reference new profiles.
installer = self._installers[appid]
installer.update(inst_profile)
for appid in self._installers:
installer = self._installers[appid]
for postdep_appid in installer.post_dependencies:
# If we have a post-dependency, we should make sure that it
# is not installed before this profile.
if postdep_appid in self._installers:
self._installers[postdep_appid].pre_dependencies.add(appid)
# Make sure that manually added profiles have a defined relationship to
# the main profile
for appid in self._installers:
if self._reasons.get(appid) == (REASON_OVERRIDE, None):
if self._is_predependency(self._profile.appid, appid) or \
self._profile.appid in self._installers[appid].parent.app_profile.extra_fors:
self._installers[appid].pre_dependencies.add(self._profile.appid)
self._installers[self._profile.appid].post_dependencies.add(appid)
else:
self._installers[self._profile.appid].pre_dependencies.add(appid)
# Break dependency loops
self._break_dependency_loops(self._profile.appid, set())
untrusted_profiles = []
for appid in self._installers:
if not self._installers[appid].parent.trusted:
untrusted_profiles.append(self._installers[appid].parent.name)
if untrusted_profiles:
untrusted = ', '.join(sorted(untrusted_profiles))
self._warnings.append(_("<span style='color: red;'>This installation will include profiles from untrusted sources</span>: %s.") % cxutils.html_escape(untrusted))
return True
def _getinstallers(self):
"""Returns the aggregated installer profiles for the application and
all its dependencies.
Note: Installation information can be split up across multiple
C4InstallerProfile objects which makes it unusable as is. This is
unlike the installer profiles in this map where all the pieces have
been 'aggregated' together into a single installer profile object.
"""
self.analyze()
return self._installers
installers = property(_getinstallers)
def _getmissing_media(self):
"""Returns the dependency application ids for which we cannot
automatically get an installer. The GUI will have to ask the user for
an installation source for these.
"""
self.analyze()
return self._missing_media
missing_media = property(_getmissing_media)
def _getmissing_profiles(self):
"""Returns the dependencies for which there is no C4 profile
information at all for the current product.
"""
self.analyze()
return self._missing_profiles
missing_profiles = property(_getmissing_profiles)
def _getwarnings(self):
"""Returns a list of localized strings describing the issues found
while analyzing this bottle.
This may be the presence of dependency loops for instance, missing
profiles, etc.
"""
self.analyze()
return self._warnings
warnings = property(_getwarnings)
def _getreasons(self):
"""Returns a dictionary mapping appids in the install to the reason
they will be installed, as a tuple of (code, appid). code is REASON_MAIN
for the main selected profile, REASON_DEPENDENCY for dependencies, or
REASON_STEAM for the Steam profile if the install source is Steam.
If code is REASON_DEPENDENCY, appid is the profile with the dependency."""
self.analyze()
return self._reasons
reasons = property(_getreasons)
def get_use_if_properties(self, appid):
"""Returns a mapping containing the use_if properties for the selected
profile, locale, source and bottle combination."""
self.check_cache()
if appid == self._profile.appid:
sourcetype = self._sourcetype
else:
sourcetype = 'dependency'
cxversion_x, cxversion_y, _dummy = (distversion.CX_VERSION + '..').split('.', 2)
properties = {'product': distversion.BUILTIN_PRODUCT_ID,
'cxversion': distversion.CX_VERSION,
'cxversion.x': cxversion_x.rjust(2, '0'),
'cxversion.y': cxversion_y.rjust(2, '0'),
'platform': distversion.PLATFORM,
'appid': appid,
'locale': self._locale,
'bottletemplate': self.template,
'sourcetype': sourcetype}
if appid == self._profile.appid and self._sourcetype == 'file' and \
self._sourcefile is not None:
properties['sourcefile'] = os.path.basename(self._sourcefile)
diag = cxdiag.get(self.bottlename)
for prop, value in diag.properties.iteritems():
if prop not in properties:
properties[prop] = value
else:
cxlog.err("the %s cxdiag property collides with a builtin one" % prop)
return properties
#####
#
# Assign a category to the bottle
#
#####
def has_category(self):
"""Returns True if we have determined this bottle's category."""
self.check_cache()
return self._category != CAT_NONE
def _cache_category(self, category):
"""A convenience method that caches the category and returns it."""
self._category = category
return self._category
def get_category(self):
"""Returns the bottle's category which is a mesure of its suitability
for the profile selected for installation.
CAT_INCOMPATIBLE means the bottle is incompatible with it.
CAT_RECOMMENDED means we recommend installing in this bottle, which
happens if it contains an application the profile is an extra for,
or that belongs in the same application bottle group as the profile.
CAT_COMPATIBLE means the bottle is compatible.
"""
self.check_cache()
if self._category != CAT_NONE or not self._profile:
return self._category
if self.template not in self._profile.app_profile.bottle_types['use']:
# The bottle's template is not in the profile's set of compatible
# templates. So we need look no further.
return self._cache_category(CAT_INCOMPATIBLE)
app_profile = self._profile.app_profile
app_profile_application_group = app_profile.application_group
if self._use_steam:
app_profile_application_group = u"Steam"
if self.bottle and not app_profile.extra_fors and \
not app_profile_application_group:
# This is a real bottle, and the profile has no property that would
# let us recommend a specific bottle. So we may be able to base our
# analysis on the template alone.
target_template = self.installtask.templates[self.template]
if target_template.get_category() == CAT_COMPATIBLE:
# If the profile is compatible with the template, then it
# should be compatible with any bottle based on it (because
# the only source of incompatibilities are the dependencies and
# new bottles are where we have most of them).
return self._cache_category(CAT_COMPATIBLE)
# We need to really analyze the dependencies now.
if not self.analyze():
# Return CAT_NONE so the caller knows he should try again later.
return CAT_NONE
# If the bottle analysis found issues, then the bottle is incompatible.
if not self.compatible:
return self._cache_category(CAT_INCOMPATIBLE)
# Prefer bottles containing one of the applications we are an extra
# for, and those containing applications that belong to our
# application bottle group.
if app_profile and self.bottle and \
(app_profile.extra_fors or app_profile_application_group):
for (appid, installed) in self.bottle.installed_packages.iteritems():
if appid in app_profile.extra_fors:
return self._cache_category(CAT_RECOMMENDED)
if app_profile_application_group and \
installed.profile and installed.profile.app_profile:
app_group = installed.profile.app_profile.application_group
if app_profile_application_group == app_group:
return self._cache_category(CAT_RECOMMENDED)
return self._cache_category(CAT_COMPATIBLE)
# Alias for Objective-C
getCategory = get_category
def _get_installprofile(self):
installers = self.installers
if installers:
return installers[self._profile.appid]
return None
installprofile = property(_get_installprofile)
class TargetTemplate(TargetBottle):
def __init__(self, installtask, template):
TargetBottle.__init__(self, installtask, None)
self._newtemplate = template
#####
#
# InstallTask delegate
#
#####
class InstallTaskDelegate(object):
"""This class defines the delegate interface that InstallTask uses to
notify the GUI of changes in the installtask state.
The GUI (especially the Mac one) does not have to provide an object that
derives from this class, but it must implement all of the methods defined
here.
"""
@cxobjc.delegate
def profileChanged(self):
"""Notifies the GUI that the profile selected for installation has
changed.
"""
pass
@cxobjc.delegate
def sourceChanged(self):
"""Notifies the GUI that the media selected for the installation has
changed.
"""
pass
@cxobjc.delegate
def categorizedBottle_(self, target_bottle):
"""Notifies the GUI that the specified bottle's category is now
available, where target_bottle is either a TargetBottle or a
TargetTemplate object.
This may be used to implement a GUI that shows the bottles right away
and then updates a field as their categories become known.
"""
pass
@cxobjc.delegate
def categorizedAllBottles(self):
"""Notifies the GUI that all the bottles have been assigned a
category.
It also means that the bottle picking, if any, is done.
This can be used to implement a GUI which only shows list of bottles
one can install into once all their categories are known.
"""
pass
@cxobjc.delegate
def analyzedBottle_(self, target_bottle):
"""Notifies the GUI that the specified bottle has been fully analyzed.
This means we have determined everything there is to know in order to
perform the installation into this bottle. So once one has received
this notification one can query the list of dependencies that will
need to be installed, and the list issues (such as dependency loops,
etc).
Note that the order of the analyzedBottle_() and categorizedBottle_()
notifications is undefined. However, once a bottle has been analyzed
its category can be computed and it is ready for installation (if
compatible).
"""
pass
@cxobjc.delegate
def invalidNewBottleName(self):
"""Notifies the GUI that the newBottleName field has been set to an
invalid value.
This may either be because it contains bad characters or because a
bottle, file or folder by that name already exists.
However such notifications are only sent if InstallTask is instructed
to install in a new bottle.
"""
pass
@cxobjc.delegate
def bottleCreateChanged(self):
"""Notifies the GUI that the flag specifying whether to install in a
new bottle or in an existing one has changed.
"""
pass
@cxobjc.delegate
def bottleNewnameChanged(self):
"""Notifies the GUI that the new bottle name has been modified.
FIXME: Specify if one can receive such notifications when not
installing in a new bottle.
"""
pass
@cxobjc.delegate
def bottleTemplateChanged(self):
"""Notifies the GUI that the new bottle template has been modified.
FIXME: Specify if one can receive such notifications when not
installing in a new bottle.
"""
pass
@cxobjc.delegate
def bottleNameChanged(self):
"""Notifies the GUI that the name of the bottle to install into has
been modified.
FIXME: Specify if one can receive such notifications when
installing in a new bottle.
"""
pass
@cxobjc.delegate
def profileMediaAdded_(self, filename):
"""Notifies the GUI that a non-mountpoint source was found for the
selected profile and added to profile_medias.
"""
pass
@cxobjc.delegate
def profileMediaRemoved_(self, filename):
"""Notifies the GUI that a non-mountpoint source in profile_medias was
removed because it no longer applies to the selected profile.
"""
pass
_NULL_DELEGATE = InstallTaskDelegate()
def _do_nothing(*_args, **_kwargs):
pass
#####
#
# InstallTask
#
#####
class InstallTask(cxobjc.Proxy):
"""The InstallTask class will keep track of the state of a user-selected
c4 profile as we go through the process of installing it. It will also
provide an interface to installation information that must be returned at
run-time, e.g. bottle compatibility.
"""
def __init__(self, show_untested_apps, delegate=None):
cxobjc.Proxy.__init__(self)
self._profiles = None
# An object with methods that will be called when attributes change.
self._delegate = _NULL_DELEGATE
self.set_delegate(delegate)
self._queued_delegate_calls = []
self.show_untested_apps = show_untested_apps
# The C4 profile of the application to install
self._profile = None
self._profileRequested = False
self.installerLocale = None
# Determine the locale to use for profiles without a language selection
for langid in cxutils.get_preferred_languages():
if langid != '' and langid in c4profiles.LANGUAGES:
self._userInstallerLocale = langid
break
else:
self._userInstallerLocale = ''
# The locale we will actually use for installs. This is equal to
# installerLocale if the current profile has a language selection,
# otherwise it's equal to _userInstallerLocale
self.realInstallerLocale = None
# The corresponding installer profile. Note that it may belong to
# another C4 profile, such as the profile for unknown applications.
self.installer = None
# A mapping of absolute paths to the corresponding SourceMedia
# objects. Only the GUI knows about and keeps track of mount points,
# etc. So it is responsible for filling in this map and keeping it up
# to date.
self.source_medias = {}
# A set of automatically-detected sources, based on the current profile.
self.profile_medias = set()
# The source media.
self.installerSourceRequested = False # True if the user requested this source
self.installerDownloadSource = None
self.installerSource = None
self.installWithSteam = False
# Whether this install task should apply cxfixes or not
self.apply_cxfixes = True
# A set of profiles to include or exclude from the install, overriding profiles.
self.dependency_overrides = {}
# The target bottle
self.bottles = {}
self.templates = {}
for template in bottlemanagement.template_list():
self.templates[template] = TargetTemplate(self, template)
self.targetBottle = None
self.targetBottleRequested = False
self.newBottleName = ""
self.newBottleNameRequested = False
self.newBottleNameForUnknown = None
def _getprofiles(self):
if self._profiles is None:
self._profiles = c4profilesmanager.C4ProfilesSet.all_profiles()
return self._profiles
def _setprofiles(self, value):
self._profiles = value
if self._profile:
if self._profile.appid in self.profiles:
self._setprofile(self.profiles[self._profile.appid], requested=self._profileRequested)
else:
self.profile = None
profiles = property(_getprofiles, _setprofiles)
# Special initializer for objc. This must /always/ be called explicitly
# on the Mac.
def initWithDelegate_showUntestedApps_(self, delegate, showUntestedApps):
self = cxobjc.Proxy.nsobject_init(self)
if self is not None:
self.__init__(showUntestedApps, delegate)
return self
def init(self):
return self.initWithDelegate_showUntestedApps_(None, False)
def set_delegate(self, delegate):
if delegate:
self._delegate = delegate
else:
self._delegate = _NULL_DELEGATE
setDelegate_ = set_delegate
def _queue_delegate_call(self, methodname, arg1=None):
self._queued_delegate_calls.append((methodname, (arg1,)))
def _flush_delegate_calls(self):
calls = self._queued_delegate_calls
self._queued_delegate_calls = []
for methodname, args in calls:
args = args[0:InstallTaskDelegate.__dict__[methodname].__code__.co_argcount-1]
getattr(self._delegate, methodname, _do_nothing)(*args)
def select_profile_from_source(self):
if self.installerSource and self.installerSourceRequested and not self._profileRequested:
# Try to select a profile based on the source media.
if os.path.isdir(self.installerSource):
profiles = cddetector.get_cd_profiles(self.installerSource, self.profiles, self.show_untested_apps)
elif os.path.isfile(self.installerSource):
profiles = downloaddetector.find_profiles(self.installerSource, self.profiles, self.show_untested_apps)
else:
profiles = {}
if len(profiles) == 1:
self._setprofile(profiles.values()[0], requested=False)
def AutoFillSettings(self):
# After all intitial settings have been set, and this object and the
# change delegate are initialized, try to guess at anything remaining.
if not self._profileRequested and not self.installerSourceRequested:
# If we can find exactly one profile to select based on media, select it.
package_to_select = None
for source_path in self.source_medias:
profiles = cddetector.get_cd_profiles(source_path, self.profiles, self.show_untested_apps)
if len(profiles) == 1:
if package_to_select:
# too many profiles
break
else:
package_to_select = profiles.values()[0]
elif profiles:
# too many profiles
break
else:
# 0 or 1 profiles found
if package_to_select:
self._setprofile(package_to_select, requested=False)
self.select_profile_from_source()
#####
#
# Profile selection
#
#####
def _get_condition_languages(self, condition, result=None):
if result is None:
result = set()
if isinstance(condition, c4profiles.C4ConditionUnary):
self._get_condition_languages(condition.child, result)
elif isinstance(condition, c4profiles.C4ConditionNary):
for i in condition.children:
self._get_condition_languages(i, result)
elif isinstance(condition, (c4profiles.C4ConditionCompare, c4profiles.C4ConditionMatch)):
if condition.name == 'locale':
result.add(condition.value)
return result
def _scan_url_locales(self, urls, default_locales, nondefault_locales):
if '' in urls:
default_url = urls['']
else:
default_url = None
for locale, url in urls.iteritems():
if locale == '':
continue
if url == default_url:
default_locales.add(locale)
else:
nondefault_locales.add(locale)
def profile_languages(self, profile=None, include_other=True):
"Returns the set of languages that should be presented to the user for a profile"
if profile is None:
profile = self._profile
if profile is not None:
use_if_languages = set()
for installer_profile in profile.installer_profiles:
use_if_languages.update(self._get_condition_languages(installer_profile.use_if))
# Get a list of the locales that are equivalent to the default,
# and of the others, for both the download pages and urls
default_locales = set()
nondefault_locales = set()
self._scan_url_locales(profile.app_profile.download_page_urls, default_locales, nondefault_locales)
if profile.app_profile.download_urls:
self._scan_url_locales(profile.app_profile.download_urls, default_locales, nondefault_locales)
self._scan_url_locales(profile.app_profile.download_urls_64bit, default_locales, nondefault_locales)
result = set()
if use_if_languages:
result.update(use_if_languages)
# If we have programmed special behavior for a particular set of
# languages, selecting a language not in the set must be
# possible.
result.add('')
if result or nondefault_locales:
result.update(nondefault_locales)
result.update(default_locales)
if result and include_other:
# Always add an 'Other language' option if there are choices.
result.add('')
# FIXME: Account for dependencies?
return result
else:
return ()
# Wrapper for Objective-C
def profileLanguages(self):
return list(self.profile_languages())
def languagesForProfile_(self, profile):
return list(self.profile_languages(profile))
def _getprofile(self):
return self._profile
def _getprofiles_matching_media(self):
matches = set()
for source in self.source_medias.itervalues():
matches.update(source.profile_ids)
return matches
profiles_matching_media = property(_getprofiles_matching_media)
def _setprofile(self, profile, requested=True):
if self._profile == profile:
return
if profile is None:
requested = False
if profile is not None and profile.appid in self.dependency_overrides:
del self.dependency_overrides[profile.appid]
hadProfile = self._profile is not None
# Profile
self._profile = profile
self._profileRequested = requested
# Locale
if profile:
languages = self.profile_languages(profile, include_other=False)
if languages:
# Try to find an installer matching one of the user's preferred
# locales
for langid in cxutils.get_preferred_languages():
if langid != '' and langid in languages:
self.installerLocale = langid
break
else:
# Barring that, pick the one locale that the application is
# available for.
if len(languages) == 1:
self.installerLocale = iter(languages).next()
else:
# But if there are multiple options we have no reason to
# pick one over the other, so select 'Other Language'
# and let the user decide.
self.installerLocale = ''
self.realInstallerLocale = self.installerLocale
else:
self.installerLocale = ''
self.realInstallerLocale = self._userInstallerLocale
# Source Media
# If the user had requested a download, we must discard the user's request.
if self.installerDownloadSource is not None or self.installWithSteam:
self.ClearInstallerSource(flush_delegate=False)
old_profile_medias = self.profile_medias
if profile:
self.profile_medias = set(downloaddetector.find_downloads(
self.download_search_paths(), profile))
else:
self.profile_medias = set()
for media in old_profile_medias - self.profile_medias:
self._queue_delegate_call('profileMediaRemoved_', media)
for media in self.profile_medias - old_profile_medias:
self._queue_delegate_call('profileMediaAdded_', media)
if not self.installerSourceRequested: # pylint: disable=R1702
if profile:
installerSource = None
installerDownloadSource = None
multipleSources = False
builtin = cxaiemedia.get_builtin_installer(self._profile.appid)
if builtin is not None:
installerSource = builtin
else:
for source in self.source_medias.itervalues():
if self._profile.appid in source.profile_ids:
if installerSource is None:
installerSource = source.path
else:
# If there are multiple matches, then let the user
# pick one
installerSource = None
multipleSources = True
break
if not installerSource:
installerDownloadSource = self.GetDownloadURL()
if installerSource:
self.SetInstallerSource(installerSource, requested=False, flush_delegate=False)
elif installerDownloadSource and not self.GetSteamID():
self.SetInstallerDownload(installerDownloadSource, requested=False, flush_delegate=False)
elif len(self.profile_medias) == 1:
self.SetInstallerSource(list(self.profile_medias)[0], requested=False, flush_delegate=False)
elif self.GetSteamID() and not installerDownloadSource:
self.SetInstallerSteam(requested=False, flush_delegate=False)
elif multipleSources or hadProfile:
self.ClearInstallerSource(flush_delegate=False)
else:
self.ClearInstallerSource(flush_delegate=False)
self._queue_delegate_call('profileChanged')
self._trigger_bottle_analysis()
self._flush_delegate_calls()
profile = property(_getprofile, _setprofile)
# Wrappers for Objective-C
def setProfile_(self, profile):
self.profile = profile
def setProfile_forUser_(self, profile, requested):
self._setprofile(profile, requested)
def update_profiles(self):
self._profiles = None
if self._profile:
if self._profile.appid in self.profiles:
self._setprofile(self.profiles[self._profile.appid], requested=self._profileRequested)
else:
self.profile = None
updateProfiles = update_profiles
def use_autorun_file(self, c4pfile):
if self._profileRequested:
return
autorun = c4pfile.get_autorun_id()
if autorun and autorun in self.profiles:
self._setprofile(self.profiles[autorun], True)
useAutorunFile_ = use_autorun_file
def _is_virtual(self):
return self._profile and 'virtual' in self._profile.app_profile.flags
virtual = property(_is_virtual)
def _get_display_name(self):
if self._profile:
return self._profile.name
return None
displayName = property(_get_display_name)
def SetLocale(self, locale):
if self.installerLocale == locale:
return
self.realInstallerLocale = self.installerLocale = locale
# Recalculate the download url if applicable
if self.installerDownloadSource or not self.installerSource:
url = self.GetDownloadURL()
if url:
self.SetInstallerDownload(url, requested=self.installerSourceRequested, flush_delegate=False)
else:
self.ClearInstallerSource(flush_delegate=False)
if self._profile:
# A change of locale may impact the bottle selection
self._trigger_bottle_analysis()
self._flush_delegate_calls()
setLocale_ = SetLocale
def AddDependencyOverrides(self, appids, override_type=OVERRIDE_INCLUDE):
for appid in appids:
self.dependency_overrides[appid] = override_type
self._trigger_bottle_analysis()
self._flush_delegate_calls()
def RemoveDependencyOverrides(self, appids):
for appid in appids:
if appid in self.dependency_overrides:
del self.dependency_overrides[appid]
self._trigger_bottle_analysis()
self._flush_delegate_calls()
def ClearDependencyOverrides(self):
self.dependency_overrides = {}
self._trigger_bottle_analysis()
self._flush_delegate_calls()
# Aliases for Objective-C
addDependencyOverrides_ofType_ = AddDependencyOverrides
removeDependencyOverrides_ = RemoveDependencyOverrides
_component_categories = set([u'Component',
u'Component/Font',
u'Component/Fonts',
u'Non Applications/CrossTie Snippets',
u'Non-Applications/CrossTie Snippets',
u'Non-Applications/Components',
u'Non-Applications/Components/Fonts'])
def SuggestedDependencyOverrides(self):
main_appid = self.profile.appid if self.profile else None
if self.target_bottle and self.target_bottle.bottle and \
self.target_bottle.bottle.get_installed_packages_ready():
installed_packages = self.target_bottle.bottle.installed_packages
else:
installed_packages = ()
result = []
for appid, profile in self.profiles.iteritems():
if profile is self._profile or appid in installed_packages or \
not (profile.app_profile.download_urls or 'virtual' in profile.app_profile.flags):
continue
if main_appid in profile.app_profile.extra_fors or \
profile.app_profile.raw_category in self._component_categories:
result.append(appid)
return result
#####
#
# Source media selection
#
#####
def add_source_media(self, mountpoint, label, device=''):
self.source_medias[mountpoint] = SourceMedia(self, mountpoint, label, self.show_untested_apps, device)
def remove_source_media(self, mountpoint):
if mountpoint in self.source_medias:
del self.source_medias[mountpoint]
# Aliases for Objective-C
addSourceMedia_withLabel_andDevice_ = add_source_media
removeSourceMedia_ = remove_source_media
def download_search_paths(self):
return (os.environ.get('HOME', '/'), cxutils.get_download_dir(), cxutils.get_desktop_dir())
def SetInstallerSource(self, inSource, requested=True, flush_delegate=True):
if self.installerSource == inSource:
return
self.installerSource = inSource
if inSource:
self.installerDownloadSource = None
self.installWithSteam = False
self.installerSourceRequested = requested
self.select_profile_from_source()
self._queue_delegate_call('sourceChanged')
self._trigger_bottle_analysis()
if flush_delegate:
self._flush_delegate_calls()
# Wrapper for Objective-C
def setInstallerSource_Requested_(self, inSource, inRequested):
self.SetInstallerSource(inSource, inRequested)
def GetInstallerSource(self):
return self.installerSource
def SetInstallerDownload(self, url=None, requested=True, flush_delegate=True):
if url is None:
url = self.GetDownloadURL()
if self.installerDownloadSource == url:
return
if url is None:
self.ClearInstallerSource()
return
self.installerSource = None
self.installerDownloadSource = url
self.installWithSteam = False
self.installerSourceRequested = requested
self._queue_delegate_call('sourceChanged')
self._trigger_bottle_analysis()
if flush_delegate:
self._flush_delegate_calls()
def SetInstallerSteam(self, requested=True, flush_delegate=True):
source = "steam://install/%s" % self._profile.app_profile.steamid
if self.installerSource == source:
return
self.installerSource = source
self.installerSourceRequested = requested
self.installWithSteam = True
self.installerDownloadSource = None
self._queue_delegate_call('sourceChanged')
self._trigger_bottle_analysis()
if flush_delegate:
self._flush_delegate_calls()
# Wrapper for Objective-C
def setInstallerDownload(self):
self.SetInstallerDownload()
def setInstallerSteam(self):
self.SetInstallerSteam()
def GetInstallerDownload(self):
return self.installerDownloadSource, self.installerLocale
def GetDownloadURL(self):
# Returns the download URL of the currently selected locale
if not self._profile:
return None
if self._profile.app_profile.download_urls:
if self._profile.app_profile.download_urls_64bit and self.is64bit():
download_urls = self._profile.app_profile.download_urls_64bit
else:
download_urls = self._profile.app_profile.download_urls
if self.installerLocale in download_urls:
return download_urls[self.installerLocale]
if '' in download_urls:
return download_urls['']
return None
def GetSteamID(self):
if not self._profile:
return None
if not self._profile.app_profile:
return None
return self._profile.app_profile.steamid
def ClearInstallerSource(self, flush_delegate=True):
if self.installerSource is None and self.installerDownloadSource is None:
return
self.installerSource = None
self.installerDownloadSource = None
self.installerSourceRequested = False
self.installWithSteam = False
self._queue_delegate_call('sourceChanged')
self._trigger_bottle_analysis()
if flush_delegate:
self._flush_delegate_calls()
# Wrapper for Objective-C
def clearInstallerSource(self):
self.ClearInstallerSource()
#####
#
# Automatic bottle selection
#
#####
def _take_bottle_snapshot(self):
self._bottle_snapshot = {
'targetBottle': self.targetBottle,
'newBottleName': self.newBottleName,
}
def _send_bottle_change_notifications(self):
"""An internal helper for sending the relevant notification(s) when
the selected bottle gets changed.
"""
if not self._bottle_snapshot:
cxlog.err('No bottle snapshot was taken!')
return
if self.targetBottle != self._bottle_snapshot['targetBottle']:
self._queue_delegate_call('bottleCreateChanged')
self._queue_delegate_call('bottleTemplateChanged')
self._queue_delegate_call('bottleNameChanged')
if self.newBottleName != self._bottle_snapshot['newBottleName']:
self._queue_delegate_call('bottleNewnameChanged')
def _unpick_bottle(self):
"""This gets called when the target profile is unset and resets the
relevant fields.
"""
self._take_bottle_snapshot()
if not self.targetBottleRequested:
self.targetBottle = None
if not self.newBottleNameRequested:
self.newBottleName = None
self._send_bottle_change_notifications()
def _pick_new_bottle_name(self):
"""Picks a suitable name for the new bottle if we are free to do so.
The caller is responsible for sending the bottle change notification(s).
"""
if not self.newBottleNameRequested:
# Note that here we know that the new bottle name is necessarily
# valid.
bottle_name_hint = self._profile.name
if self._profile.appid == 'com.codeweavers.unknown':
if self.newBottleNameForUnknown:
bottle_name_hint = self.newBottleNameForUnknown
elif self.installerSource:
if self.installerSource in self.source_medias and \
self.source_medias[self.installerSource].label:
bottle_name_hint = self.source_medias[self.installerSource].label
else:
bottle_name_hint = os.path.basename(self.installerSource.rstrip('/')).split('.', 1)[0]
if not distversion.IS_MACOSX:
# Spaces cause trouble in Nautilus (GNOME bug 700320).
# So allow them but avoid them by default
bottle_name_hint = bottle_name_hint.replace(' ', '_')
self.newBottleName = bottlequery.unique_bottle_name(cxutils.sanitize_bottlename(bottle_name_hint))
def _pick_new_bottle(self):
"""Picks a suitable template for the new bottle if we are free to do so.
The caller is responsible for sending the bottle change notification(s).
"""
if self.targetBottleRequested:
return
# Scan the profile's preferred template list and pick the first one
# which is actually compatible
fallback = None
for template in self._profile.app_profile.bottle_types['use']:
if template not in self.templates:
# Not supported in the CrossOver version!
pass
elif self.templates[template].get_category() == CAT_COMPATIBLE:
self.targetBottle = self.templates[template]
return
elif not fallback:
fallback = template
if fallback:
# No template is compatible with both the application and all its
# dependencies. So ignore the dependencies.
self.targetBottle = self.templates[fallback]
return
# None of this application's templates is supported by this CrossOver
# version! So pick the same one as for the unknown profile.
self.targetBottle = self.templates[self.profiles.unknown_profile().app_profile.preferred_bottle_type]
def _pick_bottle(self):
"""This method performs the automatic bottle selection when
appropriate.
It is a helper for _categorize_bottles() and must only be called when
all bottles have been assigned a category.
"""
self._take_bottle_snapshot()
if not self.targetBottleRequested:
self.targetBottle = None
for target_bottle in self.bottles.itervalues():
if target_bottle.get_category() != CAT_RECOMMENDED:
pass
elif self.targetBottle:
# We have more than one recommended bottle so we need
# the user to pick one
self.targetBottle = None
break
else:
self.targetBottle = target_bottle
if not self.targetBottle and ('component' not in self._profile.app_profile.flags or 'application' in self._profile.app_profile.flags):
self._pick_new_bottle()
self._pick_new_bottle_name()
self._send_bottle_change_notifications()
#####
#
# Bottle analysis and categorization
#
#####
def _trigger_bottle_analysis(self):
"""Triggers a bottle analysis to:
- determine which ones are compatible or incompatible with the
selected profile and installation source,
- determine which ones to recommend installation into (if any),
- pick a bottle if possible,
- and acquire all the data we need to start the installation.
"""
# Should this prove too slow this will remain the entry point for
# triggering the bottle analysis but part of the work will be done in
# the background.
if self._profile:
self._categorize_bottles()
else:
# Note that we don't need to go through all the target bottle
# objects to reset their categories thanks to their cache mechanism.
# So all we need is to unset the selected bottle if appropriate.
self._unpick_bottle()
# Notify the GUI that the category of each bottle and template
# has changed.
for target in self.templates.itervalues():
self._queue_delegate_call('categorizedBottle_', target)
for target in self.bottles.itervalues():
self._queue_delegate_call('categorizedBottle_', target)
# Also notify it that we have recategorized all the bottles
self._queue_delegate_call('categorizedAllBottles')
def _categorize_list(self, target_iter):
"""This is a helper for _categorize_bottles()."""
categorized_all = True
for target in target_iter:
if not target.analyzed() and target.analyze():
# Notify the GUI that we have freshly analyzed this
# target bottle
self._queue_delegate_call('analyzedBottle_', target)
if not target.has_category():
if target.get_category() != CAT_NONE:
# Notify the GUI that we have freshly categorized this
# target bottle
self._queue_delegate_call('categorizedBottle_', target)
else:
categorized_all = False
return categorized_all
def _categorize_bottles(self):
"""This goes through all the bottles and templates trying to determine
their category. If it was successful it then calls _pick_bottle() and
notifies the GUI.
"""
if self._categorize_list(self.templates.itervalues()) and \
self._categorize_list(self.bottles.itervalues()):
# We now know the category of all the bottles and templates and
# thus we can now pick one
self._pick_bottle()
# The GUI may have been waiting for all the categories to be known
# to display the bottle list. So notify it we're done.
self._queue_delegate_call('categorizedAllBottles')
# else:
# This means we are waiting for one (or more) of the bottles
# installed applications list to compute the categories. So this
# method will be called again when installed_applications_ready()
# gets called for the relevant bottles.
def get_compatible_templates(self):
"""Returns the list of bottle templates compatible with the currently
selected application, in preference order.
"""
compatible_templates = []
for template_name in self._profile.app_profile.bottle_types['use']:
if self.templates[template_name].get_category() == CAT_COMPATIBLE:
compatible_templates.append(template_name)
return compatible_templates
#####
#
# Bottle addition and removal
#
#####
def add_bottle(self, bottle):
"""Notifies InstallTask that a new bottle was detected.
The caller MUST then schedule the detection of that bottle's installed
applications and, when that's done, it MUST notify InstallTask through
installed_applications_ready(). The one exception is if the bottle
disappears before then and in that case it MUST notify InstallTask
through remove_bottle().
This MUST be called in the main thread.
"""
self.bottles[bottle.name] = TargetBottle(self, bottle)
return self.bottles[bottle.name]
# Alias for Objective-C
addBottle_ = add_bottle
def installed_applications_ready(self, _bottle):
"""Notifies InstallTask that the specified bottle's installed
applications list is now available.
This MUST be called in the main thread.
"""
# FIXME: Scan the installed applications list and update the extrafors.
# This will use the _bottle parameter.
self._trigger_bottle_analysis()
self._flush_delegate_calls()
# Alias for Objective-C
installedApplicationsReady_ = installed_applications_ready
def remove_bottle_by_name(self, bottlename):
"""Notifies InstallTask that the specified bottle has been removed.
This may be called _instead of_ installed_applications_ready() if the
removal happened before the installed application detection could
complete. In this case installed_applications_ready() must not be
called for that bottle.
It is also a bug to call remove_bottle_by_name() twice or to call it on
unregistered bottles.
This MUST be called in the main thread.
"""
bottlename = cxutils.expect_unicode(bottlename)
del self.bottles[bottlename]
# FIXME: Remove this bottle from the extrafors
if self.targetBottle and self.targetBottle.bottlename == bottlename:
self._take_bottle_snapshot()
self.targetBottle = None
self._send_bottle_change_notifications()
# The bottle removal does not impact any of the bottle categories but
# it may impact the automatic bottle selection. So review things again.
self._trigger_bottle_analysis()
self._flush_delegate_calls()
def remove_bottle(self, bottle):
"""Use remove_bottle_by_name instead."""
self.remove_bottle_by_name(bottle.name)
# Alias for Objective-C
removeBottle_ = remove_bottle
#####
#
# Target bottle selection
#
#####
def is_new_bottle_name_valid(self):
"""Returns True if the new bottle name is valid and False otherwise.
In particular this checks for that it does not match an existing
bottle name, file or directory.
This MUST be called in the main thread.
"""
is_valid = True
if not self.newBottleName:
is_valid = False
elif self.newBottleName != bottlequery.unique_bottle_name(cxutils.sanitize_bottlename(self.newBottleName)):
is_valid = False
return is_valid
isNewBottleNameValid = is_new_bottle_name_valid
def SetTargetBottle(self, target_bottle, requested=True, flush_delegate=True):
if isinstance(target_bottle, TargetTemplate):
if self.templates[target_bottle.template] != target_bottle:
raise ValueError()
elif target_bottle is not None:
if self.bottles[target_bottle.bottlename] != target_bottle:
raise ValueError()
else:
requested = False
if target_bottle != self.targetBottle:
was_64bit = self.is64bit()
self.targetBottle = target_bottle
self.targetBottleRequested = requested
if isinstance(target_bottle, TargetTemplate) and not self.is_new_bottle_name_valid():
self._queue_delegate_call('invalidNewBottleName')
# Recalculate the download url if applicable
if self.installerDownloadSource and self.is64bit() != was_64bit:
url = self.GetDownloadURL()
if url:
self.SetInstallerDownload(url, requested=self.installerSourceRequested, flush_delegate=False)
else:
self.ClearInstallerSource(flush_delegate=False)
self._queue_delegate_call('bottleCreateChanged')
self._queue_delegate_call('bottleTemplateChanged')
self._queue_delegate_call('bottleNameChanged')
if flush_delegate:
self._flush_delegate_calls()
def SetNewBottleName(self, newbottlename, requested=True, flush_delegate=True):
if newbottlename is not None:
newbottlename = cxutils.expect_unicode(newbottlename)
name_changed = self.newBottleName != newbottlename
if name_changed:
self.newBottleName = newbottlename
self.newBottleNameRequested = requested
if self.createNewBottle and not self.is_new_bottle_name_valid():
self._queue_delegate_call('invalidNewBottleName')
self._queue_delegate_call('bottleNewnameChanged')
if flush_delegate:
self._flush_delegate_calls()
def SetCreateNewBottle(self, create=None, newbottlename=None, template=None, requested=True):
if create is None:
create = self.createNewBottle
if newbottlename is None:
newbottlename = self.newBottleName
else:
newbottlename = cxutils.expect_unicode(newbottlename)
if template is None:
template = self.newBottleTemplate
create_changed = self.createNewBottle != create
name_changed = self.newBottleName != newbottlename
template_changed = self.newBottleTemplate != template
if name_changed:
self.SetNewBottleName(newbottlename, requested)
if create_changed or (create and template_changed):
if create:
self.SetTargetBottle(self.templates[template], requested)
else:
self.SetTargetBottle(None, False)
self._flush_delegate_calls()
def SetNewBottleNameForUnknown(self, name):
self.newBottleNameForUnknown = name
if self.createNewBottle and not self.newBottleNameRequested:
self._trigger_bottle_analysis()
self._flush_delegate_calls()
# Wrappers for Objective-C
def createNewBottle_template_(self, newbottlename, template):
self.SetCreateNewBottle(True, newbottlename, template)
def createNewBottle_template_forUser_(self, newbottlename, template, requested):
self.SetCreateNewBottle(True, newbottlename, template, requested)
setNewBottleNameForUnknown_ = SetNewBottleNameForUnknown
def GetCreateNewBottle(self):
return isinstance(self.targetBottle, TargetTemplate)
createNewBottle = property(GetCreateNewBottle)
def GetNewBottleTemplate(self):
if isinstance(self.targetBottle, TargetTemplate):
return self.targetBottle.template
return None
newBottleTemplate = property(GetNewBottleTemplate)
def _get_existingBottleName(self):
if self.targetBottle:
return self.targetBottle.bottlename
return None
existingBottleName = property(_get_existingBottleName)
def _get_bottle_name(self):
if isinstance(self.targetBottle, TargetTemplate):
return self.newBottleName
if self.targetBottle:
return self.targetBottle.bottlename
return None
def _set_bottle_name(self, inName, requested=True):
if inName:
self.SetTargetBottle(self.bottles[cxutils.expect_unicode(inName)], requested)
else:
self.SetTargetBottle(None, False)
bottlename = property(_get_bottle_name, _set_bottle_name)
def _gettarget_bottle(self):
"""Returns the TargetBottle object for the bottle selected for
installation. This object can then be used to get all the details about
that bottle: its category, the dependencies to install, and potential
issues.
Note also that this works equally well for existing bottles and new
bottles.
"""
return self.targetBottle
target_bottle = property(_gettarget_bottle)
def _gettarget_template(self):
if self.targetBottle:
return self.targetBottle.template
return None
target_template = property(_gettarget_template)
def is64bit(self):
template = self.target_template
if template is None:
return False
return template.endswith('_64')
#####
#
# Can Install?
#
#####
def can_install(self):
"""Returns True if we have all the data required to perform the
installation and there is no blocking issue, and False otherwise.
"""
# Do we have a profile?
if not self._profile:
return False
# Do we have an installation source?
if not self.installerDownloadSource and not self.installerSource and \
not self.virtual and not self.installWithSteam:
return False
# Do we have a target bottle?
if self.createNewBottle:
if not self.is_new_bottle_name_valid():
return False
elif not self.targetBottle:
return False
# Is there any blocking compatibility issue?
if not self.target_bottle.analyzed() or \
self.target_bottle.missing_media:
return False
# All good
return True
# Wrapper for Objective-C
canInstall = can_install
#####
#
# Getting the summary data
#
#####
def get_installer_profile(self):
"""Computes the appropriate installer profile for the selected
profile, locale and bottle type combination."""
# FIXME: This method should go away and the code that uses it should
# use or be migrated to target_bottle instead.
target_bottle = self.targetBottle
if target_bottle is None:
if self._profile.app_profile.preferred_bottle_type:
target_bottle = self.templates[self._profile.app_profile.preferred_bottle_type]
else:
target_bottle = self.templates[self.profiles.unknown_profile().app_profile.preferred_bottle_type]
elif not target_bottle.analyzed():
target_bottle = self.templates[target_bottle.template]
return target_bottle.installprofile.copy()
def get_profile_names(self, appids):
"""Converts a list of profile ids into a list of human-readable
profile names."""
installers = self.target_bottle.installers
profile_names = []
for appid in appids:
profile_names.append(installers[appid].parent.name)
return profile_names
def get_dependencies(self):
"""Returns a human-readable list of dependencies."""
to_install = []
installers = self.target_bottle.installers
missing_media = self.target_bottle.missing_media
missing_profiles = self.target_bottle.missing_profiles
# Hack: Hide dependencies of Core Fonts.
if self._profile.appid != 'com.codeweavers.c4.6959' and \
'com.codeweavers.c4.6959' in installers:
hide_deps = installers['com.codeweavers.c4.6959'].pre_dependencies
else:
hide_deps = ()
for depid in installers:
if depid == self._profile.appid or depid in hide_deps:
continue
if depid in missing_profiles:
cxlog.warn("The dependency " + cxlog.to_str(depid) + " is not available in our profile list.")
to_install.append(_("%s (unavailable)") % depid)
elif depid in missing_media:
to_install.append(_("%s (needs a CD)") % installers[depid].parent.name)
else:
to_install.append(installers[depid].parent.name)
return to_install
getDependencies = get_dependencies
def _get_installation_notes(self):
return self.get_installer_profile().installation_notes
installationNotes = property(_get_installation_notes)
def _get_post_install_url(self):
return self.get_installer_profile().post_install_url
post_install_url = property(_get_post_install_url)
_RE_SINGLE = re.compile('^[a-zA-Z0-9:_-]*$', re.IGNORECASE)
def _find_warning_in_mapping(self, warning, mapping):
# Search for the given key in a cxdiag mapping
if not self._RE_SINGLE.match(warning):
# A regular expression
regex = re.compile(warning, re.IGNORECASE)
for key in mapping:
if regex.match(key):
yield key
elif warning in mapping:
yield warning
def _filter_cxdiag_messages(self, messages, profile):
# Only report issues that are listed in the profile except for the
# 'Require' ones because they are critical for all applications, and
# the 'Recommend' ones because we consider those to be strongly
# recommended too.
filtered = {}
for key, issue in messages.iteritems():
if issue[0] == 'Require' or issue[0] == 'Recommend':
filtered[key] = issue
# Filter out the remaining issues and adjust their level according to
# the profile specifications.
for check in profile.cxdiag_checks:
check = check.lower()
if check.startswith('apprequire:') or check.startswith('apprecommend:'):
issue_id = check.split(':', 1)[1]
if check.startswith('apprequire:'):
new_level = 'Require'
elif check.startswith('apprecommend:'):
new_level = 'Recommend'
for key in self._find_warning_in_mapping(issue_id, messages):
_level, title, description = messages[key]
filtered[key] = (new_level, title, description)
elif check.startswith('ignore:'):
issue_id = check.split(':', 1)[1]
ignore_list = list(self._find_warning_in_mapping(issue_id, messages))
for key in ignore_list:
if key in filtered:
del filtered[key]
elif check == 'closed':
# We now consider all lists to be closed so this is a no-op
break
else:
cxlog.warn("unrecognized cxdiagcheck string: %s" % cxlog.to_str(check))
return filtered
def _cxdiag_level_cmp(self, a, b):
levels = ['Suggest', 'Recommend', 'Require']
try:
return cmp(levels.index(a), levels.index(b))
except ValueError:
return 0
def get_cxdiag_messages(self):
"""Returns a dictionary of cxdiag messages that apply to the current
profile."""
diag = cxdiag.get(self.existingBottleName)
if self._profile is not None and self.target_bottle and self.target_bottle.analyzed:
result = {}
for profile in self.target_bottle.installers.values():
profile_messages = self._filter_cxdiag_messages(diag.warnings, profile)
for key in profile_messages:
if key not in result or \
self._cxdiag_level_cmp(profile_messages[key][0], result[key][0]) > 0:
result[key] = profile_messages[key]
else:
result = diag.warnings.copy()
return result
def get_apply_cxfixes(self):
"""True if this task should apply necessary cxfixes, False otherwise."""
return self.apply_cxfixes
def get_summary_string(self):
lines = []
if self.profile:
lines.append('Installing: %s\n' % self.profile.name)
if self.installerLocale:
lines.append('Locale: %s\n' % self.installerLocale)
if self.bottlename:
lines.append('Bottle: %s\n' % self.bottlename)
if self.installerSource:
lines.append('From file: %s\n' % self.installerSource)
elif self.installerDownloadSource:
lines.append('From download url: %s\n' % self.installerDownloadSource)
elif self.installWithSteam:
lines.append('From Steam\n')
for k, v in self.dependency_overrides.iteritems():
if k in self.profiles:
if v == OVERRIDE_EXCLUDE:
lines.append('Manually removed from install: %s\n' % self.profiles[k].name)
elif v == OVERRIDE_INCLUDE:
lines.append('Manually added to install: %s\n' % self.profiles[k].name)
return ''.join(lines)