Commit ccfde782 authored by Eric Firestone's avatar Eric Firestone

Add ability to specify source with each dependency

Enables specifying a :source flag with each pod dependency
parent 484a0dbc
...@@ -8,6 +8,10 @@ To install release candidates run `[sudo] gem install cocoapods --pre` ...@@ -8,6 +8,10 @@ To install release candidates run `[sudo] gem install cocoapods --pre`
##### Enhancements ##### Enhancements
* Add support for specifying :source with a pod dependency.
[Eric Firestone](https://github.com/efirestone)
[#4486](https://github.com/CocoaPods/CocoaPods/pull/4486)
* Ask user to run `pod install` when a resource not found during in copy resources script. * Ask user to run `pod install` when a resource not found during in copy resources script.
[Muhammed Yavuz Nuzumlalı](https://github.com/manuyavuz) [Muhammed Yavuz Nuzumlalı](https://github.com/manuyavuz)
......
...@@ -7,7 +7,7 @@ GIT ...@@ -7,7 +7,7 @@ GIT
GIT GIT
remote: https://github.com/CocoaPods/Core.git remote: https://github.com/CocoaPods/Core.git
revision: ce26e1eb797a6ad1f1d94b0304a50c491bc41237 revision: 6c2544496ed201104e712dd95793ec7bc6a171e8
branch: master branch: master
specs: specs:
cocoapods-core (0.39.0) cocoapods-core (0.39.0)
......
...@@ -614,13 +614,21 @@ module Pod ...@@ -614,13 +614,21 @@ module Pod
def sources def sources
@sources ||= begin @sources ||= begin
sources = podfile.sources sources = podfile.sources
if sources.empty?
url = 'https://github.com/CocoaPods/Specs.git' # Add any sources specified using the :source flag on individual dependencies.
[SourcesManager.find_or_create_source_with_url(url)] dependency_sources = podfile.dependencies.map(&:podspec_repo).compact
all_dependencies_have_sources = dependency_sources.count == podfile.dependencies.count
if all_dependencies_have_sources
sources = dependency_sources
elsif sources.empty?
sources = ['https://github.com/CocoaPods/Specs.git']
else else
sources.map do |source_url| sources += dependency_sources
SourcesManager.find_or_create_source_with_url(source_url) end
end
sources.uniq.map do |source_url|
SourcesManager.find_or_create_source_with_url(source_url)
end end
end end
end end
......
...@@ -335,13 +335,17 @@ module Pod ...@@ -335,13 +335,17 @@ module Pod
# The dependency for which the set is needed. # The dependency for which the set is needed.
# #
def create_set_from_sources(dependency) def create_set_from_sources(dependency)
aggregate.search(dependency) aggregate_for_dependency(dependency).search(dependency)
end end
# @return [Source::Aggregate] The aggregate of the {#sources}. # @return [Source::Aggregate] The aggregate of the {#sources}.
# #
def aggregate def aggregate_for_dependency(dependency)
@aggregate ||= Source::Aggregate.new(sources.map(&:repo)) if dependency && dependency.podspec_repo
return SourcesManager.aggregate_for_dependency(dependency)
else
@aggregate ||= Source::Aggregate.new(sources.map(&:repo))
end
end end
# Ensures that a specification is compatible with the platform of a target. # Ensures that a specification is compatible with the platform of a target.
......
...@@ -11,7 +11,27 @@ module Pod ...@@ -11,7 +11,27 @@ module Pod
def aggregate def aggregate
return Source::Aggregate.new([]) unless config.repos_dir.exist? return Source::Aggregate.new([]) unless config.repos_dir.exist?
dirs = config.repos_dir.children.select(&:directory?) dirs = config.repos_dir.children.select(&:directory?)
Source::Aggregate.new(dirs) aggregate_with_repos(dirs)
end
# @return [Source::Aggregate] The aggregate of the sources from repos.
#
# @param [Dependency] dependency
# The dependency for which to find or build the appropriate.
# aggregate. If the dependency specifies a source podspec repo
# then only that source will be used, otherwise all sources
# will be used.
#
def aggregate_for_dependency(dependency)
if dependency.podspec_repo
source = source_with_url(dependency.podspec_repo)
raise StandardError, '[Bug] Failed to find known source with the URL ' \
"#{dependency.podspec_repo.inspect}" if source.nil?
aggregate_with_repos([source_dir(source.name)])
else
aggregate
end
end end
# @return [Array<Source>] The list of the sources with the given names. # @return [Array<Source>] The list of the sources with the given names.
...@@ -86,16 +106,16 @@ module Pod ...@@ -86,16 +106,16 @@ module Pod
sources(['master']) sources(['master'])
end end
# Search all the sources to match the set for the given dependency. # Search the appropriate sources to match the set for the given dependency.
# #
# @return [Set, nil] a set for a given dependency including all the # @return [Set, nil] a set for a given dependency including all the
# {Source} that contain the Pod. If no sources containing the # {Source} that contain the Pod. If no sources containing the
# Pod where found it returns nil. # Pod where found it returns nil.
# #
# @raise If no source including the set can be found. # @raise If no source can be found that includes the dependency.
# #
def search(dependency) def search(dependency)
aggregate.search(dependency) aggregate_for_dependency(dependency).search(dependency)
end end
# Search all the sources with the given search term. # Search all the sources with the given search term.
...@@ -448,6 +468,16 @@ module Pod ...@@ -448,6 +468,16 @@ module Pod
private private
# @return [Source::Aggregate] The aggregate of the sources from repos.
#
# @param [Array<Pathname>] repos
# The local file paths to one or more podspec repo caches.
#
def aggregate_with_repos(repos)
@aggregates_by_repos ||= {}
@aggregates_by_repos[repos] ||= Source::Aggregate.new(repos)
end
# @return [Bool] Whether the given path is writable by the current user. # @return [Bool] Whether the given path is writable by the current user.
# #
# @param [#to_s] path # @param [#to_s] path
......
Pod::Spec.new do |s|
s.name = 'CrossRepoDependent'
s.version = '1.0'
s.authors = 'Ned Needy', { 'Mr. Needy' => 'needy@example.local' }
s.homepage = 'http://example.local/cross-repo-dependent.html'
s.summary = 'I\'m dependent upon another spec repo to resolve my dependencies.'
s.description = 'I\'m dependent upon another spec repo to resolve my dependencies.'
s.platform = :ios
s.source = { :git => 'http://example.local/cross-repo-dependent.git', :tag => 'v1.0' }
s.source_files = 'Classes/*.{h,m}', 'Vendor'
s.dependency 'AFNetworking', '2.4.0'
s.license = {
:type => 'MIT',
:file => 'LICENSE',
:text => 'Permission is hereby granted ...'
}
end
...@@ -50,10 +50,11 @@ module Pod ...@@ -50,10 +50,11 @@ module Pod
end end
it "updates the Podfile's sources by default" do it "updates the Podfile's sources by default" do
config.stubs(:podfile).returns Podfile.new do podfile = Podfile.new do
source 'https://github.com/CocoaPods/Specs.git' source 'https://github.com/CocoaPods/Specs.git'
pod 'AFNetworking' pod 'AFNetworking'
end end
config.stubs(:podfile).returns(podfile)
config.stubs(:skip_repo_update?).returns(false) config.stubs(:skip_repo_update?).returns(false)
lockfile = mock lockfile = mock
lockfile.stubs(:version).returns(Version.new('1.0')) lockfile.stubs(:version).returns(Version.new('1.0'))
......
...@@ -62,6 +62,19 @@ module Pod ...@@ -62,6 +62,19 @@ module Pod
@analyzer.update_repositories @analyzer.update_repositories
end end
it 'does not update sources if there are no dependencies' do
podfile = Podfile.new do
source 'https://github.com/CocoaPods/Specs.git'
# No dependencies specified
end
config.skip_repo_update = false
config.verbose = true
SourcesManager.expects(:update).never
analyzer = Pod::Installer::Analyzer.new(config.sandbox, podfile, nil)
analyzer.update_repositories
end
it 'does not update non-git repositories' do it 'does not update non-git repositories' do
tmp_directory = Pathname(Dir.tmpdir) + 'CocoaPods' tmp_directory = Pathname(Dir.tmpdir) + 'CocoaPods'
FileUtils.mkdir_p(tmp_directory) FileUtils.mkdir_p(tmp_directory)
...@@ -88,6 +101,28 @@ module Pod ...@@ -88,6 +101,28 @@ module Pod
FileUtils.rm_rf(non_git_repo) FileUtils.rm_rf(non_git_repo)
end end
it 'updates sources specified with dependencies' do
repo_url = 'https://url/to/specs.git'
podfile = Podfile.new do
source 'repo_1'
pod 'BananaLib', '1.0', :source => repo_url
pod 'JSONKit', :source => repo_url
end
config.skip_repo_update = false
config.verbose = true
# Note that we are explicitly ignoring 'repo_1' since it isn't used.
source = mock
source.stubs(:name).returns('repo_2')
source.stubs(:repo).returns('/repo/cache/path')
SourcesManager.expects(:find_or_create_source_with_url).with(repo_url).returns(source)
SourcesManager.stubs(:git_repo?).with(source.repo).returns(true)
SourcesManager.expects(:update).once.with(source.name)
analyzer = Pod::Installer::Analyzer.new(config.sandbox, podfile, nil)
analyzer.update_repositories
end
#--------------------------------------# #--------------------------------------#
it 'generates the model to represent the target definitions' do it 'generates the model to represent the target definitions' do
...@@ -370,7 +405,7 @@ module Pod ...@@ -370,7 +405,7 @@ module Pod
@analyzer.send(:fetch_external_sources) @analyzer.send(:fetch_external_sources)
end end
xit 'it fetches the specification from either the sandbox or from the remote be default' do xit 'it fetches the specification from either the sandbox or from the remote by default' do
dependency = Dependency.new('Name', :git => 'www.example.com') dependency = Dependency.new('Name', :git => 'www.example.com')
ExternalSources::DownloaderSource.any_instance.expects(:specification_from_external).returns(Specification.new).once ExternalSources::DownloaderSource.any_instance.expects(:specification_from_external).returns(Specification.new).once
@resolver.send(:set_from_external_source, dependency) @resolver.send(:set_from_external_source, dependency)
...@@ -474,6 +509,7 @@ module Pod ...@@ -474,6 +509,7 @@ module Pod
it 'raises if no specs repo with that URL could be added' do it 'raises if no specs repo with that URL could be added' do
podfile = Podfile.new do podfile = Podfile.new do
source 'not-a-git-repo' source 'not-a-git-repo'
pod 'JSONKit', '1.4'
end end
@analyzer.instance_variable_set(:@podfile, podfile) @analyzer.instance_variable_set(:@podfile, podfile)
should.raise Informative do should.raise Informative do
...@@ -484,6 +520,7 @@ module Pod ...@@ -484,6 +520,7 @@ module Pod
it 'fetches a specs repo that is specified by the podfile' do it 'fetches a specs repo that is specified by the podfile' do
podfile = Podfile.new do podfile = Podfile.new do
source 'https://github.com/artsy/Specs.git' source 'https://github.com/artsy/Specs.git'
pod 'JSONKit', '1.4'
end end
@analyzer.instance_variable_set(:@podfile, podfile) @analyzer.instance_variable_set(:@podfile, podfile)
SourcesManager.expects(:find_or_create_source_with_url).once SourcesManager.expects(:find_or_create_source_with_url).once
......
...@@ -70,7 +70,7 @@ module Pod ...@@ -70,7 +70,7 @@ module Pod
] ]
end end
it 'it resolves specifications from external sources' do it 'resolves specifications from external sources' do
podspec = fixture('integration/Reachability/Reachability.podspec') podspec = fixture('integration/Reachability/Reachability.podspec')
spec = Specification.from_file(podspec) spec = Specification.from_file(podspec)
config.sandbox.expects(:specification).with('Reachability').returns(spec) config.sandbox.expects(:specification).with('Reachability').returns(spec)
...@@ -441,46 +441,6 @@ module Pod ...@@ -441,46 +441,6 @@ module Pod
e.message.should.match(/`pod update CocoaLumberjack`/) e.message.should.match(/`pod update CocoaLumberjack`/)
end end
it 'consults all sources when finding a matching spec' do
podfile = Podfile.new do
platform :ios
pod 'JSONKit', '> 2'
end
file = fixture('spec-repos/test_repo/JSONKit/999.999.999/JSONKit.podspec')
sources = SourcesManager.sources(%w(master test_repo))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '999.999.999'
spec.defined_in_file.should == file
sources = SourcesManager.sources(%w(test_repo master))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '999.999.999'
resolver.resolve.values.flatten.first.defined_in_file.should == file
end
it 'warns and chooses the first source when multiple sources contain ' \
'a pod' do
podfile = Podfile.new do
platform :ios
pod 'JSONKit', '1.4'
end
sources = SourcesManager.sources(%w(master test_repo))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '1.4'
spec.defined_in_file.should == fixture('spec-repos/master/Specs/JSONKit/1.4/JSONKit.podspec.json')
sources = SourcesManager.sources(%w(test_repo master))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '1.4'
resolver.resolve.values.flatten.first.defined_in_file.should == fixture('spec-repos/test_repo/JSONKit/1.4/JSONKit.podspec')
UI.warnings.should.match /multiple specifications/
end
describe 'concerning dependencies that are scoped by consumer platform' do describe 'concerning dependencies that are scoped by consumer platform' do
def resolve def resolve
Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all).resolve Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all).resolve
...@@ -550,6 +510,152 @@ module Pod ...@@ -550,6 +510,152 @@ module Pod
#-------------------------------------------------------------------------# #-------------------------------------------------------------------------#
describe 'Multiple sources' do
it 'consults all sources when finding a matching spec' do
podfile = Podfile.new do
platform :ios
pod 'JSONKit', '> 2'
end
file = fixture('spec-repos/test_repo/JSONKit/999.999.999/JSONKit.podspec')
sources = SourcesManager.sources(%w(master test_repo))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '999.999.999'
spec.defined_in_file.should == file
sources = SourcesManager.sources(%w(test_repo master))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '999.999.999'
resolver.resolve.values.flatten.first.defined_in_file.should == file
end
it 'warns and chooses the first source when multiple sources contain ' \
'a pod' do
podfile = Podfile.new do
platform :ios
pod 'JSONKit', '1.4'
end
sources = SourcesManager.sources(%w(master test_repo))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '1.4'
spec.defined_in_file.should == fixture('spec-repos/master/Specs/JSONKit/1.4/JSONKit.podspec.json')
sources = SourcesManager.sources(%w(test_repo master))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '1.4'
resolver.resolve.values.flatten.first.defined_in_file.should == fixture('spec-repos/test_repo/JSONKit/1.4/JSONKit.podspec')
UI.warnings.should.match /multiple specifications/
end
it 'does not warn when multiple sources contain a pod but a dependency ' \
'has an explicit source specified' do
test_repo_url = SourcesManager.source_with_name_or_url('test_repo').url
podfile = Podfile.new do
platform :ios
pod 'JSONKit', '1.4', :source => test_repo_url
end
sources = SourcesManager.sources(%w(master test_repo))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
resolver.resolve
UI.warnings.should.not.match /multiple specifications/
end
it 'fails to resolve a dependency with an explicit source even if it can be ' \
'resolved using the global sources' do
test_repo_url = SourcesManager.source_with_name_or_url('test_repo').url
podfile = Podfile.new do
platform :ios
pod 'JSONKit', '1.5pre', :source => test_repo_url
end
sources = SourcesManager.sources(%w(master))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
e = lambda { resolver.resolve }.should.raise Informative
e.message.should.match(/None of the spec sources contain a spec satisfying/)
e.message.should.match(/JSONKit/)
e.message.should.match(/\= 1.5pre/)
end
it 'resolves a dependency with an explicit source even if it can\'t be ' \
'resolved using the global sources' do
master_repo_url = SourcesManager.source_with_name_or_url('master').url
podfile = Podfile.new do
platform :ios
pod 'JSONKit', '1.5pre', :source => master_repo_url
end
sources = SourcesManager.sources(%w(test_repo))
sources.map(&:url).should.not.include(master_repo_url)
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '1.5pre'
spec.defined_in_file.should == fixture('spec-repos/master/Specs/JSONKit/1.5pre/JSONKit.podspec.json')
end
it 'uses explicit source repos for a dependency even when it\'s transitive' do
master_repo_url = SourcesManager.source_with_name_or_url('master').url
test_repo_url = SourcesManager.source_with_name_or_url('test_repo').url
podfile = Podfile.new do
platform :ios
# KeenClient has a dependency on JSONKit 1.4
pod 'KeenClient', '3.2.2', :source => master_repo_url
pod 'JSONKit', '1.4', :source => test_repo_url
end
sources = SourcesManager.sources(%w(master test_repo))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
resolver.resolve
possible_specs = resolver.search_for(Dependency.new('JSONKit', '1.4'))
# JSONKit, v1.4 exists in both repos, but we should only ever be offered the test_repo version.
possible_specs.count.should == 1
possible_specs.first.version.to_s.should == '1.4'
possible_specs.first.defined_in_file.should == fixture('spec-repos/test_repo/JSONKit/1.4/JSONKit.podspec')
end
it 'uses global source repos for resolving a transitive dependency even ' \
'if the root dependency has an explicit source' do
test_repo_url = SourcesManager.source_with_name_or_url('test_repo').url
podfile = Podfile.new do
platform :ios, '6.0'
pod 'CrossRepoDependent', '1.0', :source => test_repo_url
end
# CrossRepoDependent depends on AFNetworking which is only available in the master repo.
sources = SourcesManager.sources(%w(master))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
resolver.resolve
specs = resolver.resolve.values.flatten
specs.map(&:name).should ==
%w(AFNetworking AFNetworking/NSURLConnection AFNetworking/NSURLSession AFNetworking/Reachability) +
%w(AFNetworking/Security AFNetworking/Serialization AFNetworking/UIKit CrossRepoDependent)
afnetworking_spec = specs.find { |s| s.name == 'AFNetworking' }
afnetworking_spec.should.not.be.nil
afnetworking_spec.defined_in_file.should == fixture('spec-repos/master/Specs/AFNetworking/2.4.0/AFNetworking.podspec.json')
# Check that if the master source is not available the dependency cannot be resolved.
sources = SourcesManager.sources(%w(test_repo))
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
e = lambda { resolver.resolve }.should.raise Informative
e.message.should.match(/Unable to find a specification for/)
e.message.should.match(/`AFNetworking \(= 2.4.0\)`/)
end
end
#-------------------------------------------------------------------------#
describe 'Pre-release versions' do describe 'Pre-release versions' do
it 'resolves explicitly requested pre-release versions' do it 'resolves explicitly requested pre-release versions' do
@podfile = Podfile.new do @podfile = Podfile.new do
......
...@@ -53,6 +53,25 @@ module Pod ...@@ -53,6 +53,25 @@ module Pod
SourcesManager.all.map(&:name).should == %w(master test_repo) SourcesManager.all.map(&:name).should == %w(master test_repo)
end end
it 'includes all sources in an aggregate for a dependency if no source is specified' do
dependency = Dependency.new('JSONKit', '1.4')
aggregate = SourcesManager.aggregate_for_dependency(dependency)
aggregate.sources.map(&:name).should == %w(master test_repo)
end
it 'includes only the one source in an aggregate for a dependency if a source is specified' do
repo_url = 'https://url/to/specs.git'
dependency = Dependency.new('JSONKit', '1.4', :source => repo_url)
source = mock
source.stubs(:name).returns('repo')
SourcesManager.expects(:source_with_url).with(repo_url).returns(source)
aggregate = SourcesManager.aggregate_for_dependency(dependency)
aggregate.sources.map(&:name).should == [source.name]
end
it 'searches for the set of a dependency' do it 'searches for the set of a dependency' do
set = SourcesManager.search(Dependency.new('BananaLib')) set = SourcesManager.search(Dependency.new('BananaLib'))
set.class.should == Specification::Set set.class.should == Specification::Set
...@@ -135,7 +154,7 @@ module Pod ...@@ -135,7 +154,7 @@ module Pod
end end
end end
it "generates the search index before performing a search if it doesn't exits" do it "generates the search index before performing a search if it doesn't exist" do
SourcesManager.stubs(:all).returns([@test_source]) SourcesManager.stubs(:all).returns([@test_source])
Source::Aggregate.any_instance.expects(:generate_search_index_for_source).with(@test_source).returns('BananaLib' => ['BananaLib']) Source::Aggregate.any_instance.expects(:generate_search_index_for_source).with(@test_source).returns('BananaLib' => ['BananaLib'])
SourcesManager.updated_search_index = nil SourcesManager.updated_search_index = nil
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment