Class: Pod::Resolver

Inherits:
Object
  • Object
show all
Includes:
Molinillo::SpecificationProvider, Molinillo::UI
Defined in:
lib/cocoapods/resolver.rb,
lib/cocoapods/resolver/resolver_specification.rb

Overview

The resolver is responsible of generating a list of specifications grouped by target for a given Podfile.

Defined Under Namespace

Classes: EdgeAndPlatform, ResolverSpecification

Resolution collapse

Instance Attribute Summary collapse

Resolution collapse

Private helpers collapse

Instance Method Summary collapse

Constructor Details

#initialize(sandbox, podfile, locked_dependencies, sources, specs_updated, podfile_dependency_cache: Installer::Analyzer::PodfileDependencyCache.from_podfile(podfile), sources_manager: Config.instance.sources_manager) ⇒ Resolver

Init a new Resolver

Parameters:

  • sandbox (Sandbox)

    @see sandbox

  • podfile (Podfile)

    @see podfile

  • locked_dependencies (Array<Dependency>)

    @see locked_dependencies

  • sources (Array<Source>, Source)

    @see sources

  • specs_updated (Boolean)

    @see specs_updated

  • podfile_dependency_cache (PodfileDependencyCache) (defaults to: Installer::Analyzer::PodfileDependencyCache.from_podfile(podfile))

    the podfile dependency cache to use within this Resolver.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/cocoapods/resolver.rb', line 56

def initialize(sandbox, podfile, locked_dependencies, sources, specs_updated,
               podfile_dependency_cache: Installer::Analyzer::PodfileDependencyCache.from_podfile(podfile),
               sources_manager: Config.instance.sources_manager)
  @sandbox = sandbox
  @podfile = podfile
  @locked_dependencies = locked_dependencies
  @sources = Array(sources)
  @specs_updated = specs_updated
  @podfile_dependency_cache = podfile_dependency_cache
  @sources_manager = sources_manager
  @platforms_by_dependency = Hash.new { |h, k| h[k] = [] }

  @cached_sets = {}
  @podfile_requirements_by_root_name = @podfile_dependency_cache.podfile_dependencies.group_by(&:root_name).each_value { |a| a.map!(&:requirement).freeze }.freeze
  @search = {}
  @validated_platforms = Set.new
end

Instance Attribute Details

#cached_setsHash<String => Set> (readonly, private)

Note:

Sets store the resolved dependencies and return the highest available specification found in the sources. This is done globally and not per target definition because there can be just one Pod installation, so different version of the same Pods for target definitions are not allowed.

Returns A cache that keeps tracks of the sets loaded by the resolution process.

Returns:

  • (Hash<String => Set>)

    A cache that keeps tracks of the sets loaded by the resolution process.



337
338
339
# File 'lib/cocoapods/resolver.rb', line 337

def cached_sets
  @cached_sets
end

#locked_dependenciesArray<Dependency> (readonly)

Returns the list of dependencies locked to a specific version.

Returns:

  • (Array<Dependency>)

    the list of dependencies locked to a specific version.



30
31
32
# File 'lib/cocoapods/resolver.rb', line 30

def locked_dependencies
  @locked_dependencies
end

#podfilePodfile (readonly)

Returns the Podfile used by the resolver.

Returns:

  • (Podfile)

    the Podfile used by the resolver.



25
26
27
# File 'lib/cocoapods/resolver.rb', line 25

def podfile
  @podfile
end

#sandboxSandbox (readonly)

Returns the Sandbox used by the resolver to find external dependencies.

Returns:

  • (Sandbox)

    the Sandbox used by the resolver to find external dependencies.



21
22
23
# File 'lib/cocoapods/resolver.rb', line 21

def sandbox
  @sandbox
end

#sourcesArray<Source> (readonly)

Returns The list of the sources which will be used for the resolution.

Returns:

  • (Array<Source>)

    The list of the sources which will be used for the resolution.



35
36
37
# File 'lib/cocoapods/resolver.rb', line 35

def sources
  @sources
end

#sources_managerSource::Manager (readonly)

Returns the manager to use for dependency resolution.

Returns:



44
45
46
# File 'lib/cocoapods/resolver.rb', line 44

def sources_manager
  @sources_manager
end

#specs_updatedBoolean (readonly) Also known as: specs_updated?

Returns Whether the resolver has sources repositories up-to-date.

Returns:

  • (Boolean)

    Whether the resolver has sources repositories up-to-date.



39
40
41
# File 'lib/cocoapods/resolver.rb', line 39

def specs_updated
  @specs_updated
end

Instance Method Details

#after_resolutionVoid

Called after resolution ends.

Completely silence this, as we show nothing.

Returns:

  • (Void)


310
311
# File 'lib/cocoapods/resolver.rb', line 310

def after_resolution
end

#aggregate_for_dependency(dependency) ⇒ Source::Aggregate (private)

Returns The aggregate of the #sources.

Returns:

  • (Source::Aggregate)

    The aggregate of the #sources.



421
422
423
424
425
426
427
428
429
# File 'lib/cocoapods/resolver.rb', line 421

def aggregate_for_dependency(dependency)
  if dependency && dependency.podspec_repo
    sources_manager.aggregate_for_dependency(dependency)
  elsif (locked_vertex = @locked_dependencies.vertex_named(dependency.name)) && (locked_dependency = locked_vertex.payload) && locked_dependency.podspec_repo
    sources_manager.aggregate_for_dependency(locked_dependency)
  else
    @aggregate ||= Source::Aggregate.new(sources)
  end
end

#before_resolutionVoid

Called before resolution starts.

Completely silence this, as we show nothing.

Returns:

  • (Void)


301
302
# File 'lib/cocoapods/resolver.rb', line 301

def before_resolution
end

#create_set_from_sources(dependency) ⇒ Set (private)

Returns Creates a set for the Pod of the given dependency from the sources. The set will contain all versions from all sources that include the Pod.

Parameters:

  • dependency (Dependency)

    The dependency for which the set is needed.

Returns:

  • (Set)

    Creates a set for the Pod of the given dependency from the sources. The set will contain all versions from all sources that include the Pod.



415
416
417
# File 'lib/cocoapods/resolver.rb', line 415

def create_set_from_sources(dependency)
  aggregate_for_dependency(dependency).search(dependency)
end

#dependencies_for(specification) ⇒ Array<Specification>

Returns the dependencies of specification.

Parameters:

  • specification (Specification)

    the specification whose own dependencies are being asked for.

Returns:



176
177
178
179
180
181
182
183
184
185
# File 'lib/cocoapods/resolver.rb', line 176

def dependencies_for(specification)
  root_name = Specification.root_name(specification.name)
  specification.all_dependencies.map do |dependency|
    if dependency.root_name == root_name
      dependency.dup.tap { |d| d.specific_version = specification.version }
    else
      dependency
    end
  end
end

#edge_is_valid_for_target_platform?(edge, target_platform) ⇒ Boolean (private)

Whether the given edge should be followed to find dependencies for the given target_platform.

Returns:

  • (Boolean)


580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/cocoapods/resolver.rb', line 580

def edge_is_valid_for_target_platform?(edge, target_platform)
  @edge_validity ||= Hash.new do |hash, edge_and_platform|
    e = edge_and_platform.edge
    platform = edge_and_platform.target_platform
    requirement_name = e.requirement.name

    hash[edge_and_platform] = e.origin.payload.all_dependencies(platform).any? do |dep|
      dep.name == requirement_name
    end
  end

  @edge_validity[EdgeAndPlatform.new(edge, target_platform)]
end

#find_cached_set(dependency) ⇒ Set (private)

Parameters:

  • dependency (Dependency)

    The dependency for which the set is needed.

Returns:

  • (Set)

    Loads or returns a previously initialized set for the Pod of the given dependency.

  • (Set)

    the cached set for a given dependency.



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/cocoapods/resolver.rb', line 374

def find_cached_set(dependency)
  name = dependency.root_name
  cached_sets[name] ||= begin
    if dependency.external_source
      spec = sandbox.specification(name)
      unless spec
        raise StandardError, '[Bug] Unable to find the specification ' \
          "for `#{dependency}`."
      end
      set = Specification::Set::External.new(spec)
    else
      set = create_set_from_sources(dependency)
    end

    unless set
      raise Molinillo::NoSuchDependencyError.new(dependency) # rubocop:disable Style/RaiseArgs
    end

    set
  end
end

#handle_resolver_error(error) ⇒ void (private)

This method returns an undefined value.

Handles errors that come out of a Molinillo::Resolver.

Parameters:



453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# File 'lib/cocoapods/resolver.rb', line 453

def handle_resolver_error(error)
  message = error.message
  type = Informative
  unless specs_updated?
    specs_update_message = "\n * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`."
  end
  case error
  when Molinillo::VersionConflict
    message = error.message_with_trees(
      :solver_name => 'CocoaPods',
      :possibility_type => 'pod',
      :version_for_spec => lambda(&:version),
      :additional_message_for_conflict => lambda do |o, name, conflict|
        local_pod_parent = conflict.requirement_trees.flatten.reverse.find(&:local?)
        if local_pod_parent && !specifications_for_dependency(conflict.requirement).empty? && !conflict.possibility && conflict.locked_requirement
          # Conflict was caused by a requirement from a local dependency.
          # Tell user to use `pod update`.
          o << "\n\nYou have either:#{specs_update_message}" \
               "\n * changed the constraints of dependency `#{name}` inside your development pod `#{local_pod_parent.name}`." \
               "\n   You should run `pod update #{name}` to apply changes you've made."
        elsif !conflict.possibility && conflict.locked_requirement && conflict.locked_requirement.external_source && conflict.locked_requirement.external_source[:podspec] &&
                                       conflict.requirement && conflict.requirement.external_source && conflict.requirement.external_source[:podspec]
          # The internal version of the Podspec doesn't match the external definition of a podspec
          o << "\nIt seems like you've changed the version of the dependency `#{name}` " \
          "and it differs from the version stored in `Pods/Local Podspecs`.\nYou should run `pod update #{name} --no-repo-update` to apply " \
          'changes made locally.'
        elsif (conflict.possibility && conflict.possibility.version.prerelease?) &&
            (conflict.requirement && !(
            conflict.requirement.prerelease? ||
            conflict.requirement.external_source)
            )
          # Conflict was caused by not specifying an explicit version for the requirement #[name],
          # and there is no available stable version satisfying constraints for the requirement.
          o << "\nThere are only pre-release versions available satisfying the following requirements:\n"
          conflict.requirements.values.flatten.uniq.each do |r|
            unless search_for(r).empty?
              o << "\n\t'#{name}', '#{r.requirement}'\n"
            end
          end
          o << "\nYou should explicitly specify the version in order to install a pre-release version"
        elsif !conflict.existing
          conflicts = conflict.requirements.values.flatten.uniq
          found_conflicted_specs = conflicts.reject { |c| search_for(c).empty? }
          if found_conflicted_specs.empty?
            # There are no existing specification inside any of the spec repos with given requirements.
            type = NoSpecFoundError
            dependencies = conflicts.count == 1 ? 'dependency' : 'dependencies'
            o << "\nNone of your spec sources contain a spec satisfying "\
              "the #{dependencies}: `#{conflicts.join(', ')}`." \
              "\n\nYou have either:#{specs_update_message}" \
              "\n * mistyped the name or version." \
              "\n * not added the source repo that hosts the Podspec to your Podfile."

          else
            o << "\nSpecs satisfying the `#{conflicts.join(', ')}` dependency were found, " \
              'but they required a higher minimum deployment target.'
          end
        end
      end,
    )
  when Molinillo::NoSuchDependencyError
    message += <<-EOS


You have either:#{specs_update_message}
 * mistyped the name or version.
 * not added the source repo that hosts the Podspec to your Podfile.
    EOS
  end
  raise type.new(message).tap { |e| e.set_backtrace(error.backtrace) }
end

#indicate_progressVoid

Called during resolution to indicate progress.

Completely silence this, as we show nothing.

Returns:

  • (Void)


319
320
# File 'lib/cocoapods/resolver.rb', line 319

def indicate_progress
end

#name_for(dependency) ⇒ String

Returns the name for the given dependency.

Parameters:

  • dependency (Dependency)

    the dependency whose name is being queried.

Returns:

  • (String)

    the name for the given dependency.



194
195
196
# File 'lib/cocoapods/resolver.rb', line 194

def name_for(dependency)
  dependency.name
end

#name_for_explicit_dependency_sourceString

Returns the user-facing name for a Podfile.

Returns:

  • (String)

    the user-facing name for a Podfile.



200
201
202
# File 'lib/cocoapods/resolver.rb', line 200

def name_for_explicit_dependency_source
  'Podfile'
end

#name_for_locking_dependency_sourceString

Returns the user-facing name for a Lockfile.

Returns:

  • (String)

    the user-facing name for a Lockfile.



206
207
208
# File 'lib/cocoapods/resolver.rb', line 206

def name_for_locking_dependency_source
  'Podfile.lock'
end

#outputUserInterface

The UI object the resolver should use for displaying user-facing output.

Returns:



291
292
293
# File 'lib/cocoapods/resolver.rb', line 291

def output
  UI
end

#requirement_for_locked_pod_named(name) ⇒ Requirement, Nil (private)

Returns The Requirement that locks the dependency with name name in #locked_dependencies.

Returns:

  • (Requirement, Nil)

    The Requirement that locks the dependency with name name in #locked_dependencies.



400
401
402
403
404
405
406
# File 'lib/cocoapods/resolver.rb', line 400

def requirement_for_locked_pod_named(name)
  if vertex = locked_dependencies.vertex_named(name)
    if dependency = vertex.payload
      dependency.requirement
    end
  end
end

#requirement_satisfied_by?(requirement, activated, spec) ⇒ Boolean

Determines whether the given requirement is satisfied by the given spec, in the context of the current activated dependency graph.

Parameters:

  • requirement (Dependency)

    the dependency in question.

  • activated (Molinillo::DependencyGraph)

    the current dependency graph in the resolution process.

  • spec (Specification)

    the specification in question.

Returns:

  • (Boolean)

    whether requirement is satisfied by spec in the context of the current activated dependency graph.



223
224
225
226
227
228
229
# File 'lib/cocoapods/resolver.rb', line 223

def requirement_satisfied_by?(requirement, activated, spec)
  version = spec.version
  return false unless requirement.requirement.satisfied_by?(version)
  return false unless valid_possibility_version_for_root_name?(requirement, activated, spec)
  return false unless spec_is_platform_compatible?(activated, requirement, spec)
  true
end

#resolveHash{TargetDefinition => Array<ResolverSpecification>}

Identifies the specifications that should be installed.

Returns:

  • (Hash{TargetDefinition => Array<ResolverSpecification>})

    resolver_specs_by_target the resolved specifications that need to be installed grouped by target definition.



86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/cocoapods/resolver.rb', line 86

def resolve
  dependencies = @podfile_dependency_cache.target_definition_list.flat_map do |target|
    @podfile_dependency_cache.target_definition_dependencies(target).each do |dep|
      next unless target.platform
      @platforms_by_dependency[dep].push(target.platform)
    end
  end.uniq
  @platforms_by_dependency.each_value(&:uniq!)
  @activated = Molinillo::Resolver.new(self, self).resolve(dependencies, locked_dependencies)
  resolver_specs_by_target
rescue Molinillo::ResolverError => e
  handle_resolver_error(e)
end

#resolver_specs_by_targetHash{Podfile::TargetDefinition => Array<ResolverSpecification>}

Note:

The returned specifications can be subspecs.

Returns the resolved specifications grouped by target.

Returns:

  • (Hash{Podfile::TargetDefinition => Array<ResolverSpecification>})

    returns the resolved specifications grouped by target.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/cocoapods/resolver.rb', line 105

def resolver_specs_by_target
  @resolver_specs_by_target ||= {}.tap do |resolver_specs_by_target|
    @podfile_dependency_cache.target_definition_list.each do |target|
      next if target.abstract? && !target.platform

      # can't use vertex.root? since that considers _all_ targets
      explicit_dependencies = @podfile_dependency_cache.target_definition_dependencies(target).map(&:name).to_set

      used_by_aggregate_target_by_spec_name = {}
      used_vertices_by_spec_name = {}

      # it's safe to make a single pass here since we iterate in topological order,
      # so all of the predecessors have been visited before we get to a node.
      # #tsort returns no-children vertices first, and we want them last (i.e. we want no-parent vertices first)
      @activated.tsort.reverse_each do |vertex|
        spec_name = vertex.name
        explicitly_included = explicit_dependencies.include?(spec_name)
        if explicitly_included || vertex.incoming_edges.any? { |edge| used_vertices_by_spec_name.key?(edge.origin.name) && edge_is_valid_for_target_platform?(edge, target.platform) }
          validate_platform(vertex.payload, target)
          used_vertices_by_spec_name[spec_name] = vertex
          used_by_aggregate_target_by_spec_name[spec_name] = vertex.payload.library_specification? &&
            (explicitly_included || vertex.predecessors.any? { |predecessor| used_by_aggregate_target_by_spec_name.fetch(predecessor.name, false) })
        end
      end

      resolver_specs_by_target[target] = used_vertices_by_spec_name.each_value.
        map do |vertex|
          payload = vertex.payload
          non_library = !used_by_aggregate_target_by_spec_name.fetch(vertex.name)
          spec_source = payload.respond_to?(:spec_source) && payload.spec_source
          ResolverSpecification.new(payload, non_library, spec_source)
        end.
        sort_by(&:name)
    end
  end
end

#search_for(dependency) ⇒ Array<Specification>

Returns (and caches) the specification that satisfy the given dependency.

Parameters:

  • dependency (Dependency)

    the dependency that is being searched for.

Returns:

  • (Array<Specification>)

    the specifications that satisfy the given dependency.



157
158
159
160
161
162
163
164
165
166
167
# File 'lib/cocoapods/resolver.rb', line 157

def search_for(dependency)
  @search[dependency] ||= begin
    additional_requirements = if locked_requirement = requirement_for_locked_pod_named(dependency.name)
                                [locked_requirement]
                              else
                                Array(@podfile_requirements_by_root_name[dependency.root_name])
                              end

    specifications_for_dependency(dependency, additional_requirements).freeze
  end
end

#sort_dependencies(dependencies, activated, conflicts) ⇒ Array<Dependency>

Sort dependencies so that the ones that are easiest to resolve are first. Easiest to resolve is (usually) defined by: 1) Is this dependency already activated? 2) How relaxed are the requirements? 3) Are there any conflicts for this dependency? 4) How many possibilities are there to satisfy this dependency?

Parameters:

  • dependencies (Array<Dependency>)

    the unsorted dependencies.

  • activated (Molinillo::DependencyGraph)

    the dependency graph of currently activated specs.

  • conflicts ({String => Array<Conflict>})

    the current conflicts.

Returns:

  • (Array<Dependency>)

    the sorted dependencies.



266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/cocoapods/resolver.rb', line 266

def sort_dependencies(dependencies, activated, conflicts)
  dependencies.sort_by! do |dependency|
    name = name_for(dependency)
    [
      activated.vertex_named(name).payload ? 0 : 1,
      dependency.external_source ? 0 : 1,
      dependency.prerelease? ? 0 : 1,
      conflicts[name] ? 0 : 1,
      search_for(dependency).count,
    ]
  end
end

#spec_is_platform_compatible?(dependency_graph, dependency, spec) ⇒ Boolean (private)

Returns whether the given spec is platform-compatible with the dependency graph, taking into account the dependency that has required the spec.

Parameters:

  • dependency_graph (Molinillo::DependencyGraph)
  • dependency (Dependency)
  • spec (Specification)

Returns:

  • (Boolean)


536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/cocoapods/resolver.rb', line 536

def spec_is_platform_compatible?(dependency_graph, dependency, spec)
  # This is safe since a pod will only be in locked dependencies if we're
  # using the same exact version
  return true if locked_dependencies.vertex_named(spec.name)

  vertex = dependency_graph.vertex_named(dependency.name)
  predecessors = vertex.recursive_predecessors.select(&:root?)
  predecessors << vertex if vertex.root?
  platforms_to_satisfy = predecessors.flat_map(&:explicit_requirements).flat_map { |r| @platforms_by_dependency[r] }.uniq

  available_platforms = spec.available_platforms

  platforms_to_satisfy.all? do |platform_to_satisfy|
    available_platforms.all? do |spec_platform|
      next true unless spec_platform.name == platform_to_satisfy.name
      # For non library specs all we care is to match by the platform name, not to satisfy the version.
      next true if spec.non_library_specification?
      platform_to_satisfy.supports?(spec_platform)
    end
  end
end

#specifications_for_dependency(dependency, additional_requirements = []) ⇒ Array<Specification> (private)

Returns available specifications which satisfy requirements of given dependency and additional requirements.

Parameters:

  • dependency (Dependency)

    The dependency whose requirements will be satisfied.

  • additional_requirements (Array<Requirement>) (defaults to: [])

    List of additional requirements which should also be satisfied.

Returns:

  • (Array<Specification>)

    List of specifications satisfying given requirements.



356
357
358
359
360
361
362
363
364
# File 'lib/cocoapods/resolver.rb', line 356

def specifications_for_dependency(dependency, additional_requirements = [])
  requirement_list = dependency.requirement.as_list + additional_requirements.flat_map(&:as_list)
  requirement_list.uniq!
  requirement = Requirement.new(requirement_list)
  find_cached_set(dependency).
    all_specifications(warn_for_multiple_pod_sources, requirement).
    map { |s| s.subspec_by_name(dependency.name, false, true) }.
    compact
end

#valid_possibility_version_for_root_name?(requirement, activated, spec) ⇒ Boolean (private)

Returns:

  • (Boolean)


231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/cocoapods/resolver.rb', line 231

def valid_possibility_version_for_root_name?(requirement, activated, spec)
  return true if prerelease_requirement = requirement.prerelease? || requirement.external_source || !spec.version.prerelease?

  activated.each do |vertex|
    next unless vertex.payload
    next unless Specification.root_name(vertex.name) == requirement.root_name

    prerelease_requirement ||= vertex.requirements.any? { |r| r.prerelease? || r.external_source }

    if vertex.payload.respond_to?(:version)
      return true if vertex.payload.version == spec.version
      break
    end
  end

  prerelease_requirement
end

#validate_platform(spec, target) ⇒ void (private)

This method returns an undefined value.

Ensures that a specification is compatible with the platform of a target.

Raises:

  • If the specification is not supported by the target.



437
438
439
440
441
442
443
444
445
# File 'lib/cocoapods/resolver.rb', line 437

def validate_platform(spec, target)
  return unless target_platform = target.platform
  return unless @validated_platforms.add?([spec.object_id, target_platform])
  unless spec.available_platforms.any? { |p| target_platform.to_sym == p.to_sym }
    raise Informative, "The platform of the target `#{target.name}` "     \
      "(#{target.platform}) is not compatible with `#{spec}`, which does "  \
      "not support `#{target.platform.string_name}`."
  end
end

#warn_for_multiple_pod_sourcesBoolean (private)

Returns whether to emit a warning when a pod is found in multiple sources.

Returns:

  • (Boolean)

    whether to emit a warning when a pod is found in multiple sources



596
597
598
# File 'lib/cocoapods/resolver.rb', line 596

def warn_for_multiple_pod_sources
  podfile.installation_options.warn_for_multiple_pod_sources
end