cocoapods 0.5.1 → 0.6.0.rc1

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