cocoapods 0.10.0 → 0.11.0

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.
data/CHANGELOG.md CHANGED
@@ -1,4 +1,21 @@
1
- ## 0.10.0 (Unreleased)
1
+ ## 0.11.0
2
+
3
+ [CocoaPods](https://github.com/CocoaPods/CocoaPods/compare/0.10.0...master)
4
+
5
+ ###### Enhancements
6
+
7
+ - Added support for public headers. [#440]
8
+ - Added `pod repo lint`. [#423]
9
+ - Improved support for `:head` option and svn repositories.
10
+ - When integrating Pods with a project without "Frameworks" group in root of the project, raise an informative message. [#431](https://github.com/CocoaPods/CocoaPods/pull/431)
11
+ - Dropped support for legacy `config.ios?` and `config.osx?`
12
+
13
+ ###### Bug fixes
14
+
15
+ - Version message now correctly terminates with a 0 exit status.
16
+ - Resolved an issue that lead to git error messages in the error report.
17
+
18
+ ## 0.10.0
2
19
 
3
20
  [CocoaPods](http://git.io/4i75YA)
4
21
 
data/lib/cocoapods.rb CHANGED
@@ -13,7 +13,7 @@ unless Gem::Version::Requirement.new('>= 1.4.0').satisfied_by?(Gem::Version.new(
13
13
  end
14
14
 
15
15
  module Pod
16
- VERSION = '0.10.0'
16
+ VERSION = '0.11.0'
17
17
 
18
18
  class PlainInformative < StandardError
19
19
  end
@@ -5,6 +5,7 @@ module Pod
5
5
  autoload :ErrorReport, 'cocoapods/command/error_report'
6
6
  autoload :Install, 'cocoapods/command/install'
7
7
  autoload :List, 'cocoapods/command/list'
8
+ autoload :Linter, 'cocoapods/command/linter'
8
9
  autoload :Presenter, 'cocoapods/command/presenter'
9
10
  autoload :Push, 'cocoapods/command/push'
10
11
  autoload :Repo, 'cocoapods/command/repo'
@@ -86,7 +87,10 @@ module Pod
86
87
 
87
88
  def self.parse(*argv)
88
89
  argv = ARGV.new(argv)
89
- raise PlainInformative, VERSION if argv.option('--version')
90
+ if argv.option('--version')
91
+ puts VERSION
92
+ exit!(0)
93
+ end
90
94
 
91
95
  show_help = argv.option('--help')
92
96
  Config.instance.silent = argv.option('--silent')
@@ -10,7 +10,7 @@ module Pod
10
10
  def report(error)
11
11
  return <<-EOS
12
12
 
13
- #{'――― MARKDOWN TEMPLATE ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――'.reversed}
13
+ #{'――― MARKDOWN TEMPLATE ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――'.reversed}
14
14
 
15
15
  ### Report
16
16
 
@@ -59,14 +59,14 @@ EOS
59
59
 
60
60
  def markdown_podfile
61
61
  return '' unless Config.instance.project_podfile && Config.instance.project_podfile.exist?
62
- <<-EOS
62
+ <<-EOS
63
63
 
64
64
  ### Podfile
65
65
 
66
66
  ```ruby
67
- #{Config.instance.project_podfile.read.strip}
67
+ #{Config.instance.project_podfile.read.strip}
68
68
  ```
69
- EOS
69
+ EOS
70
70
  end
71
71
 
72
72
  def error_from_podfile(error)
@@ -89,8 +89,8 @@ EOS
89
89
  Pod::Source.all.map do |source|
90
90
  repo = source.repo
91
91
  Dir.chdir(repo) do
92
- url = `git config --get remote.origin.url`.strip
93
- sha = `git rev-parse HEAD`.strip
92
+ url = `git config --get remote.origin.url 2>&1`.strip
93
+ sha = `git rev-parse HEAD 2>&1`.strip
94
94
  "#{repo.basename} - #{url} @ #{sha}"
95
95
  end
96
96
  end
@@ -0,0 +1,292 @@
1
+ module Pod
2
+ class Command
3
+ class Linter
4
+ include Config::Mixin
5
+
6
+ # TODO: Add check to ensure that attributes inherited by subspecs are not duplicated ?
7
+
8
+ attr_accessor :quick, :no_clean, :repo_path
9
+ attr_reader :spec, :file
10
+ attr_reader :errors, :warnings, :notes
11
+
12
+ def initialize(podspec)
13
+ @file = podspec
14
+ end
15
+
16
+ def spec_name
17
+ name = file.basename('.*').to_s
18
+ if @spec
19
+ name << " (#{spec.version})"
20
+ elsif @repo_path
21
+ name << " (#{file.dirname.basename})"
22
+ end
23
+ name
24
+ end
25
+
26
+ # Takes an array of podspec files and lints them all
27
+ #
28
+ # It returns true if the spec passed validation
29
+ #
30
+ def lint
31
+ @errors, @warnings, @notes = [], [], []
32
+ @platform_errors, @platform_warnings, @platform_notes = {}, {}, {}
33
+
34
+ if !deprecation_errors.empty?
35
+ @errors = deprecation_errors
36
+ @errors << "#{spec_name} [!] Fatal errors found skipping the rest of the validation"
37
+ else
38
+ @spec = Specification.from_file(file)
39
+ platforms = spec.available_platforms
40
+
41
+ if @repo_path
42
+ expected_path = "#{@spec.name}/#{@spec.version}/#{@spec.name}.podspec"
43
+ path = file.relative_path_from(@repo_path).to_s
44
+ @errors << "Incorrect path, the path is `#{file}` and should be `#{expected_path}`" unless path.end_with?(expected_path)
45
+ end
46
+
47
+ platforms.each do |platform|
48
+ @platform_errors[platform], @platform_warnings[platform], @platform_notes[platform] = [], [], []
49
+
50
+ spec.activate_platform(platform)
51
+ @platform = platform
52
+ puts "\n\n#{spec} - Analyzing on #{platform} platform.".green.reversed if config.verbose? && !@quick
53
+
54
+ # Skip validation if there are errors in the podspec as it would result in a crash
55
+ if !podspec_errors.empty?
56
+ @platform_errors[platform] += podspec_errors
57
+ @platform_notes[platform] << "#{platform.name} [!] Fatal errors found skipping the rest of the validation"
58
+ else
59
+ @platform_warnings[platform] += podspec_warnings
60
+ peform_extensive_analysis unless quick
61
+ end
62
+ end
63
+
64
+ # Get common messages
65
+ @errors += @platform_errors.values.reduce(:&)
66
+ @warnings += @platform_warnings.values.reduce(:&)
67
+ @notes += @platform_notes.values.reduce(:&)
68
+
69
+ platforms.each do |platform|
70
+ # Mark platform specific messages
71
+ @errors += (@platform_errors[platform] - @errors).map {|m| "[#{platform}] #{m}"}
72
+ @warnings += (@platform_warnings[platform] - @warnings).map {|m| "[#{platform}] #{m}"}
73
+ @notes += (@platform_notes[platform] - @notes).map {|m| "[#{platform}] #{m}"}
74
+ end
75
+ end
76
+ end
77
+
78
+ def result_type
79
+ return :error unless errors.empty?
80
+ return :warning unless warnings.empty?
81
+ return :note unless notes.empty?
82
+ :success
83
+ end
84
+
85
+ # Performs platform specific analysis.
86
+ # It requires to download the source at each iteration
87
+ #
88
+ def peform_extensive_analysis
89
+ set_up_lint_environment
90
+ install_pod
91
+ if `which xcodebuild`.strip.empty?
92
+ puts "Skipping compilation with `xcodebuild' because it can't be found.\n".yellow if config.verbose?
93
+ else
94
+ puts "Building with xcodebuild.\n".yellow if config.verbose?
95
+ # treat xcodebuild warnings as notes because the spec maintainer might not be the author of the library
96
+ xcodebuild_output.each { |msg| ( msg.include?('error: ') ? @platform_errors[@platform] : @platform_notes[@platform] ) << msg }
97
+ end
98
+ @platform_errors[@platform] += file_patterns_errors
99
+ @platform_warnings[@platform] += file_patterns_warnings
100
+ tear_down_lint_environment
101
+ end
102
+
103
+ def install_pod
104
+ podfile = podfile_from_spec
105
+ config.verbose
106
+ installer = Installer.new(podfile)
107
+ installer.install!
108
+ @pod = installer.pods.find { |pod| pod.top_specification == spec }
109
+ config.silent
110
+ end
111
+
112
+ def podfile_from_spec
113
+ name = spec.name
114
+ podspec = file.realpath.to_s
115
+ platform = @platform
116
+ podfile = Pod::Podfile.new do
117
+ platform(platform.to_sym, platform.deployment_target)
118
+ pod name, :podspec => podspec
119
+ end
120
+ podfile
121
+ end
122
+
123
+ def set_up_lint_environment
124
+ tmp_dir.rmtree if tmp_dir.exist?
125
+ tmp_dir.mkpath
126
+ @original_config = Config.instance.clone
127
+ config.project_root = tmp_dir
128
+ config.project_pods_root = tmp_dir + 'Pods'
129
+ config.silent = !config.verbose
130
+ config.integrate_targets = false
131
+ config.generate_docs = false
132
+ end
133
+
134
+ def tear_down_lint_environment
135
+ tmp_dir.rmtree unless no_clean
136
+ Config.instance = @original_config
137
+ end
138
+
139
+ def tmp_dir
140
+ Pathname.new('/tmp/CocoaPods/Lint')
141
+ end
142
+
143
+ def pod_dir
144
+ tmp_dir + 'Pods' + spec.name
145
+ end
146
+
147
+ # It reads a podspec file and checks for strings corresponding
148
+ # to features that are or will be deprecated
149
+ #
150
+ # @return [Array<String>]
151
+ #
152
+ def deprecation_errors
153
+ text = @file.read
154
+ deprecations = []
155
+ deprecations << "`config.ios?' and `config.osx?' are deprecated" if text. =~ /config\..?os.?/
156
+ deprecations << "clean_paths are deprecated and ignored (use preserve_paths)" if text. =~ /clean_paths/
157
+ deprecations
158
+ end
159
+
160
+ # @return [Array<String>] List of the fatal defects detected in a podspec
161
+ def podspec_errors
162
+ messages = []
163
+ messages << "The name of the spec should match the name of the file" unless names_match?
164
+ messages << "Unrecognized platfrom" unless platform_valid?
165
+ messages << "Missing name" unless spec.name
166
+ messages << "Missing version" unless spec.version
167
+ messages << "Missing summary" unless spec.summary
168
+ messages << "Missing homepage" unless spec.homepage
169
+ messages << "Missing author(s)" unless spec.authors
170
+ messages << "Missing or invalid source: #{spec.source}" unless source_valid?
171
+
172
+ # attributes with multiplatform values
173
+ return messages unless platform_valid?
174
+ messages << "The spec appears to be empty (no source files, resources, or preserve paths)" if spec.source_files.empty? && spec.subspecs.empty? && spec.resources.empty? && spec.preserve_paths.empty?
175
+ messages += paths_starting_with_a_slash_errors
176
+ messages += deprecation_errors
177
+ messages
178
+ end
179
+
180
+ def names_match?
181
+ return true unless spec.name
182
+ root_name = spec.name.match(/[^\/]*/)[0]
183
+ file.basename.to_s == root_name + '.podspec'
184
+ end
185
+
186
+ def platform_valid?
187
+ !spec.platform || [:ios, :osx].include?(spec.platform.name)
188
+ end
189
+
190
+ def source_valid?
191
+ spec.source && !(spec.source =~ /http:\/\/EXAMPLE/)
192
+ end
193
+
194
+ def paths_starting_with_a_slash_errors
195
+ messages = []
196
+ %w[source_files public_header_files resources clean_paths].each do |accessor|
197
+ patterns = spec.send(accessor.to_sym)
198
+ # Some values are multiplaform
199
+ patterns = patterns.is_a?(Hash) ? patterns.values.flatten(1) : patterns
200
+ patterns = patterns.compact # some patterns may be nil (public_header_files, for instance)
201
+ patterns.each do |pattern|
202
+ # Skip FileList that would otherwise be resolved from the working directory resulting
203
+ # in a potentially very expensi operation
204
+ next if pattern.is_a?(FileList)
205
+ invalid = pattern.is_a?(Array) ? pattern.any? { |path| path.start_with?('/') } : pattern.start_with?('/')
206
+ if invalid
207
+ messages << "Paths cannot start with a slash (#{accessor})"
208
+ break
209
+ end
210
+ end
211
+ end
212
+ messages
213
+ end
214
+
215
+ # @return [Array<String>] List of the **non** fatal defects detected in a podspec
216
+ def podspec_warnings
217
+ license = spec.license || {}
218
+ source = spec.source || {}
219
+ text = @file.read
220
+ messages = []
221
+ messages << "Missing license type" unless license[:type]
222
+ messages << "Sample license type" if license[:type] && license[:type] =~ /\(example\)/
223
+ messages << "Invalid license type" if license[:type] && license[:type] =~ /\n/
224
+ messages << "The summary is not meaningful" if spec.summary =~ /A short description of/
225
+ messages << "The description is not meaningful" if spec.description && spec.description =~ /An optional longer description of/
226
+ messages << "The summary should end with a dot" if spec.summary !~ /.*\./
227
+ messages << "The description should end with a dot" if spec.description !~ /.*\./ && spec.description != spec.summary
228
+ messages << "Git sources should specify either a tag or a commit" if source[:git] && !source[:commit] && !source[:tag]
229
+ messages << "Github repositories should end in `.git'" if github_source? && source[:git] !~ /.*\.git/
230
+ messages << "Github repositories should use `https' link" if github_source? && source[:git] !~ /https:\/\/github.com/
231
+ messages << "Comments must be deleted" if text.scan(/^\s*#/).length > 24
232
+ messages
233
+ end
234
+
235
+ def github_source?
236
+ spec.source && spec.source[:git] =~ /github.com/
237
+ end
238
+
239
+ # It creates a podfile in memory and builds a library containing
240
+ # the pod for all available platfroms with xcodebuild.
241
+ #
242
+ # @return [Array<String>]
243
+ #
244
+ def xcodebuild_output
245
+ return [] if `which xcodebuild`.strip.empty?
246
+ messages = []
247
+ output = Dir.chdir(config.project_pods_root) { `xcodebuild clean build 2>&1` }
248
+ clean_output = process_xcode_build_output(output)
249
+ messages += clean_output
250
+ puts(output) if config.verbose?
251
+ messages
252
+ end
253
+
254
+ def process_xcode_build_output(output)
255
+ output_by_line = output.split("\n")
256
+ selected_lines = output_by_line.select do |l|
257
+ l.include?('error: ') && (l !~ /errors? generated\./) && (l !~ /error: \(null\)/)\
258
+ || l.include?('warning: ') && (l !~ /warnings? generated\./)\
259
+ || l.include?('note: ') && (l !~ /expanded from macro/)
260
+ end
261
+ selected_lines.map do |l|
262
+ new = l.gsub(/\/tmp\/CocoaPods\/Lint\/Pods\//,'') # Remove the unnecessary tmp path
263
+ new.gsub!(/^ */,' ') # Remove indentation
264
+ "XCODEBUILD > " << new # Mark
265
+ end
266
+ end
267
+
268
+ # It checks that every file pattern specified in a spec yields
269
+ # at least one file. It requires the pods to be alredy present
270
+ # in the current working directory under Pods/spec.name.
271
+ #
272
+ # @return [Array<String>]
273
+ #
274
+ def file_patterns_errors
275
+ messages = []
276
+ messages << "The sources did not match any file" if !spec.source_files.empty? && @pod.source_files.empty?
277
+ messages << "The resources did not match any file" if !spec.resources.empty? && @pod.resource_files.empty?
278
+ messages << "The preserve_paths did not match any file" if !spec.preserve_paths.empty? && @pod.preserve_files.empty?
279
+ messages << "The exclude_header_search_paths did not match any file" if !spec.exclude_header_search_paths.empty? && @pod.headers_excluded_from_search_paths.empty?
280
+ messages
281
+ end
282
+
283
+ def file_patterns_warnings
284
+ messages = []
285
+ unless @pod.license_file || spec.license && ( spec.license[:type] == 'Public Domain' || spec.license[:text] )
286
+ messages << "Unable to find a license file"
287
+ end
288
+ messages
289
+ end
290
+ end
291
+ end
292
+ end
@@ -8,9 +8,10 @@ module Pod
8
8
 
9
9
  $ pod push REPO [NAME.podspec]
10
10
 
11
- Validates NAME.podspec or `*.podspec' in the current working dir, updates
12
- the local copy of the repository named REPO, adds the specifications
13
- to the REPO, and finally it pushes REPO to its remote.}
11
+ Validates NAME.podspec or `*.podspec' in the current working dir, creates
12
+ a directory and version folder for the pod in the local copy of
13
+ REPO (~/.cocoapods/[REPO]), copies the podspec file into the version directory,
14
+ and finally it pushes REPO to its remote.}
14
15
  end
15
16
 
16
17
  def self.options
@@ -11,10 +11,20 @@ module Pod
11
11
  Clones `URL' in the local spec-repos directory at `~/.cocoapods'. The
12
12
  remote can later be referred to by `NAME'.
13
13
 
14
- $ pod repo update NAME
14
+ $ pod repo update [NAME]
15
15
 
16
16
  Updates the local clone of the spec-repo `NAME'. If `NAME' is omitted
17
- this will update all spec-repos in `~/.cocoapods'.}
17
+ this will update all spec-repos in `~/.cocoapods'.
18
+
19
+ $ pod repo update [NAME | DIRECTORY]
20
+
21
+ Lints the spec-repo `NAME'. If a directory is provided it is assumed
22
+ to be the root of a repo. Finally, if NAME is not provided this will
23
+ lint all the spec-repos known to CocoaPods.}
24
+ end
25
+
26
+ def self.options
27
+ [["--only-errors", "Lint presents only the errors"]].concat(super)
18
28
  end
19
29
 
20
30
  extend Executable
@@ -29,6 +39,9 @@ module Pod
29
39
  @branch = argv.arguments[3]
30
40
  when 'update'
31
41
  @name = argv.arguments[1]
42
+ when 'lint'
43
+ @name = argv.arguments[1]
44
+ @only_errors = argv.option('--only-errors')
32
45
  else
33
46
  super
34
47
  end
@@ -66,6 +79,60 @@ module Pod
66
79
  end
67
80
  end
68
81
 
82
+ def lint
83
+ if @name
84
+ dirs = File.exists?(@name) ? [ Pathname.new(@name) ] : [ dir ]
85
+ else
86
+ dirs = config.repos_dir.children.select {|c| c.directory?}
87
+ end
88
+ dirs.each do |dir|
89
+ check_versions(dir)
90
+ puts "\nLinting spec repo `#{dir.realpath.basename}'\n".yellow
91
+ podspecs = dir.glob('**/*.podspec')
92
+ invalid_count = 0
93
+
94
+ podspecs.each do |podspec|
95
+ linter = Linter.new(podspec)
96
+ linter.quick = true
97
+ linter.repo_path = dir
98
+
99
+ linter.lint
100
+
101
+ case linter.result_type
102
+ when :error
103
+ invalid_count += 1
104
+ color = :red
105
+ should_display = true
106
+ when :warning
107
+ color = :yellow
108
+ should_display = !@only_errors
109
+ end
110
+
111
+ if should_display
112
+ puts " -> ".send(color) << linter.spec_name
113
+ print_messages('ERROR', linter.errors)
114
+ unless @only_errors
115
+ print_messages('WARN', linter.warnings)
116
+ print_messages('NOTE', linter.notes)
117
+ end
118
+ puts unless config.silent?
119
+ end
120
+ end
121
+ puts "Analyzed #{podspecs.count} podspecs files.\n\n" unless config.silent?
122
+
123
+ if invalid_count == 0
124
+ puts "All the specs passed validation.".green << "\n\n" unless config.silent?
125
+ else
126
+ raise Informative, "#{invalid_count} podspecs failed validation."
127
+ end
128
+ end
129
+ end
130
+
131
+ def print_messages(type, messages)
132
+ return if config.silent?
133
+ messages.each {|msg| puts " - #{type.ljust(5)} | #{msg}"}
134
+ end
135
+
69
136
  def check_versions(dir)
70
137
  versions = versions(dir)
71
138
  unless is_compatilbe(versions)