Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

beebox / crossover   deb

Repository URL to install this package:

Version: 18.5.0-1 

/ opt / cxoffice / lib / python / installtask.py

# (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)