cocoapods 0.5.1 → 0.6.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.
Files changed (44) hide show
  1. data/CHANGELOG.md +229 -2
  2. data/README.md +50 -20
  3. data/bin/pod +3 -2
  4. data/lib/cocoapods.rb +23 -9
  5. data/lib/cocoapods/command.rb +71 -30
  6. data/lib/cocoapods/command/error_report.rb +102 -0
  7. data/lib/cocoapods/command/install.rb +27 -19
  8. data/lib/cocoapods/command/list.rb +51 -8
  9. data/lib/cocoapods/command/presenter.rb +61 -0
  10. data/lib/cocoapods/command/presenter/cocoa_pod.rb +123 -0
  11. data/lib/cocoapods/command/push.rb +102 -0
  12. data/lib/cocoapods/command/repo.rb +70 -14
  13. data/lib/cocoapods/command/search.rb +7 -10
  14. data/lib/cocoapods/command/setup.rb +76 -15
  15. data/lib/cocoapods/command/spec.rb +581 -97
  16. data/lib/cocoapods/config.rb +23 -26
  17. data/lib/cocoapods/dependency.rb +86 -40
  18. data/lib/cocoapods/downloader.rb +30 -18
  19. data/lib/cocoapods/downloader/git.rb +125 -15
  20. data/lib/cocoapods/downloader/http.rb +73 -0
  21. data/lib/cocoapods/downloader/mercurial.rb +3 -9
  22. data/lib/cocoapods/downloader/subversion.rb +3 -9
  23. data/lib/cocoapods/executable.rb +26 -3
  24. data/lib/cocoapods/generator/acknowledgements.rb +37 -0
  25. data/lib/cocoapods/generator/acknowledgements/markdown.rb +38 -0
  26. data/lib/cocoapods/generator/acknowledgements/plist.rb +63 -0
  27. data/lib/cocoapods/generator/copy_resources_script.rb +8 -4
  28. data/lib/cocoapods/generator/documentation.rb +99 -0
  29. data/lib/cocoapods/generator/dummy_source.rb +14 -0
  30. data/lib/cocoapods/installer.rb +140 -109
  31. data/lib/cocoapods/installer/target_installer.rb +78 -83
  32. data/lib/cocoapods/installer/user_project_integrator.rb +162 -0
  33. data/lib/cocoapods/local_pod.rb +240 -0
  34. data/lib/cocoapods/platform.rb +41 -18
  35. data/lib/cocoapods/podfile.rb +234 -21
  36. data/lib/cocoapods/project.rb +67 -0
  37. data/lib/cocoapods/resolver.rb +62 -32
  38. data/lib/cocoapods/sandbox.rb +63 -0
  39. data/lib/cocoapods/source.rb +42 -20
  40. data/lib/cocoapods/specification.rb +294 -271
  41. data/lib/cocoapods/specification/set.rb +10 -28
  42. data/lib/cocoapods/specification/statistics.rb +112 -0
  43. metadata +124 -11
  44. data/lib/cocoapods/xcodeproj_pods.rb +0 -111
@@ -6,7 +6,7 @@ module Pod
6
6
  def self.banner
7
7
  %{Managing spec-repos:
8
8
 
9
- $ pod repo add NAME URL
9
+ $ pod repo add NAME URL [BRANCH]
10
10
 
11
11
  Clones `URL' in the local spec-repos directory at `~/.cocoapods'. The
12
12
  remote can later be referred to by `NAME'.
@@ -14,11 +14,7 @@ module Pod
14
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'.
18
-
19
- $ pod repo set-url NAME URL
20
-
21
- Updates the remote `URL' of the spec-repo `NAME'.}
17
+ this will update all spec-repos in `~/.cocoapods'.}
22
18
  end
23
19
 
24
20
  extend Executable
@@ -26,10 +22,11 @@ module Pod
26
22
 
27
23
  def initialize(argv)
28
24
  case @action = argv.arguments[0]
29
- when 'add', 'set-url'
25
+ when 'add'
30
26
  unless (@name = argv.arguments[1]) && (@url = argv.arguments[2])
31
27
  raise Informative, "#{@action == 'add' ? 'Adding' : 'Updating the remote of'} a repo needs a `name' and a `url'."
32
28
  end
29
+ @branch = argv.arguments[3]
33
30
  when 'update'
34
31
  @name = argv.arguments[1]
35
32
  else
@@ -46,24 +43,83 @@ module Pod
46
43
  end
47
44
 
48
45
  def add
49
- puts "Cloning spec repo `#{@name}' from `#{@url}'" unless config.silent?
46
+ print_subtitle "Cloning spec repo `#{@name}' from `#{@url}'#{" (branch `#{@branch}')" if @branch}"
50
47
  config.repos_dir.mkpath
51
48
  Dir.chdir(config.repos_dir) { git("clone '#{@url}' #{@name}") }
49
+ Dir.chdir(dir) { git("checkout #{@branch}") } if @branch
50
+ check_versions(dir)
52
51
  end
53
52
 
54
53
  def update
55
- dirs = @name ? [dir] : config.repos_dir.children
54
+ dirs = @name ? [dir] : config.repos_dir.children.select {|c| c.directory?}
56
55
  dirs.each do |dir|
57
- puts "Updating spec repo `#{dir.basename}'" unless config.silent?
58
- Dir.chdir(dir) { git("pull") }
56
+ print_subtitle "Updating spec repo `#{dir.basename}'"
57
+ Dir.chdir(dir) do
58
+ `git rev-parse >/dev/null 2>&1`
59
+ if $?.exitstatus.zero?
60
+ git("pull")
61
+ else
62
+ puts(" Not a git repository") if config.verbose?
63
+ end
64
+ end
65
+ check_versions(dir)
59
66
  end
60
67
  end
61
68
 
62
- def set_url
63
- Dir.chdir(dir) do
64
- git("remote set-url origin '#{@url}'")
69
+ def check_versions(dir)
70
+ versions = versions(dir)
71
+ unless is_compatilbe(versions)
72
+ min, max = versions['min'], versions['max']
73
+ version_msg = ( min == max ) ? min : "#{min} - #{max}"
74
+ raise Informative,
75
+ "\n[!] The `#{dir.basename.to_s}' repo requires CocoaPods #{version_msg}\n".red +
76
+ "Update Cocoapods, or checkout the appropriate tag in the repo.\n\n"
65
77
  end
78
+ puts "\nCocoapods #{versions['last']} is available.\n".green if has_update(versions)
66
79
  end
80
+
81
+ def self.compatible?(name)
82
+ dir = Config.instance.repos_dir + name
83
+ versions = versions(dir)
84
+ is_compatilbe(versions)
85
+ end
86
+
87
+ private
88
+
89
+ def versions(dir)
90
+ self.class.versions(dir)
91
+ end
92
+
93
+ def self.versions(dir)
94
+ require 'yaml'
95
+ yaml_file = dir + 'CocoaPods-version.yml'
96
+ yaml_file.exist? ? YAML.load_file(yaml_file) : {}
97
+ end
98
+
99
+ def is_compatilbe(versions)
100
+ self.class.is_compatilbe(versions)
101
+ end
102
+
103
+ def self.is_compatilbe(versions)
104
+ min, max = versions['min'], versions['max']
105
+ supports_min = !min || bin_version >= Gem::Version.new(min)
106
+ supports_max = !max || bin_version <= Gem::Version.new(max)
107
+ supports_min && supports_max
108
+ end
109
+
110
+ def has_update(versions)
111
+ self.class.has_update(versions)
112
+ end
113
+
114
+ def self.has_update(versions)
115
+ last = versions['last']
116
+ last && Gem::Version.new(last) > bin_version
117
+ end
118
+
119
+ def self.bin_version
120
+ Gem::Version.new(VERSION)
121
+ end
122
+
67
123
  end
68
124
  end
69
125
  end
@@ -12,23 +12,20 @@ module Pod
12
12
  end
13
13
 
14
14
  def self.options
15
- " --full Search by name, summary, and description\n" +
16
- super
15
+ [["--full", "Search by name, summary, and description"]].concat(Presenter.options).concat(super)
17
16
  end
18
17
 
19
18
  def initialize(argv)
20
19
  @full_text_search = argv.option('--full')
21
- unless @query = argv.arguments.first
22
- super
23
- end
20
+ @presenter = Presenter.new(argv)
21
+ @query = argv.shift_argument
22
+ super unless argv.empty? && @query
24
23
  end
25
24
 
26
25
  def run
27
- Source.search_by_name(@query.strip, @full_text_search).each do |set|
28
- puts "==> #{set.name} (#{set.versions.reverse.join(", ")})"
29
- puts " #{set.specification.summary.strip}"
30
- puts
31
- end
26
+ sets = Source.search_by_name(@query.strip, @full_text_search)
27
+ sets.each {|s| puts @presenter.describe(s)}
28
+ puts
32
29
  end
33
30
  end
34
31
  end
@@ -2,7 +2,7 @@ module Pod
2
2
  class Command
3
3
  class Setup < Command
4
4
  def self.banner
5
- %{Setup CocoaPods environment:
5
+ %{Setup CocoaPods environment:
6
6
 
7
7
  $ pod setup
8
8
 
@@ -14,36 +14,97 @@ module Pod
14
14
  If the clone already exists, it will ensure that it is up-to-date.}
15
15
  end
16
16
 
17
+ def self.options
18
+ [["--push", "Use this option to enable push access once granted"]].concat(super)
19
+ end
20
+
21
+ extend Executable
22
+ executable :git
23
+
17
24
  def initialize(argv)
25
+ @push_option = argv.option('--push')
18
26
  super unless argv.empty?
19
27
  end
20
28
 
21
- def master_repo_url
29
+ def dir
30
+ config.repos_dir + 'master'
31
+ end
32
+
33
+ def read_only_url
22
34
  'git://github.com/CocoaPods/Specs.git'
23
35
  end
24
36
 
25
- def add_master_repo_command
26
- @command ||= Repo.new(ARGV.new(['add', 'master', master_repo_url]))
37
+ def read_write_url
38
+ 'git@github.com:CocoaPods/Specs.git'
39
+ end
40
+
41
+ def url
42
+ if push?
43
+ read_write_url
44
+ else
45
+ read_only_url
46
+ end
47
+ end
48
+
49
+ def origin_url_read_only?
50
+ read_master_repo_url.chomp == read_only_url
27
51
  end
28
52
 
29
- def update_master_repo_remote_command
30
- Repo.new(ARGV.new(['set-url', 'master', master_repo_url]))
53
+ def origin_url_push?
54
+ read_master_repo_url.chomp == read_write_url
31
55
  end
32
56
 
33
- def update_master_repo_command
34
- Repo.new(ARGV.new(['update', 'master']))
57
+ def push?
58
+ @push_option || (dir.exist? && origin_url_push?)
59
+ end
60
+
61
+ def read_master_repo_url
62
+ Dir.chdir(dir) do
63
+ origin_url = git('config --get remote.origin.url')
64
+ end
65
+ end
66
+
67
+ def set_master_repo_url
68
+ Dir.chdir(dir) do
69
+ git("remote set-url origin '#{url}'")
70
+ end
71
+ end
72
+
73
+ def add_master_repo
74
+ @command ||= Repo.new(ARGV.new(['add', 'master', url, '0.6'])).run
75
+ end
76
+
77
+ def update_master_repo
78
+ Repo.new(ARGV.new(['update', 'master'])).run
79
+ end
80
+
81
+ #TODO: remove after rc
82
+ def set_master_repo_branch
83
+ Dir.chdir(dir) do
84
+ git("checkout 0.6")
85
+ end
86
+ end
87
+
88
+ def run_if_needed
89
+ run unless dir.exist? && Repo.compatible?('master')
35
90
  end
36
91
 
37
92
  def run
38
- if (config.repos_dir + 'master').exist?
39
- update_master_repo_remote_command.run
40
- update_master_repo_command.run
93
+ print_title "Setting up CocoaPods master repo"
94
+ if dir.exist?
95
+ set_master_repo_url
96
+ set_master_repo_branch
97
+ update_master_repo
41
98
  else
42
- add_master_repo_command.run
99
+ add_master_repo
100
+ end
101
+ # Mainly so the specs run with submodule repos
102
+ if (dir + '.git/hooks').exist?
103
+ hook = dir + '.git/hooks/pre-commit'
104
+ hook.open('w') { |f| f << "#!/bin/sh\nrake lint" }
105
+ `chmod +x '#{hook}'`
43
106
  end
44
- hook = config.repos_dir + 'master/.git/hooks/pre-commit'
45
- hook.open('w') { |f| f << "#!/bin/sh\nrake lint" }
46
- `chmod +x '#{hook}'`
107
+ print_subtitle "Setup completed (#{push? ? "push" : "read-only"} access)"
47
108
  end
48
109
  end
49
110
  end
@@ -1,26 +1,45 @@
1
+ # encoding: utf-8
2
+
1
3
  module Pod
2
4
  class Command
3
5
  class Spec < Command
4
6
  def self.banner
5
- %{Managing PodSpec files:
7
+ %{Managing PodSpec files:
6
8
 
7
- $ pod spec create NAME
9
+ $ pod spec create [ NAME | https://github.com/USER/REPO ]
8
10
 
9
11
  Creates a PodSpec, in the current working dir, called `NAME.podspec'.
12
+ If a GitHub url is passed the spec is prepopulated.
10
13
 
11
- $ pod spec lint NAME.podspec
14
+ $ pod spec lint [ NAME.podspec | REPO ]
12
15
 
13
16
  Validates `NAME.podspec'. In case `NAME.podspec' is omitted, it defaults
14
- to `*.podspec' in the current working dir.}
17
+ to `*.podspec' in the current working dir. If the name of a repo is
18
+ provided it validates all its specs.}
19
+ end
20
+
21
+ def self.options
22
+ [ ["--quick", "Lint skips checks that would require to donwload and build the spec"],
23
+ ["--only-errors", "Lint validates even if warnings are present"],
24
+ ["--no-clean", "Lint leaves the build directory intact for inspection"] ].concat(super)
15
25
  end
16
26
 
17
27
  def initialize(argv)
18
- args = argv.arguments
19
- unless (args[0] == 'create' && args.size == 2) ||
20
- (args[0] == 'lint' && args.size <= 2)
28
+ @action = argv.shift_argument
29
+ if @action == 'create'
30
+ @name_or_url = argv.shift_argument
31
+ @url = argv.shift_argument
32
+ super if @name_or_url.nil?
33
+ elsif @action == 'lint'
34
+ @quick = argv.option('--quick')
35
+ @only_errors = argv.option('--only-errors')
36
+ @no_clean = argv.option('--no-clean')
37
+ @repo_or_podspec = argv.shift_argument unless argv.empty?
38
+ super unless argv.size <= 1
39
+ else
21
40
  super
22
41
  end
23
- @action, @name = args.first(2)
42
+ super unless argv.empty?
24
43
  end
25
44
 
26
45
  def run
@@ -28,99 +47,564 @@ module Pod
28
47
  end
29
48
 
30
49
  def create
31
- author = `git config --get user.name`.strip
32
- email = `git config --get user.email`.strip
33
- spec = <<-SPEC.gsub(/^ /, '')
34
- #
35
- # Be sure to run `pod spec lint #{@name}.podspec' to ensure this is a
36
- # valid spec.
37
- #
38
- # Remove all comments before submitting the spec.
39
- #
40
- Pod::Spec.new do |s|
41
- s.name = '#{@name}'
42
- s.version = '1.0.0'
43
- s.license = 'MIT'
44
- s.summary = 'A short description of #{@name}.'
45
- s.homepage = 'http://EXAMPLE/#{@name}'
46
- s.author = { '#{author}' => '#{email}' }
47
-
48
- # Specify the location from where the source should be retreived.
49
- #
50
- s.source = { :git => 'http://EXAMPLE/#{@name}.git', :tag => '1.0.0' }
51
- # s.source = { :svn => 'http://EXAMPLE/#{@name}/tags/1.0.0' }
52
- # s.source = { :hg => 'http://EXAMPLE/#{@name}', :revision => '1.0.0' }
53
-
54
- s.description = 'An optional longer description of #{@name}.'
55
-
56
- # If this Pod runs only on iOS or OS X, then specify that with one of
57
- # these, or none if it runs on both platforms.
58
- #
59
- # s.platform = :ios
60
- # s.platform = :osx
61
-
62
- # A list of file patterns which select the source files that should be
63
- # added to the Pods project. If the pattern is a directory then the
64
- # path will automatically have '*.{h,m,mm,c,cpp}' appended.
65
- #
66
- # Alternatively, you can use the FileList class for even more control
67
- # over the selected files.
68
- # (See http://rake.rubyforge.org/classes/Rake/FileList.html.)
69
- #
70
- s.source_files = 'Classes', 'Classes/**/*.{h,m}'
71
-
72
- # A list of resources included with the Pod. These are copied into the
73
- # target bundle with a build phase script.
74
- #
75
- # Also allows the use of the FileList class like `source_files does.
76
- #
77
- # s.resource = "icon.png"
78
- # s.resources = "Resources/*.png"
79
-
80
- # A list of paths to remove after installing the Pod without the
81
- # `--no-clean' option. These can be examples, docs, and any other type
82
- # of files that are not needed to build the Pod.
83
- #
84
- # *NOTE*: Never remove license and README files.
85
- #
86
- # Also allows the use of the FileList class like `source_files does.
87
- #
88
- # s.clean_path = "examples"
89
- # s.clean_paths = "examples", "doc"
90
-
91
- # Specify a list of frameworks that the application needs to link
92
- # against for this Pod to work.
93
- #
94
- # s.framework = 'SomeFramework'
95
- # s.frameworks = 'SomeFramework', 'AnotherFramework'
96
-
97
- # Specify a list of libraries that the application needs to link
98
- # against for this Pod to work.
99
- #
100
- # s.library = 'iconv'
101
- # s.libraries = 'iconv', 'xml2'
102
-
103
- # If this Pod uses ARC, specify it like so.
104
- #
105
- # s.requires_arc = true
106
-
107
- # If you need to specify any other build settings, add them to the
108
- # xcconfig hash.
109
- #
110
- # s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' }
111
-
112
- # Finally, specify any Pods that this Pod depends on.
113
- #
114
- # s.dependency 'JSONKit', '~> 1.4'
50
+ if repo_id_match = (@url || @name_or_url).match(/github.com\/([^\/\.]*\/[^\/\.]*)\.*/)
51
+ # This is to make sure Faraday doesn't warn the user about the `system_timer` gem missing.
52
+ old_warn, $-w = $-w, nil
53
+ begin
54
+ require 'faraday'
55
+ ensure
56
+ $-w = old_warn
115
57
  end
116
- SPEC
117
- (Pathname.pwd + "#{@name}.podspec").open('w') { |f| f << spec }
58
+ require 'octokit'
59
+
60
+ repo_id = repo_id_match[1]
61
+ data = github_data_for_template(repo_id)
62
+ data[:name] = @name_or_url if @url
63
+ puts semantic_versioning_notice(repo_id, data[:name]) if data[:version] == '0.0.1'
64
+ else
65
+ data = default_data_for_template(@name_or_url)
66
+ end
67
+ spec = spec_template(data)
68
+ (Pathname.pwd + "#{data[:name]}.podspec").open('w') { |f| f << spec }
69
+ puts "\nSpecification created at #{data[:name]}.podspec".green
118
70
  end
119
71
 
120
72
  def lint
121
- file = @name ? Pathname.new(@name) : Pathname.pwd.glob('*.podspec').first
122
- spec = Specification.from_file(file)
123
- puts "This pod specification contains all required attributes." if spec.validate!
73
+ puts
74
+ invalid_count = lint_podspecs
75
+ count = specs_to_lint.count
76
+ if invalid_count == 0
77
+ lint_passed_message = count == 1 ? "#{podspecs_to_lint.first.basename} passed validation" : "All the #{count} specs passed validation"
78
+ puts lint_passed_message.green << "\n\n" unless config.silent?
79
+ else
80
+ raise Informative, count == 1 ? "The spec did not pass validation" : "#{invalid_count} out of #{count} specs failed validation"
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def lint_podspecs
87
+ invalid_count = 0
88
+ specs_to_lint.each do |spec|
89
+ # Show immediatly which pod is being processed.
90
+ print " -> #{spec}\r" unless config.silent? || is_repo?
91
+ $stdout.flush
92
+
93
+ linter = Linter.new(spec)
94
+ linter.lenient = @only_errors
95
+ linter.quick = @quick || is_repo?
96
+ linter.no_clean = @no_clean
97
+ invalid_count += 1 unless linter.lint
98
+
99
+ # This overwrites the previously printed text
100
+ puts " -> ".send(lint_result_color(linter)) << spec.to_s unless config.silent? || should_skip?(linter)
101
+ print_messages(spec, 'ERROR', linter.errors)
102
+ print_messages(spec, 'WARN', linter.warnings)
103
+ print_messages(spec, 'NOTE', linter.notes)
104
+
105
+ puts unless config.silent? || should_skip?(linter)
106
+ end
107
+ puts "Analyzed #{specs_to_lint.count} specs in #{podspecs_to_lint.count} podspecs files.\n\n" if is_repo? && !config.silent?
108
+ invalid_count
109
+ end
110
+
111
+ def lint_result_color(linter)
112
+ if linter.errors.empty? && linter.warnings.empty?
113
+ :green
114
+ elsif linter.errors.empty?
115
+ :yellow
116
+ else
117
+ :red
118
+ end
119
+ end
120
+
121
+ def should_skip?(linter)
122
+ is_repo? && linter.errors.empty? && linter.warnings.empty? && linter.notes.empty?
123
+ end
124
+
125
+ def print_messages(spec, type, messages)
126
+ return if config.silent?
127
+ messages.each {|msg| puts " - #{type.ljust(5)} | #{msg}"}
128
+ end
129
+
130
+ def podspecs_to_lint
131
+ @podspecs_to_lint ||= begin
132
+ if (is_repo?)
133
+ files = (config.repos_dir + @repo_or_podspec).glob('**/*.podspec')
134
+ elsif @repo_or_podspec
135
+ files = [Pathname.new(@repo_or_podspec)]
136
+ raise Informative, "Unable to find a spec named #{@repo_or_podspec}" unless files[0].exist? && @repo_or_podspec.include?('.podspec')
137
+ else
138
+ files = Pathname.pwd.glob('*.podspec')
139
+ raise Informative, "No specs found in the current directory" if files.empty?
140
+ end
141
+ files
142
+ end
143
+ end
144
+
145
+ def specs_to_lint
146
+ @specs_to_lint ||= begin
147
+ podspecs_to_lint.map do |podspec|
148
+ root_spec = Specification.from_file(podspec)
149
+ # TODO find a way to lint subspecs
150
+ # root_spec.preferred_dependency ? root_spec.subspec_dependencies : root_spec
151
+ end.flatten
152
+ end
153
+ end
154
+
155
+ def is_repo?
156
+ @is_repo ||= @repo_or_podspec && (config.repos_dir + @repo_or_podspec).exist? && !@repo_or_podspec.include?('/')
157
+ end
158
+
159
+ # Linter class
160
+ #
161
+ class Linter
162
+ include Config::Mixin
163
+
164
+ # TODO: Add check to ensure that attributes inherited by subspecs are not duplicated ?
165
+
166
+ attr_accessor :quick, :lenient, :no_clean
167
+ attr_reader :spec, :file
168
+ attr_reader :errors, :warnings, :notes
169
+
170
+ def initialize(spec)
171
+ @spec = spec
172
+ @file = spec.defined_in_file.realpath
173
+ end
174
+
175
+ # Takes an array of podspec files and lints them all
176
+ #
177
+ # It returns true if the spec passed validation
178
+ #
179
+ def lint
180
+ @platform_errors, @platform_warnings, @platform_notes = {}, {}, {}
181
+
182
+ platforms = @spec.available_platforms
183
+ platforms.each do |platform|
184
+ @platform_errors[platform], @platform_warnings[platform], @platform_notes[platform] = [], [], []
185
+
186
+ @spec.activate_platform(platform)
187
+ @platform = platform
188
+ puts "\n\n#{spec} - Analyzing on #{platform} platform.".green.reversed if config.verbose? && !@quick
189
+
190
+ # Skip validation if there are errors in the podspec as it would result in a crash
191
+ if !podspec_errors.empty?
192
+ @platform_errors[platform] += podspec_errors
193
+ @platform_notes[platform] << "#{platform.name} [!] Fatal errors found skipping the rest of the validation"
194
+ else
195
+ @platform_warnings[platform] += podspec_warnings + deprecation_warnings
196
+ @platform_notes[platform] += podspec_notes
197
+ peform_extensive_analysis unless quick
198
+ end
199
+ end
200
+
201
+ # Get common messages
202
+ @errors = @platform_errors.values.reduce(:&) || []
203
+ @warnings = @platform_warnings.values.reduce(:&) || []
204
+ @notes = @platform_notes.values.reduce(:&) || []
205
+
206
+ platforms.each do |platform|
207
+ # Mark platform specific messages
208
+ @errors += (@platform_errors[platform] - @errors).map {|m| "[#{platform}] #{m}"}
209
+ @warnings += (@platform_warnings[platform] - @warnings).map {|m| "[#{platform}] #{m}"}
210
+ @notes += (@platform_notes[platform] - @notes).map {|m| "[#{platform}] #{m}"}
211
+ end
212
+
213
+ valid?
214
+ end
215
+
216
+ def valid?
217
+ lenient ? errors.empty? : ( errors.empty? && warnings.empty? )
218
+ end
219
+
220
+ # Performs platform specific analysis.
221
+ # It requires to download the source at each iteration
222
+ #
223
+ def peform_extensive_analysis
224
+ set_up_lint_environment
225
+ install_pod
226
+ puts "Building with xcodebuild.\n".yellow if config.verbose?
227
+ # treat xcodebuild warnings as notes because the spec maintainer might not be the author of the library
228
+ xcodebuild_output.each { |msg| ( msg.include?('error') ? @platform_errors[@platform] : @platform_notes[@platform] ) << msg }
229
+ @platform_errors[@platform] += file_patterns_errors
230
+ @platform_warnings[@platform] += file_patterns_warnings
231
+ tear_down_lint_environment
232
+ end
233
+
234
+ def install_pod
235
+ podfile = podfile_from_spec
236
+ config.verbose
237
+ installer = Installer.new(podfile)
238
+ installer.install!
239
+ @pod = installer.pods.find { |pod| pod.top_specification == @spec }
240
+ config.silent
241
+ end
242
+
243
+ def podfile_from_spec
244
+ name = spec.name
245
+ podspec = file.realpath.to_s
246
+ platform_sym = @platform.to_sym
247
+ podfile = Pod::Podfile.new do
248
+ platform(platform_sym)
249
+ dependency name, :podspec => podspec
250
+ end
251
+ end
252
+
253
+ def set_up_lint_environment
254
+ tmp_dir.rmtree if tmp_dir.exist?
255
+ tmp_dir.mkpath
256
+ @original_config = Config.instance.clone
257
+ config.project_root = tmp_dir
258
+ config.project_pods_root = tmp_dir + 'Pods'
259
+ config.silent = !config.verbose
260
+ config.integrate_targets = false
261
+ config.generate_docs = false
262
+ end
263
+
264
+ def tear_down_lint_environment
265
+ tmp_dir.rmtree unless no_clean
266
+ Config.instance = @original_config
267
+ end
268
+
269
+ def tmp_dir
270
+ Pathname.new('/tmp/CocoaPods/Lint')
271
+ end
272
+
273
+ def pod_dir
274
+ tmp_dir + 'Pods' + spec.name
275
+ end
276
+
277
+ # @return [Array<String>] List of the fatal defects detected in a podspec
278
+ def podspec_errors
279
+ messages = []
280
+ messages << "The name of the spec should match the name of the file" unless names_match?
281
+ messages << "Unrecognized platfrom" unless platform_valid?
282
+ messages << "Missing name" unless spec.name
283
+ messages << "Missing version" unless spec.version
284
+ messages << "Missing summary" unless spec.summary
285
+ messages << "Missing homepage" unless spec.homepage
286
+ messages << "Missing author(s)" unless spec.authors
287
+ messages << "Missing source" unless spec.source
288
+
289
+ # attributes with multiplatform values
290
+ return messages unless platform_valid?
291
+ messages << "Missing source_files" if spec.source_files.empty? && spec.subspecs.empty? && spec.resources.empty?
292
+ messages += paths_starting_with_a_slash_errors
293
+ messages
294
+ end
295
+
296
+ def names_match?
297
+ return true unless spec.name
298
+ root_name = spec.name.match(/[^\/]*/)[0]
299
+ file.basename.to_s == root_name + '.podspec'
300
+ end
301
+
302
+ def platform_valid?
303
+ !spec.platform || [:ios, :osx].include?(spec.platform.name)
304
+ end
305
+
306
+ def paths_starting_with_a_slash_errors
307
+ messages = []
308
+ %w[source_files resources clean_paths].each do |accessor|
309
+ patterns = spec.send(accessor.to_sym)
310
+ # Some values are multiplaform
311
+ patterns = patterns.is_a?(Hash) ? patterns.values.flatten(1) : patterns
312
+ patterns.each do |pattern|
313
+ # Skip Filelist that would otherwise be resolved from the working directory resulting
314
+ # in a potentially very expensi operation
315
+ next if pattern.is_a?(FileList)
316
+ invalid = pattern.is_a?(Array) ? pattern.any? { |path| path.start_with?('/') } : pattern.start_with?('/')
317
+ if invalid
318
+ messages << "Paths cannot start with a slash (#{accessor})"
319
+ break
320
+ end
321
+ end
322
+ end
323
+ messages
324
+ end
325
+
326
+ # @return [Array<String>] List of the **non** fatal defects detected in a podspec
327
+ def podspec_warnings
328
+ license = @spec.license || {}
329
+ source = @spec.source || {}
330
+ text = @file.read
331
+ messages = []
332
+ messages << "Missing license type" unless license[:type]
333
+ messages << "Sample license type" if license[:type] && license[:type] =~ /\(example\)/
334
+ messages << "The summary is not meaningful" if spec.summary =~ /A short description of/
335
+ messages << "The description is not meaningful" if spec.description && spec.description =~ /An optional longer description of/
336
+ messages << "The summary should end with a dot" if @spec.summary !~ /.*\./
337
+ messages << "The description should end with a dot" if @spec.description !~ /.*\./ && @spec.description != @spec.summary
338
+ messages << "Git sources should specify either a tag or a commit" if source[:git] && !source[:commit] && !source[:tag]
339
+ messages << "Github repositories should end in `.git'" if github_source? && source[:git] !~ /.*\.git/
340
+ messages << "Github repositories should use `https' link" if github_source? && source[:git] !~ /https:\/\/github.com/
341
+ messages << "Comments must be deleted" if text =~ /^\w*#\n\w*#/ # allow a single line comment as it is generally used in subspecs
342
+ messages
343
+ end
344
+
345
+ def github_source?
346
+ @spec.source && @spec.source[:git] =~ /github.com/
347
+ end
348
+
349
+ # @return [Array<String>] List of the comments detected in the podspec
350
+ def podspec_notes
351
+ text = @file.read
352
+ deprecations = []
353
+ deprecations << "The `post_install' hook is reserved for edge cases" if text. =~ /post_install/
354
+ deprecations
355
+ end
356
+
357
+ # It reads a podspec file and checks for strings corresponding
358
+ # to features that are or will be deprecated
359
+ #
360
+ # @return [Array<String>]
361
+ #
362
+ def deprecation_warnings
363
+ text = @file.read
364
+ deprecations = []
365
+ deprecations << "`config.ios?' and `config.osx?' are deprecated" if text. =~ /config\..?os.?/
366
+ deprecations << "clean_paths are deprecated and ignored (use preserve_paths)" if text. =~ /clean_paths/
367
+ deprecations
368
+ end
369
+
370
+ # It creates a podfile in memory and builds a library containing
371
+ # the pod for all available platfroms with xcodebuild.
372
+ #
373
+ # @return [Array<String>]
374
+ #
375
+ def xcodebuild_output
376
+ return [] if `which xcodebuild`.strip.empty?
377
+ messages = []
378
+ output = Dir.chdir(config.project_pods_root) { `xcodebuild clean build 2>&1` }
379
+ clean_output = process_xcode_build_output(output)
380
+ messages += clean_output
381
+ puts(output) if config.verbose?
382
+ messages
383
+ end
384
+
385
+ def process_xcode_build_output(output)
386
+ output_by_line = output.split("\n")
387
+ selected_lines = output_by_line.select do |l|
388
+ l.include?('error:') && (l !~ /errors? generated\./) \
389
+ || l.include?('warning:') && (l !~ /warnings? generated\./)\
390
+ || l.include?('note:')
391
+ end
392
+ selected_lines.map do |l|
393
+ new = l.gsub(/\/tmp\/CocoaPods\/Lint\/Pods\//,'') # Remove the unnecessary tmp path
394
+ new.gsub!(/^ */,' ') # Remove indentation
395
+ "XCODEBUILD > " << new # Mark
396
+ end
397
+ end
398
+
399
+ # It checks that every file pattern specified in a spec yields
400
+ # at least one file. It requires the pods to be alredy present
401
+ # in the current working directory under Pods/spec.name.
402
+ #
403
+ # @return [Array<String>]
404
+ #
405
+ def file_patterns_errors
406
+ messages = []
407
+ messages << "The sources did not match any file" if !@spec.source_files.empty? && @pod.source_files.empty?
408
+ messages << "The resources did not match any file" if !@spec.resources.empty? && @pod.resources.empty?
409
+ messages << "The preserve_paths did not match any file" if !@spec.preserve_paths.empty? && @pod.preserve_paths.empty?
410
+ 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?
411
+ messages
412
+ end
413
+
414
+ def file_patterns_warnings
415
+ messages = []
416
+ messages << "Unable to find a license file" unless @pod.license_file
417
+ messages
418
+ end
419
+ end
420
+
421
+ # Templates and github information retrival for spec create
422
+
423
+ def default_data_for_template(name)
424
+ data = {}
425
+ data[:name] = name
426
+ data[:version] = '0.0.1'
427
+ data[:summary] = "A short description of #{name}."
428
+ data[:homepage] = "http://EXAMPLE/#{name}"
429
+ data[:author_name] = `git config --get user.name`.strip
430
+ data[:author_email] = `git config --get user.email`.strip
431
+ data[:source_url] = "http://EXAMPLE/#{name}.git"
432
+ data[:ref_type] = ':tag'
433
+ data[:ref] = '0.0.1'
434
+ data
435
+ end
436
+
437
+ def github_data_for_template(repo_id)
438
+ repo = Octokit.repo(repo_id)
439
+ user = Octokit.user(repo['owner']['login'])
440
+ data = {}
441
+
442
+ data[:name] = repo['name']
443
+ data[:summary] = repo['description'].gsub(/["]/, '\"')
444
+ data[:homepage] = (repo['homepage'] && !repo['homepage'].empty? ) ? repo['homepage'] : repo['html_url']
445
+ data[:author_name] = user['name'] || user['login']
446
+ data[:author_email] = user['email'] || 'email@address.com'
447
+ data[:source_url] = repo['clone_url']
448
+
449
+ data.merge suggested_ref_and_version(repo)
450
+ end
451
+
452
+ def suggested_ref_and_version(repo)
453
+ tags = Octokit.tags(:username => repo['owner']['login'], :repo => repo['name']).map {|tag| tag["name"]}
454
+ versions_tags = {}
455
+ tags.each do |tag|
456
+ clean_tag = tag.gsub(/^v(er)? ?/,'')
457
+ versions_tags[Gem::Version.new(clean_tag)] = tag if Gem::Version.correct?(clean_tag)
458
+ end
459
+ version = versions_tags.keys.sort.last || '0.0.1'
460
+ data = {:version => version}
461
+ if version == '0.0.1'
462
+ branches = Octokit.branches(:username => repo['owner']['login'], :repo => repo['name'])
463
+ master_name = repo['master_branch'] || 'master'
464
+ master = branches.select {|branch| branch['name'] == master_name }.first
465
+ data[:ref_type] = ':commit'
466
+ data[:ref] = master['commit']['sha']
467
+ else
468
+ data[:ref_type] = ':tag'
469
+ data[:ref] = versions_tags[version]
470
+ end
471
+ data
472
+ end
473
+
474
+ def spec_template(data)
475
+ return <<-SPEC
476
+ #
477
+ # Be sure to run `pod spec lint #{data[:name]}.podspec' to ensure this is a
478
+ # valid spec.
479
+ #
480
+ # Remove all comments before submitting the spec. Optional attributes are commented.
481
+ #
482
+ # For details see: https://github.com/CocoaPods/CocoaPods/wiki/The-podspec-format
483
+ #
484
+ Pod::Spec.new do |s|
485
+ s.name = "#{data[:name]}"
486
+ s.version = "#{data[:version]}"
487
+ s.summary = "#{data[:summary]}"
488
+ # s.description = 'An optional longer description of #{data[:name]}.'
489
+ s.homepage = "#{data[:homepage]}"
490
+
491
+ # Specify the license type. CocoaPods detects automatically the license file if it is named
492
+ # `LICENSE*', however if the name is different, specify it.
493
+ # Only if no dedicated file is available include the full text of the license.
494
+ #
495
+ s.license = 'MIT (example)'
496
+ # s.license = { :type => 'MIT', :file => 'LICENSE', :text => 'Permission is hereby granted ...' }
497
+
498
+ # Specify the authors of the library, with email addresses. You can often find
499
+ # the email addresses of the authors by using the SCM log. E.g. $ git log
500
+ #
501
+ s.author = { "#{data[:author_name]}" => "#{data[:author_email]}" }
502
+ # s.authors = { "#{data[:author_name]}" => "#{data[:author_email]}", "other author" => "and email address" }
503
+ #
504
+ # If absolutely no email addresses are available, then you can use this form instead.
505
+ #
506
+ # s.author = '#{data[:author_name]}', 'other author'
507
+
508
+ # Specify the location from where the source should be retreived.
509
+ #
510
+ s.source = { :git => "#{data[:source_url]}", #{data[:ref_type]} => "#{data[:ref]}" }
511
+ # s.source = { :svn => 'http://EXAMPLE/#{data[:name]}/tags/1.0.0' }
512
+ # s.source = { :hg => 'http://EXAMPLE/#{data[:name]}', :revision => '1.0.0' }
513
+
514
+ # If this Pod runs only on iOS or OS X, then specify the platform and
515
+ # the deployment target.
516
+ #
517
+ # s.platform = :ios, '5.0'
518
+ # s.platform = :ios
519
+
520
+ # ――― MULTI-PLATFORM VALUES ――――――――――――――――――――――――――――――――――――――――――――――――― #
521
+
522
+ # If this Pod runs on both platforms, then specify the deployment
523
+ # targets.
524
+ #
525
+ # s.ios.deployment_target = '5.0'
526
+ # s.osx.deployment_target = '10.7'
527
+
528
+ # A list of file patterns which select the source files that should be
529
+ # added to the Pods project. If the pattern is a directory then the
530
+ # path will automatically have '*.{h,m,mm,c,cpp}' appended.
531
+ #
532
+ # Alternatively, you can use the FileList class for even more control
533
+ # over the selected files.
534
+ # (See http://rake.rubyforge.org/classes/Rake/FileList.html.)
535
+ #
536
+ s.source_files = 'Classes', 'Classes/**/*.{h,m}'
537
+
538
+ # A list of resources included with the Pod. These are copied into the
539
+ # target bundle with a build phase script.
540
+ #
541
+ # Also allows the use of the FileList class like `source_files does.
542
+ #
543
+ # s.resource = "icon.png"
544
+ # s.resources = "Resources/*.png"
545
+
546
+ # A list of paths to preserve after installing the Pod.
547
+ # CocoaPods cleans by default any file that is not used.
548
+ # Also allows the use of the FileList class like `source_files does.
549
+ #
550
+ # s.preserve_paths = "examples", "doc"
551
+
552
+ # Specify a list of frameworks that the application needs to link
553
+ # against for this Pod to work.
554
+ #
555
+ # s.framework = 'SomeFramework'
556
+ # s.frameworks = 'SomeFramework', 'AnotherFramework'
557
+
558
+ # Specify a list of libraries that the application needs to link
559
+ # against for this Pod to work.
560
+ #
561
+ # s.library = 'iconv'
562
+ # s.libraries = 'iconv', 'xml2'
563
+
564
+ # If this Pod uses ARC, specify it like so.
565
+ #
566
+ # s.requires_arc = true
567
+
568
+ # If you need to specify any other build settings, add them to the
569
+ # xcconfig hash.
570
+ #
571
+ # s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' }
572
+
573
+ # Finally, specify any Pods that this Pod depends on.
574
+ #
575
+ # s.dependency 'JSONKit', '~> 1.4'
576
+ end
577
+ SPEC
578
+ end
579
+
580
+ def semantic_versioning_notice(repo_id, repo)
581
+ return <<-EOS
582
+
583
+ #{'――― MARKDOWN TEMPLATE ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――'.reversed}
584
+
585
+ I’ve recently added [#{repo}](https://github.com/CocoaPods/Specs/tree/master/#{repo}) to the [CocoaPods](https://github.com/CocoaPods/CocoaPods) package manager repo.
586
+
587
+ CocoaPods is a tool for managing dependencies for OSX and iOS Xcode projects and provides a central repository for iOS/OSX libraries. This makes adding libraries to a project and updating them extremely easy and it will help users to resolve dependencies of the libraries they use.
588
+
589
+ However, #{repo} doesn't have any version tags. I’ve added the current HEAD as version 0.0.1, but a version tag will make dependency resolution much easier.
590
+
591
+ [Semantic version](http://semver.org) tags (instead of plain commit hashes/revisions) allow for [resolution of cross-dependencies](https://github.com/CocoaPods/Specs/wiki/Cross-dependencies-resolution-example).
592
+
593
+ In case you didn’t know this yet; you can tag the current HEAD as, for instance, version 1.0.0, like so:
594
+
595
+ ```
596
+ $ git tag -a 1.0.0 -m "Tag release 1.0.0"
597
+ $ git push --tags
598
+ ```
599
+
600
+ #{'――― TEMPLATE END ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――'.reversed}
601
+
602
+ #{'[!] This repo does not appear to have semantic version tags.'.yellow}
603
+
604
+ After commiting the specification, consider opening a ticket with the template displayed above:
605
+ - link: https://github.com/#{repo_id}/issues/new
606
+ - title: Please add semantic version tags
607
+ EOS
124
608
  end
125
609
  end
126
610
  end