cocoapods 0.13.0 → 0.14.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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