cocoapods 0.13.0 → 0.14.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,267 @@
1
+ module Pod
2
+ class Lockfile
3
+
4
+ # @return [Lockfile] Returns the Lockfile saved in path.
5
+ # Returns {nil} If the file can't be loaded.
6
+ #
7
+ def self.from_file(path)
8
+ return nil unless path.exist?
9
+ hash = YAML.load(File.open(path))
10
+ lockfile = Lockfile.new(hash)
11
+ lockfile.defined_in_file = path
12
+ lockfile
13
+ end
14
+
15
+ # @return [Lockfile] Generates a lockfile from a {Podfile} and the
16
+ # list of {Specifications} that were installed.
17
+ #
18
+ def self.generate(podfile, specs)
19
+ Lockfile.new(generate_hash_from_podfile(podfile, specs))
20
+ end
21
+
22
+ # @return [String] The file where this Lockfile is defined.
23
+ #
24
+ attr_accessor :defined_in_file
25
+
26
+ # @return [String] The hash used to initialize the Lockfile.
27
+ #
28
+ attr_reader :to_hash
29
+
30
+ # @param [Hash] hash A Hash representation of a Lockfile.
31
+ #
32
+ def initialize(hash)
33
+ @to_hash = hash
34
+ end
35
+
36
+ # @return [Array<String, Hash{String => Array[String]}>] The pods installed
37
+ # and their dependencies.
38
+ #
39
+ def pods
40
+ @pods ||= to_hash['PODS'] || []
41
+ end
42
+
43
+ # @return [Array<Dependency>] The Podfile dependencies used during the last
44
+ # install.
45
+ #
46
+ def dependencies
47
+ @dependencies ||= to_hash['DEPENDENCIES'].map { |dep| dependency_from_string(dep) } || []
48
+ end
49
+
50
+ # @return [Hash{String => Hash}] A hash where the name of the pods are
51
+ # the keys and the values are the parameters of an {AbstractExternalSource}
52
+ # of the dependency that required the pod.
53
+ #
54
+ def external_sources
55
+ @external_sources ||= to_hash["EXTERNAL SOURCES"] || {}
56
+ end
57
+
58
+ # @return [Array<String>] The names of the installed Pods.
59
+ #
60
+ def pods_names
61
+ @pods_names ||= pods.map do |pod|
62
+ pod = pod.keys.first unless pod.is_a?(String)
63
+ name_and_version_for_pod(pod)[0]
64
+ end
65
+ end
66
+
67
+ # @return [Hash{String => Version}] A Hash containing the name
68
+ # of the installed Pods as the keys and their corresponding {Version}
69
+ # as the values.
70
+ #
71
+ def pods_versions
72
+ unless @pods_versions
73
+ @pods_versions = {}
74
+ pods.each do |pod|
75
+ pod = pod.keys.first unless pod.is_a?(String)
76
+ name, version = name_and_version_for_pod(pod)
77
+ @pods_versions[name] = version
78
+ end
79
+ end
80
+ @pods_versions
81
+ end
82
+
83
+ # @return [Dependency] A dependency that describes the exact installed version
84
+ # of a Pod.
85
+ #
86
+ def dependency_for_installed_pod_named(name)
87
+ version = pods_versions[name]
88
+ raise Informative, "Attempt to lock a Pod without an known version." unless version
89
+ dependency = Dependency.new(name, version)
90
+ if external_source = external_sources[name]
91
+ dependency.external_source = Dependency::ExternalSources.from_params(dependency.name, external_source)
92
+ end
93
+ dependency
94
+ end
95
+
96
+ # @param [String] The string that describes a {Specification} generated
97
+ # from {Specification#to_s}.
98
+ #
99
+ # @example Strings examples
100
+ # "libPusher"
101
+ # "libPusher (1.0)"
102
+ # "libPusher (HEAD based on 1.0)"
103
+ # "RestKit/JSON"
104
+ #
105
+ # @return [String, Version] The name and the version of a
106
+ # pod.
107
+ #
108
+ def name_and_version_for_pod(string)
109
+ match_data = string.match(/(\S*) \((.*)\)/)
110
+ name = match_data[1]
111
+ vers = Version.from_string(match_data[2])
112
+ [name, vers]
113
+ end
114
+
115
+ # @param [String] The string that describes a {Dependency} generated
116
+ # from {Dependency#to_s}.
117
+ #
118
+ # @example Strings examples
119
+ # "libPusher"
120
+ # "libPusher (= 1.0)"
121
+ # "libPusher (~> 1.0.1)"
122
+ # "libPusher (> 1.0, < 2.0)"
123
+ # "libPusher (HEAD)"
124
+ # "libPusher (from `www.example.com')"
125
+ # "libPusher (defined in Podfile)"
126
+ # "RestKit/JSON"
127
+ #
128
+ # @return [Dependency] The dependency described by the string.
129
+ #
130
+ def dependency_from_string(string)
131
+ match_data = string.match(/(\S*)( (.*))?/)
132
+ name = match_data[1]
133
+ version = match_data[2]
134
+ version = version.gsub(/[()]/,'') if version
135
+ case version
136
+ when nil
137
+ Dependency.new(name)
138
+ when /defined in Podfile/
139
+ # @TODO: store the whole spec?, the version?
140
+ Dependency.new(name)
141
+ when /from `(.*)'/
142
+ external_source_info = external_sources[name]
143
+ Dependency.new(name, external_source_info)
144
+ when /HEAD/
145
+ # @TODO: find a way to serialize from the Downloader the information
146
+ # necessary to restore a head version.
147
+ Dependency.new(name, :head)
148
+ else
149
+ Dependency.new(name, version)
150
+ end
151
+ end
152
+
153
+ # Analyzes the {Lockfile} and detects any changes applied to the {Podfile}
154
+ # since the last installation.
155
+ #
156
+ # For each Pod, it detects one state among the following:
157
+ #
158
+ # - added: Pods that weren't present in the Podfile.
159
+ # - changed: Pods that were present in the Podfile but changed:
160
+ # - Pods whose version is not compatible anymore with Podfile,
161
+ # - Pods that changed their head or external options.
162
+ # - removed: Pods that were removed form the Podfile.
163
+ # - unchanged: Pods that are still compatible with Podfile.
164
+ #
165
+ # @TODO: detect changes for inline dependencies?
166
+ #
167
+ # @return [Hash{Symbol=>Array[Strings]}] A hash where pods are grouped
168
+ # by the state in which they are.
169
+ #
170
+ def detect_changes_with_podfile(podfile)
171
+ previous_podfile_deps = dependencies.map(&:name)
172
+ user_installed_pods = pods_names.reject { |name| !previous_podfile_deps.include?(name) }
173
+ deps_to_install = podfile.dependencies.dup
174
+
175
+ result = {}
176
+ result[:added] = []
177
+ result[:changed] = []
178
+ result[:removed] = []
179
+ result[:unchanged] = []
180
+
181
+ user_installed_pods.each do |pod_name|
182
+ dependency = deps_to_install.find { |d| d.name == pod_name }
183
+ deps_to_install.delete(dependency)
184
+ version = pods_versions[pod_name]
185
+ external_source = Dependency::ExternalSources.from_params(pod_name, external_sources[pod_name])
186
+
187
+ if dependency.nil?
188
+ result[:removed] << pod_name
189
+ elsif !dependency.match_version?(version) || dependency.external_source != external_source
190
+ result[:changed] << pod_name
191
+ else
192
+ result[:unchanged] << pod_name
193
+ end
194
+ end
195
+
196
+ deps_to_install.each do |dependency|
197
+ result[:added] << dependency.name
198
+ end
199
+ result
200
+ end
201
+
202
+ # @return [void] Writes the Lockfile to {#path}.
203
+ #
204
+ def write_to_disk(path)
205
+ File.open(path, 'w') {|f| f.write(to_yaml) }
206
+ defined_in_file = path
207
+ end
208
+
209
+ # @return [String] A string useful to represent the Lockfile in a message
210
+ # presented to the user.
211
+ #
212
+ def to_s
213
+ "Podfile.lock"
214
+ end
215
+
216
+ # @return [String] The YAML representation of the Lockfile, used for
217
+ # serialization.
218
+ #
219
+ def to_yaml
220
+ to_hash.to_yaml.gsub(/^--- ?\n/,"").gsub(/^([A-Z])/,"\n\\1")
221
+ end
222
+
223
+ # @return [Hash] The Hash representation of the Lockfile generated from
224
+ # a given Podfile and the list of resolved Specifications.
225
+ #
226
+ def self.generate_hash_from_podfile(podfile, specs)
227
+ hash = {}
228
+
229
+ # Get list of [name, dependencies] pairs.
230
+ pod_and_deps = specs.map do |spec|
231
+ [spec.to_s, spec.dependencies.map(&:to_s).sort]
232
+ end.uniq
233
+
234
+ # Merge dependencies of iOS and OS X version of the same pod.
235
+ tmp = {}
236
+ pod_and_deps.each do |name, deps|
237
+ if tmp[name]
238
+ tmp[name].concat(deps).uniq!
239
+ else
240
+ tmp[name] = deps
241
+ end
242
+ end
243
+ pod_and_deps = tmp.sort_by(&:first).map do |name, deps|
244
+ deps.empty? ? name : { name => deps }
245
+ end
246
+ hash["PODS"] = pod_and_deps
247
+
248
+ hash["DEPENDENCIES"] = podfile.dependencies.map{ |d| d.to_s }.sort
249
+
250
+ external_sources = {}
251
+ deps = podfile.dependencies.select(&:external?).sort{ |d, other| d.name <=> other.name}
252
+ deps.each{ |d| external_sources[d.name] = d.external_source.params }
253
+ hash["EXTERNAL SOURCES"] = external_sources unless external_sources.empty?
254
+
255
+ checksums = {}
256
+ specs.select { |spec| !spec.defined_in_file.nil? }.each do |spec|
257
+ checksum = Digest::SHA1.hexdigest(File.read(spec.defined_in_file))
258
+ checksum = checksum.encode('UTF-8') if checksum.respond_to?(:encode)
259
+ checksums[spec.name] = checksum
260
+ end
261
+ hash["SPEC CHECKSUMS"] = checksums unless checksums.empty?
262
+ hash["COCOAPODS"] = VERSION
263
+ hash
264
+ end
265
+ end
266
+ end
267
+
@@ -538,5 +538,9 @@ module Pod
538
538
 
539
539
  def validate!
540
540
  end
541
+
542
+ def to_s
543
+ "Podfile"
544
+ end
541
545
  end
542
546
  end
@@ -31,15 +31,20 @@ module Pod
31
31
 
32
32
  # Shortcut access to the `Pods' PBXGroup.
33
33
  def pods
34
- groups.find { |g| g.name == 'Pods' } || groups.new({ 'name' => 'Pods' })
34
+ @pods ||= groups.where(:name => 'Pods') || groups.new('name' => 'Pods')
35
+ end
36
+
37
+ # Shortcut access to the `Local Pods' PBXGroup.
38
+ def local_pods
39
+ @local_pods ||= groups.where(:name => 'Local Pods') || groups.new('name' => 'Local Pods')
35
40
  end
36
41
 
37
42
  # Adds a group as child to the `Pods' group namespacing subspecs.
38
- def add_spec_group(name)
39
- groups = pods.groups
43
+ def add_spec_group(name, parent_group)
44
+ groups = parent_group.groups
40
45
  group = nil
41
46
  name.split('/').each do |name|
42
- group = groups.find { |g| g.name == name } || groups.new({ 'name' => name })
47
+ group = groups.where(:name => name) || groups.new('name' => name)
43
48
  groups = group.groups
44
49
  end
45
50
  group
@@ -4,44 +4,169 @@ module Pod
4
4
  class Resolver
5
5
  include Config::Mixin
6
6
 
7
- attr_reader :podfile, :sandbox
8
- attr_accessor :cached_sets, :cached_sources
7
+ # @return [Bool] Whether the resolver should find the pods to install or
8
+ # the pods to update.
9
+ #
10
+ attr_accessor :update_mode
11
+
12
+ # @return [Bool] Whether the resolver should update the external specs
13
+ # in the resolution process.
14
+ #
15
+ attr_accessor :update_external_specs
16
+
17
+ # @return [Podfile] The Podfile used by the resolver.
18
+ #
19
+ attr_reader :podfile
20
+
21
+ # @return [Lockfile] The Lockfile used by the resolver.
22
+ #
23
+ attr_reader :lockfile
24
+
25
+ # @return [Sandbox] The Sandbox used by the resolver to find external
26
+ # dependencies.
27
+ #
28
+ attr_reader :sandbox
29
+
30
+ # @return [Array<Strings>] The name of the pods that have an
31
+ # external source.
32
+ #
33
+ attr_reader :pods_from_external_sources
34
+
35
+ # @return [Array<Set>] A cache of the sets used to resolve the dependencies.
36
+ #
37
+ attr_reader :cached_sets
38
+
39
+ # @return [Source::Aggregate] A cache of the sources needed to find the
40
+ # podspecs.
41
+ #
42
+ attr_reader :cached_sources
43
+
44
+ # @return [Hash{Podfile::TargetDefinition => Array<Specification>}]
45
+ # Returns the resolved specifications grouped by target.
46
+ #
47
+ attr_reader :specs_by_target
48
+
49
+ def initialize(podfile, lockfile, sandbox)
50
+ @podfile = podfile
51
+ @lockfile = lockfile
52
+ @sandbox = sandbox
53
+ @update_external_specs = true
9
54
 
10
- def initialize(podfile, sandbox)
11
- @podfile = podfile
12
- @sandbox = sandbox
13
55
  @cached_sets = {}
14
56
  @cached_sources = Source::Aggregate.new
15
- @log_indent = 0;
16
57
  end
17
58
 
59
+ # Identifies the specifications that should be installed according whether
60
+ # the resolver is in update mode or not.
61
+ #
62
+ # @return [Hash{Podfile::TargetDefinition => Array<Specification>}] specs_by_target
63
+ #
18
64
  def resolve
19
- @specs = {}
20
- targets_and_specs = {}
65
+ @cached_specs = {}
66
+ @specs_by_target = {}
67
+ @pods_from_external_sources = []
68
+ @pods_to_lock = []
69
+ @log_indent = 0
70
+
71
+ if @lockfile
72
+ puts "\nFinding added, modified or removed dependencies:".green if config.verbose?
73
+ @pods_by_state = @lockfile.detect_changes_with_podfile(podfile)
74
+ if config.verbose?
75
+ @pods_by_state.each do |symbol, pod_names|
76
+ case symbol
77
+ when :added
78
+ mark = "A".green
79
+ when :changed
80
+ mark = "M".yellow
81
+ when :removed
82
+ mark = "R".red
83
+ when :unchanged
84
+ mark = "-"
85
+ end
86
+ pod_names.each do |pod_name|
87
+ puts " #{mark} " << pod_name
88
+ end
89
+ end
90
+ end
91
+ @pods_to_lock = (lockfile.pods_names - @pods_by_state[:added] - @pods_by_state[:changed] - @pods_by_state[:removed]).uniq
92
+ end
21
93
 
22
94
  @podfile.target_definitions.values.each do |target_definition|
23
- puts "\nResolving dependencies for target `#{target_definition.name}' (#{target_definition.platform})".green if config.verbose?
95
+ puts "\nResolving dependencies for target `#{target_definition.name}' (#{target_definition.platform}):".green if config.verbose?
24
96
  @loaded_specs = []
25
97
  find_dependency_specs(@podfile, target_definition.dependencies, target_definition)
26
- targets_and_specs[target_definition] = @specs.values_at(*@loaded_specs).sort_by(&:name)
98
+ @specs_by_target[target_definition] = @cached_specs.values_at(*@loaded_specs).sort_by(&:name)
99
+ end
100
+
101
+ @cached_specs.values.sort_by(&:name)
102
+ @specs_by_target
103
+ end
104
+
105
+ # @return [Array<Specification>] The specifications loaded by the resolver.
106
+ #
107
+ def specs
108
+ @cached_specs.values.uniq
109
+ end
110
+
111
+ # @return [Bool] Whether a pod should be installed/reinstalled.
112
+ #
113
+ def should_install?(name)
114
+ pods_to_install.include? name
115
+ end
116
+
117
+ # @return [Array<Strings>] The name of the pods that should be
118
+ # installed/reinstalled.
119
+ #
120
+ def pods_to_install
121
+ unless @pods_to_install
122
+ if lockfile
123
+ @pods_to_install = specs.select do |spec|
124
+ spec.version != lockfile.pods_versions[spec.pod_name]
125
+ end.map(&:name)
126
+ if update_mode
127
+ @pods_to_install += specs.select do |spec|
128
+ spec.version.head? || pods_from_external_sources.include?(spec.pod_name)
129
+ end.map(&:name)
130
+ end
131
+ @pods_to_install += @pods_by_state[:added] + @pods_by_state[:changed]
132
+ else
133
+ @pods_to_install = specs.map(&:name)
134
+ end
27
135
  end
136
+ @pods_to_install
137
+ end
28
138
 
29
- @specs.values.sort_by(&:name)
30
- targets_and_specs
139
+ # @return [Array<Strings>] The name of the pods that were installed
140
+ # but don't have any dependency anymore. The name of the Pods are
141
+ # stripped from subspecs.
142
+ #
143
+ def removed_pods
144
+ return [] unless lockfile
145
+ unless @removed_pods
146
+ previusly_installed = lockfile.pods_names.map { |pod_name| pod_name.split('/').first }
147
+ installed = specs.map { |spec| spec.name.split('/').first }
148
+ @removed_pods = previusly_installed - installed
149
+ end
150
+ @removed_pods
31
151
  end
32
152
 
33
153
  private
34
154
 
155
+ # @return [Set] The cached set for a given dependency.
156
+ #
35
157
  def find_cached_set(dependency, platform)
36
158
  set_name = dependency.name.split('/').first
37
159
  @cached_sets[set_name] ||= begin
38
160
  if dependency.specification
39
161
  Specification::Set::External.new(dependency.specification)
40
162
  elsif external_source = dependency.external_source
41
- # The platform isn't actually being used by the LocalPod instance
42
- # that's being used behind the scenes, but passing it anyways for
43
- # completeness sake.
44
- specification = external_source.specification_from_sandbox(@sandbox, platform)
163
+ if update_mode && update_external_specs
164
+ # Always update external sources in update mode.
165
+ specification = external_source.specification_from_external(@sandbox, platform)
166
+ else
167
+ # Don't update external sources in install mode if not needed.
168
+ specification = external_source.specification_from_sandbox(@sandbox, platform)
169
+ end
45
170
  set = Specification::Set::External.new(specification)
46
171
  if dependency.subspec_dependency?
47
172
  @cached_sets[dependency.top_level_spec_name] ||= set
@@ -53,29 +178,48 @@ module Pod
53
178
  end
54
179
  end
55
180
 
181
+ # Resolves the dependencies of a specification and stores them in @cached_specs
182
+ #
183
+ # @param [Specification] dependent_specification
184
+ # @param [Array<Dependency>] dependencies
185
+ # @param [TargetDefinition] target_definition
186
+ #
187
+ # @return [void]
188
+ #
56
189
  def find_dependency_specs(dependent_specification, dependencies, target_definition)
57
190
  @log_indent += 1
58
191
  dependencies.each do |dependency|
192
+ # Replace the dependency with a more specific one if the pod is already installed.
193
+ if !update_mode && @pods_to_lock.include?(dependency.name)
194
+ dependency = lockfile.dependency_for_installed_pod_named(dependency.name)
195
+ end
196
+
59
197
  puts ' ' * @log_indent + "- #{dependency}" if config.verbose?
60
198
  set = find_cached_set(dependency, target_definition.platform)
61
- set.required_by(dependent_specification)
199
+ set.required_by(dependency, dependent_specification.to_s)
200
+
62
201
  # Ensure we don't resolve the same spec twice for one target
63
202
  unless @loaded_specs.include?(dependency.name)
64
203
  spec = set.specification_by_name(dependency.name)
204
+ @pods_from_external_sources << spec.pod_name if dependency.external?
65
205
  @loaded_specs << spec.name
66
- @specs[spec.name] = spec
206
+ @cached_specs[spec.name] = spec
67
207
  # Configure the specification
68
208
  spec.activate_platform(target_definition.platform)
69
209
  spec.version.head = dependency.head?
70
210
  # And recursively load the dependencies of the spec.
71
211
  find_dependency_specs(spec, spec.dependencies, target_definition) if spec.dependencies
72
212
  end
73
- validate_platform!(spec || @specs[dependency.name], target_definition)
213
+ validate_platform(spec || @cached_specs[dependency.name], target_definition)
74
214
  end
75
215
  @log_indent -= 1
76
216
  end
77
217
 
78
- def validate_platform!(spec, target)
218
+ # Ensures that a spec is compatible with platform of a target.
219
+ #
220
+ # @raises If the spec is not supported by the target.
221
+ #
222
+ def validate_platform(spec, target)
79
223
  unless spec.available_platforms.any? { |platform| target.platform.supports?(platform) }
80
224
  raise Informative, "[!] The platform of the target `#{target.name}' (#{target.platform}) is not compatible with `#{spec}' which has a minimun requirement of #{spec.available_platforms.join(' - ')}.".red
81
225
  end