Exemplo n.º 1
0
  def testGetDependenciesWithPlatformFilter(self):
    snapshot = self.CreateSnapshotFromStrings(
        1,
        'a,b,c,d,e,f,g,h',
        'a->b,c|b->d,e|c->f,g|d->h|f->h')
    self.ChangePlatformForComponents(
        snapshot, ['b'],
        platforms.Platform(platforms.OperatingSystem.WINDOWS, None))

    all_set = set(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
    filtered_set = set(['a', 'c', 'f', 'g', 'h'])

    self.assertEqual(all_set, snapshot.DependencyClosureForComponents(
        ['a'],
        platform_filter=platforms.Platform(platforms.OperatingSystem.WINDOWS,
                                           platforms.Architecture.x86_64)))
    self.assertEqual(filtered_set, snapshot.DependencyClosureForComponents(
        ['a'],
        platform_filter=platforms.Platform(platforms.OperatingSystem.LINUX,
                                           platforms.Architecture.x86_64)))

    self.assertEqual(set(), snapshot.ConnectedComponents(
        ['b'],
        platform_filter=platforms.Platform(platforms.OperatingSystem.LINUX,
                                           platforms.Architecture.x86_64)))
    self.assertEqual(
        set(['h', 'd', 'f', 'c', 'a']), snapshot.ConsumerClosureForComponents(
            ['h'],
            platform_filter=platforms.Platform(platforms.OperatingSystem.LINUX,
                                               platforms.Architecture.x86_64)))
Exemplo n.º 2
0
  def testGetEffectiveSize(self):
    snapshot = self.CreateSnapshotFromStrings(1, 'a1,b1,c1,d1,e1,f1',
                                              'a->b|b->c|a->d|d->e|a->f')
    snapshot.components['b'].is_hidden = True
    snapshot.components['b'].data = schemas.ComponentData.FromDictionary(
        {'type': 'tar', 'source': 'foo', 'size': 1})
    snapshot.components['c'].is_hidden = True
    snapshot.components['c'].data = schemas.ComponentData.FromDictionary(
        {'type': 'tar', 'source': 'foo', 'size': 2})
    snapshot.components['d'].is_hidden = False
    snapshot.components['d'].data = schemas.ComponentData.FromDictionary(
        {'type': 'tar', 'source': 'foo', 'size': 4})
    snapshot.components['e'].is_hidden = True
    snapshot.components['e'].data = schemas.ComponentData.FromDictionary(
        {'type': 'tar', 'source': 'foo', 'size': 8})
    snapshot.components['f'].is_hidden = True
    snapshot.components['f'].data = schemas.ComponentData.FromDictionary(
        {'type': 'tar', 'source': 'foo', 'size': 16})
    self.ChangePlatformForComponents(
        snapshot, ['f'],
        platforms.Platform(platforms.OperatingSystem.WINDOWS, None))

    # We only include the size of b in the size of a.
    self.assertEqual(
        1, snapshot.GetEffectiveComponentSize('a', platform_filter=None))
    # c is a regular component with data just return its size.
    self.assertEqual(
        2, snapshot.GetEffectiveComponentSize('c', platform_filter=None))
    # asdf does not exist.  It has no size.
    self.assertEqual(
        0, snapshot.GetEffectiveComponentSize('asdf', platform_filter=None))
    # f does not match this platform.  It has no size.
    self.assertEqual(
        0, snapshot.GetEffectiveComponentSize('f', platform_filter=None))
Exemplo n.º 3
0
    def SetUp(self):
        self.platform = platforms.Platform(platforms.OperatingSystem.WINDOWS,
                                           platforms.Architecture.x86)
        self.real_manager = update_manager.UpdateManager(
            self.sdk_root_path,
            self.URLFromFile(self.Resource('parsetest.json')),
            platform_filter=self.platform)
        # This is because disable_updater is true for linux distributions which
        # causes packaging tests fo fail.
        config.INSTALLATION_CONFIG.disable_updater = False
        # This is for testListOnlyLocalState but needs to be done in SetUp because
        # once the updater patcher starts, calls to Update start throwing errors.
        self.real_manager.Update(['c1'])

        patcher = mock.patch(
            'googlecloudsdk.core.updater.update_manager.UpdateManager',
            autospec=True)
        self.addCleanup(patcher.stop)
        self.updater_mock = patcher.start()

        patcher = mock.patch('googlecloudsdk.core.util.platforms.Platform',
                             autospec=True)
        self.addCleanup(patcher.stop)
        self.platform_mock = patcher.start()
        self.platform_mock.Current.side_effect = lambda *args: args
Exemplo n.º 4
0
    def testMatchEither(self):
        component_platform = schemas.ComponentPlatform([
            platforms.OperatingSystem.WINDOWS, platforms.OperatingSystem.LINUX
        ], None)

        for p in [
                platforms.Platform(platforms.OperatingSystem.WINDOWS, None),
                platforms.Platform(platforms.OperatingSystem.LINUX,
                                   platforms.Architecture.x86_64)
        ]:
            self.assertTrue(component_platform.Matches(p))

        for p in [
                platforms.Platform(None, None),
                platforms.Platform(platforms.OperatingSystem.MACOSX, None),
                platforms.Platform(platforms.OperatingSystem.MACOSX,
                                   platforms.Architecture.x86_64)
        ]:
            self.assertFalse(component_platform.Matches(p))
Exemplo n.º 5
0
    def testMatchUnknown(self):
        """Test we can parse unknown enums and that the matching works."""
        component_platform = schemas.ComponentPlatform.FromDictionary(
            {'operating_systems': ['ASDF', 'LINUX']})
        # Doesn't match unknown, even though the enum itself is unknown.
        self.assertFalse(
            component_platform.Matches(platforms.Platform(None, None)))
        # Doesn't match a known OS that is wrong.
        self.assertFalse(
            component_platform.Matches(
                platforms.Platform(platforms.OperatingSystem.WINDOWS, None)))
        # Matches a known OS that is correct.
        self.assertTrue(
            component_platform.Matches(
                platforms.Platform(platforms.OperatingSystem.LINUX, None)))

        component_platform = schemas.ComponentPlatform.FromDictionary(
            {'architectures': ['ASDF', 'x86_64']})
        # Doesn't match unknown, even though the enum itself is unknown.
        self.assertFalse(
            component_platform.Matches(platforms.Platform(None, None)))
        # Doesn't match a known arch that is wrong
        self.assertFalse(
            component_platform.Matches(
                platforms.Platform(None, platforms.Architecture.x86)))
        # Matches a known arch that is correct.
        self.assertTrue(
            component_platform.Matches(
                platforms.Platform(None, platforms.Architecture.x86_64)))
Exemplo n.º 6
0
 def testMatchNone(self):
     """Ensure that an empty filter will match all OS and Arch."""
     for component_platform in [
             schemas.ComponentPlatform(None, None),
             schemas.ComponentPlatform([], [])
     ]:
         for p in [
                 platforms.Platform(None, None),
                 platforms.Platform(platforms.OperatingSystem.WINDOWS,
                                    None),
                 platforms.Platform(platforms.OperatingSystem.MACOSX, None),
                 platforms.Platform(None, platforms.Architecture.x86),
                 platforms.Platform(None, platforms.Architecture.x86_64),
                 platforms.Platform(platforms.OperatingSystem.WINDOWS,
                                    platforms.Architecture.x86)
         ]:
             self.assertTrue(component_platform.Matches(p))
Exemplo n.º 7
0
class Base(sdk_test_base.SdkBase):
    """Base class for all updater tests."""

    # It doesn't really matter what these are, just as long as they are different.
    CURRENT_PLATFORM = platforms.Platform(platforms.OperatingSystem.LINUX,
                                          None)
    OTHER_PLATFORM = platforms.Platform(platforms.OperatingSystem.WINDOWS,
                                        None)
    PATHS = [
        os.path.join('lib', '{0}-{1}', 'file1.py'),
        os.path.join('lib', '{0}-{1}', 'file2.py'),
        os.path.join('platform', '{0}-{1}', 'code1.py'),
        os.path.join('platform', '{0}-{1}', 'code2.py'),
        os.path.join('bin', '{0}-{1}.py'),
    ]

    def SetUp(self):
        # sys.getfilesystemencoding() is initialized early in python startup and
        # cannot be changed thereafter. Some test runners do not give us the
        # opportunity to do the incantation to get it initialized to handle unicode
        # paths. So we check here if the default test runner encoding supports
        # unicode and if so use an installation directory name containing unicode
        # characters, otherwise just an ascii installation directory name. We know
        # that most of our test runners support unicode in pathnames.
        if FilesystemSupportsUnicodeEncodedPaths():
            sdk_root_dir = 'Ṳᾔḯ¢◎ⅾℯ-cloudsdk'
        else:
            sdk_root_dir = 'cloudsdk'
        self.sdk_root_path = self.CreateTempDir(sdk_root_dir)

        self.staging_path = self.CreateTempDir('staging')
        self.old_executable = sys.executable
        sys.executable = 'current/python'

    def TearDown(self):
        sys.executable = self.old_executable

    def Resource(self, *args):
        """Returns the path to a test resource under the 'data' directory.

    Args:
      *args: str, The relative path parts of the resource under the 'data'
        directory.

    Returns:
      str, The full path to the resource.
    """
        return super(Base, self).Resource('tests', 'unit', 'core', 'updater',
                                          'testdata', *args)

    def URLFromFile(self, base_path, *paths):
        """Creates a URL from a file and more path parts.

    Correctly converts Windows paths as well.

    Args:
      base_path: str, The absolute file path.
      *paths: str, More parts to add onto the ends of paths.

    Returns:
      str, A properly formatted URL.
    """
        url = ('file://' + ('/' if not base_path.startswith('/') else '') +
               base_path.replace('\\', '/'))
        return '/'.join([url] + list(paths))

    def CheckPathsExist(self, rel_paths, exists=True, alt_root=None):
        """Verifies that a given path does or does not exist.

    Args:
      rel_paths: list of str, The paths relative to the root directory to
        verify.
      exists: bool, True to ensure they exist, False to ensure they do not
        exist.
      alt_root: An optional path string to check the paths relative to.  If
        None, Directories.RootDir() will be used
    """
        root = alt_root if alt_root else self.sdk_root_path
        for p in rel_paths:
            full_path = os.path.join(root, p)
            if (os.path.isfile(full_path) or os.path.isdir(full_path)
                    or os.path.islink(full_path)) is not exists:
                self.fail(
                    'File [{0}] in wrong state, should exist [{1}]'.format(
                        p, exists))

    def CreateTempTar(self, temp_dir, rel_paths, file_contents=''):
        """Creates a tar file with the given files in it."""
        tar_dir = tempfile.mkdtemp(dir=temp_dir)
        for rel_path in rel_paths:
            self.Touch(tar_dir,
                       rel_path,
                       contents=file_contents,
                       makedirs=True)
        return self.CreateTempTarFromDir(temp_dir, tar_dir)

    def CreateTempTarFromDir(self, temp_dir, tar_dir):
        """Creates a tar file from the contents of a directory."""
        f, name = tempfile.mkstemp(suffix='.tar.gz', dir=temp_dir)
        os.close(f)

        with tarfile.open(name, mode='w|gz') as tar_file:
            for top_element in os.listdir(tar_dir):
                tar_file.add(os.path.join(tar_dir, top_element), top_element)
        return name

    def CreateFakeComponent(self, name, deps, version=None, is_required=False):
        """Constructs a schemas.Component from the given data."""
        return schemas.Component(
            id=name,
            details=None,
            version=schemas.ComponentVersion(build_number=version,
                                             version_string=version),
            dependencies=deps,
            data=None,
            is_hidden=False,
            is_required=is_required,
            is_configuration=False,
            platform=schemas.ComponentPlatform(None, None))

    def CreateSnapshotFromStrings(self, revision, component_string,
                                  dependency_string):
        """Generates an entire ComponentSnaphost from some strings.

    Args:
      revision: int, The revision of the snapshot.
      component_string: A comma separated string of components like a1,b2,c1
        where the letters are component names and the numbers are the current
        version of that component in this snapshot.
      dependency_string: A string like a->b|b->c where the values are component
        names (no versions) and the -> represents an dependency.  Multiple
        dependencies are separated by the | character.

    Returns:
      A ComponentSnapshot.
    """
        regex = r'([a-z]+)(\d*)'
        component_versions = {}
        if component_string:
            for c in component_string.split(','):
                result = re.match(regex, c)
                component_versions[result.group(1)] = result.group(2)

        dependencies = dict((c, list()) for c in component_versions)
        if dependency_string:
            for dependency in dependency_string.split('|'):
                component, dependency_list = dependency.split('->')
                for d in dependency_list.split(','):
                    dependencies[component].append(d)

        component_tuples = []
        for c, deps in six.iteritems(dependencies):
            component_tuples.append((c, component_versions[c], deps, None))

        return self.CreateSnapshotFromComponents(revision, component_tuples)

    def CreateComponentJSON(self,
                            component_id,
                            version,
                            dependencies,
                            is_required=False,
                            data_url=None):
        """Generates a JSON dictionary for a component from the given data."""
        data = {
            'id': component_id,
            'is_required': is_required,
            'details': {
                'display_name': component_id + ' Nice Name',
                'description': 'This is component: ' + component_id
            },
            'version': {
                'build_number': version,
                'version_string': str(version)
            },
            'dependencies': dependencies,
        }
        if data_url:
            data['data'] = {
                'type': 'tar',
                'source': data_url,
                'checksum': str(version),
                'contents_checksum': str(version)
            }
        return data

    def CreateSnapshotFromComponents(self,
                                     revision,
                                     component_tuples,
                                     release_notes_file=None,
                                     notifications=None):
        """Generates a ComponentSnapshot from the given tuples.

    Args:
      revision: int, The revision of the snapshot.
      component_tuples: A list of tuples in the format
        (component_id, version, dependencies, data_url) for each component to
        add to the snapshot.
      release_notes_file: str, The path to a local release notes file for
        testing.  If None, a dummy file is used.
      notifications: list, A list of dictionary representations of
        NotificationSpec objects to insert into this snapshot.

    Returns:
      A ComponentSnapshot.
    """
        components = []
        for component_id, version, dependencies, data_url in component_tuples:
            is_required = component_id.startswith('req_')
            data = self.CreateComponentJSON(component_id, version,
                                            dependencies, is_required,
                                            data_url)
            components.append(data)
        release_notes_url = (self.URLFromFile(release_notes_file)
                             if release_notes_file else 'RELEASE_NOTES')

        data = {
            'revision': revision,
            'release_notes_url': release_notes_url,
            'version': str(revision),
            'components': components,
            'notifications': notifications
        }
        sdk_definition = schemas.SDKDefinition.FromDictionary(data)
        return snapshots.ComponentSnapshot(sdk_definition)

    def GeneratePathsFor(self, component_id, version):
        """Generates a list of relative file paths for this component."""
        return [p.format(component_id, version) for p in self.PATHS]

    def CreateSnapshotFromComponentsGenerateTars(self,
                                                 revision,
                                                 component_tuples,
                                                 release_notes_file=None,
                                                 notifications=None):
        """Generates a ComponentSnapshot and a real .tar file for each component.

    Args:
      revision: int, The revision of the snapshot.
      component_tuples: A list of tuples in the format
        (component_id, version, dependencies) for each component to add to the
        snapshot.
      release_notes_file: str, The path to a local release notes file for
        testing.  If None, a dummy file is used.
      notifications: list, A list of dictionary representations of
        NotificationSpec objects to insert into this snapshot.

    Returns:
      A tuple of (ComponentSnapshot, relative file paths).
    """
        new_tuples = []
        paths = {}
        for component_id, version, dependencies in component_tuples:
            rel_paths = self.GeneratePathsFor(component_id, version)
            tar_file = self.CreateTempTar(self.staging_path, rel_paths)
            new_tuples.append(
                (component_id, version, dependencies,
                 self.URLFromFile(
                     six.moves.urllib.request.pathname2url(tar_file))))
            paths[component_id] = rel_paths
        snapshot = self.CreateSnapshotFromComponents(
            revision,
            new_tuples,
            release_notes_file=release_notes_file,
            notifications=notifications)
        return snapshot, paths

    def CreateTempSnapshotFileFromSnapshot(self, snapshot, versioned=False):
        """Writes the given snapshot to a real file and return the path to it."""
        handle, path = tempfile.mkstemp(suffix='.temp_snapshot',
                                        dir=self.staging_path)
        os.close(handle)
        snapshot.WriteToFile(path)

        if versioned:
            file_name = (
                update_manager.UpdateManager.VERSIONED_SNAPSHOT_FORMAT.format(
                    snapshot.version))
            snapshot.WriteToFile(os.path.join(os.path.dirname(path),
                                              file_name))

        return path

    def ChangePlatformForComponents(self, snapshot, ids, platform=None):
        """Change the opt-in platform for the given components in the snapshot.

    Args:
      snapshot: snapshots.ComponentSnapshot, The snapshot to update
      ids: list(str), The components to update the platforms for
      platform: platforms.Platform, The platform to set the given components to.
    """
        if not platform:
            platform = self.OTHER_PLATFORM
        operating_system = ([platform.operating_system]
                            if platform.operating_system else None)
        architecture = [platform.architecture
                        ] if platform.architecture else None
        component_platform = schemas.ComponentPlatform(operating_system,
                                                       architecture)
        for component_id in ids:
            snapshot.components[component_id].platform = component_platform
Exemplo n.º 8
0
class ComponentSnapshotDiff(object):
    """Provides the ability to compare two ComponentSnapshots.

  This class is used to see how the current state-of-the-word compares to what
  we have installed.  It can be for informational purposes (to list available
  updates) but also to determine specifically what components need to be
  uninstalled / installed for a specific update command.

  Attributes:
    current: ComponentSnapshot, The current snapshot state.
    latest: CompnentSnapshot, The new snapshot that is being compared.
  """

    DARWIN_X86_64 = platforms.Platform(platforms.OperatingSystem.MACOSX,
                                       platforms.Architecture.x86_64)

    def __init__(self,
                 current,
                 latest,
                 platform_filter=None,
                 enable_fallback=False):
        """Creates a new diff between two ComponentSnapshots.

    Args:
      current: The current ComponentSnapshot
      latest: The ComponentSnapshot representing a new state we can move to
      platform_filter: platforms.Platform, A platform that components must
        match in order to be considered for any operations.
      enable_fallback: bool, True to enable fallback from darwin arm64 version
        to darwin x86_64 version of the component.
    """
        self.current = current
        self.latest = latest
        self.__platform_filter = platform_filter
        self.__enable_fallback = (enable_fallback and platform_filter
                                  and platform_filter.operating_system
                                  == platforms.OperatingSystem.MACOSX
                                  and platform_filter.architecture
                                  == platforms.Architecture.arm)

        self.__all_components = (
            current.AllComponentIdsMatching(platform_filter)
            | latest.AllComponentIdsMatching(platform_filter))

        if self.__enable_fallback:
            # Fall back to darwin_x86 version when darwin_arm version is unavailable.
            self.__all_darwin_x86_64_components = (
                current.AllComponentIdsMatching(self.DARWIN_X86_64)
                | latest.AllComponentIdsMatching(self.DARWIN_X86_64))
            self.__darwin_x86_64_components = (
                self.__all_darwin_x86_64_components - self.__all_components)
            self.__native_all_components = set(self.__all_components)
            self.__all_components |= self.__darwin_x86_64_components

            self.__diffs = [
                ComponentDiff(component_id,
                              current,
                              latest,
                              platform_filter=platform_filter)
                for component_id in self.__native_all_components
            ]
            self.__diffs.extend([
                ComponentDiff(component_id,
                              current,
                              latest,
                              platform_filter=self.DARWIN_X86_64)
                for component_id in self.__darwin_x86_64_components
            ])
        else:
            self.__diffs = [
                ComponentDiff(component_id,
                              current,
                              latest,
                              platform_filter=platform_filter)
                for component_id in self.__all_components
            ]

        self.__removed_components = set(
            diff.id for diff in self.__diffs
            if diff.state is ComponentState.REMOVED)
        self.__new_components = set(diff.id for diff in self.__diffs
                                    if diff.state is ComponentState.NEW)
        self.__updated_components = set(
            diff.id for diff in self.__diffs
            if diff.state is ComponentState.UPDATE_AVAILABLE)

    def InvalidUpdateSeeds(self, component_ids):
        """Sees if any of the given components don't exist locally or remotely.

    Args:
      component_ids: list of str, The components that the user wants to update.

    Returns:
      set of str, The component ids that do not exist anywhere.
    """
        if self.__enable_fallback:
            native_invalid_ids = set(
                component_ids) - self.__native_all_components
            arm_x86_ids = native_invalid_ids & self.__darwin_x86_64_components
            if arm_x86_ids:
                rosetta2_installed = os.path.isfile(
                    '/Library/Apple/System/Library/LaunchDaemons/com.apple.oahd.plist'
                )
                if rosetta2_installed:
                    log.warning(
                        'The ARM versions of the following components are not '
                        'available yet, using x86_64 versions instead: [{}].'.
                        format(', '.join(arm_x86_ids)))
                else:
                    log.warning(
                        'The ARM versions of the components [{}] are not '
                        'available yet. To download and execute the x86_64 '
                        'version of the components, please install Rosetta 2 '
                        'first by running the command: '
                        'softwareupdate --install-rosetta.'.format(
                            ', '.join(arm_x86_ids)))
                    return (set(component_ids) -
                            self.__all_components) | arm_x86_ids
        return set(component_ids) - self.__all_components

    def AllDiffs(self):
        """Gets all ComponentDiffs for this snapshot comparison.

    Returns:
      The list of all ComponentDiffs between the snapshots.
    """
        return self._FilterDiffs(None)

    def AvailableUpdates(self):
        """Gets ComponentDiffs for components where there is an update available.

    Returns:
      The list of ComponentDiffs.
    """
        return self._FilterDiffs(ComponentState.UPDATE_AVAILABLE)

    def AvailableToInstall(self):
        """Gets ComponentDiffs for new components that can be installed.

    Returns:
      The list of ComponentDiffs.
    """
        return self._FilterDiffs(ComponentState.NEW)

    def Removed(self):
        """Gets ComponentDiffs for components that no longer exist.

    Returns:
      The list of ComponentDiffs.
    """
        return self._FilterDiffs(ComponentState.REMOVED)

    def UpToDate(self):
        """Gets ComponentDiffs for installed components that are up to date.

    Returns:
      The list of ComponentDiffs.
    """
        return self._FilterDiffs(ComponentState.UP_TO_DATE)

    def _FilterDiffs(self, state):
        if not state:
            filtered = self.__diffs
        else:
            filtered = [diff for diff in self.__diffs if diff.state is state]
        return sorted(filtered, key=lambda d: d.name)

    def ToRemove(self, update_seed):
        """Calculate the components that need to be uninstalled.

    Based on this given set of components, determine what we need to remove.
    When an update is done, we update all components connected to the initial
    set.  Based on this, we need to remove things that have been updated, or
    that no longer exist.  This method works with ToInstall().  For a given
    update set the update process should remove anything from ToRemove()
    followed by installing everything in ToInstall().  It is possible (and
    likely) that a component will be in both of these sets (when a new version
    is available).

    Args:
      update_seed: list of str, The component ids that we want to update.

    Returns:
      set of str, The component ids that should be removed.
    """
        # Get the full set of everything that needs to be updated together that we
        # currently have installed
        if self.__enable_fallback:
            native_seed = set(update_seed) & self.__native_all_components
            darwin_x86_64 = set(update_seed) - native_seed
            connected = self.current.ConnectedComponents(
                native_seed, platform_filter=self.__platform_filter)
            connected |= self.latest.ConnectedComponents(
                connected | set(native_seed),
                platform_filter=self.__platform_filter)
            connected_darwin_x86_64 = self.current.ConnectedComponents(
                darwin_x86_64, platform_filter=self.DARWIN_X86_64)
            connected_darwin_x86_64 |= self.latest.ConnectedComponents(
                connected_darwin_x86_64 | set(darwin_x86_64),
                platform_filter=self.DARWIN_X86_64)
            connected |= connected_darwin_x86_64
        else:
            connected = self.current.ConnectedComponents(
                update_seed, platform_filter=self.__platform_filter)
            connected |= self.latest.ConnectedComponents(
                connected | set(update_seed),
                platform_filter=self.__platform_filter)
        removal_candidates = connected & set(self.current.components.keys())
        # We need to remove anything that no longer exists or that has been updated
        return (self.__removed_components
                | self.__updated_components) & removal_candidates

    def ToInstall(self, update_seed):
        """Calculate the components that need to be installed.

    Based on this given set of components, determine what we need to install.
    When an update is done, we update all components connected to the initial
    set.  Based on this, we need to install things that have been updated or
    that are new.  This method works with ToRemove().  For a given update set
    the update process should remove anything from ToRemove() followed by
    installing everything in ToInstall().  It is possible (and likely) that a
    component will be in both of these sets (when a new version is available).

    Args:
      update_seed: list of str, The component ids that we want to update.

    Returns:
      set of str, The component ids that should be removed.
    """
        installed_components = list(self.current.components.keys())

        if self.__enable_fallback:
            native_seed = set(update_seed) & self.__native_all_components
            darwin_x86_64 = set(update_seed) - native_seed

            local_connected = self.current.ConnectedComponents(
                native_seed, platform_filter=self.__platform_filter)
            all_required = self.latest.DependencyClosureForComponents(
                local_connected | set(native_seed),
                platform_filter=self.__platform_filter)
            local_connected_darwin_x86_64 = self.current.ConnectedComponents(
                darwin_x86_64, platform_filter=self.DARWIN_X86_64)
            all_required |= self.latest.DependencyClosureForComponents(
                local_connected_darwin_x86_64 | set(darwin_x86_64),
                platform_filter=self.DARWIN_X86_64)

            remote_connected = self.latest.ConnectedComponents(
                local_connected | set(native_seed),
                platform_filter=self.__platform_filter)
            remote_connected |= self.latest.ConnectedComponents(
                local_connected_darwin_x86_64 | set(darwin_x86_64),
                platform_filter=self.__platform_filter)
            all_required |= remote_connected & set(installed_components)
        else:
            local_connected = self.current.ConnectedComponents(
                update_seed, platform_filter=self.__platform_filter)
            all_required = self.latest.DependencyClosureForComponents(
                local_connected | set(update_seed),
                platform_filter=self.__platform_filter)

            # Add in anything in the connected graph that we already have installed.
            # Even though the update seed might not depend on it, if it was already
            # installed, it might have been removed in ToRemove() if an update was
            # available so we should put it back.
            remote_connected = self.latest.ConnectedComponents(
                local_connected | set(update_seed),
                platform_filter=self.__platform_filter)
            all_required |= remote_connected & set(installed_components)

        different = self.__new_components | self.__updated_components

        # all new or updated components, or if it has not been changed but we
        # don't have it
        return set(c for c in all_required
                   if c in different or c not in installed_components)

    def DetailsForCurrent(self, component_ids):
        """Gets the schema.Component objects for all ids from the current snapshot.

    Args:
      component_ids: list of str, The component ids to get.

    Returns:
      A list of schema.Component objects sorted by component display name.
    """
        return sorted(self.current.ComponentsFromIds(component_ids),
                      key=lambda c: c.details.display_name)

    def DetailsForLatest(self, component_ids):
        """Gets the schema.Component objects for all ids from the latest snapshot.

    Args:
      component_ids: list of str, The component ids to get.

    Returns:
      A list of schema.Component objects sorted by component display name.
    """
        return sorted(self.latest.ComponentsFromIds(component_ids),
                      key=lambda c: c.details.display_name)