cinder 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +5 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +79 -0
  5. data/Rakefile +10 -0
  6. data/bin/cinder +19 -0
  7. data/cinder.gemspec +34 -0
  8. data/lib/cinder.rb +5 -0
  9. data/lib/cinder/commands.rb +13 -0
  10. data/lib/cinder/commands/build.rb +69 -0
  11. data/lib/cinder/commands/lint.rb +284 -0
  12. data/lib/cinder/commands/setup.rb +32 -0
  13. data/lib/cinder/external_commands.rb +43 -0
  14. data/lib/cinder/version.rb +3 -0
  15. data/spec/cinder/commands/lint_spec.rb +121 -0
  16. data/spec/cinder_spec.rb +9 -0
  17. data/spec/fake_workspace.rb +23 -0
  18. data/spec/fixtures/both-configs-sans-ad_hoc-provisioning/Foo.xcodeproj/project.pbxproj +329 -0
  19. data/spec/fixtures/both-configs-sans-ad_hoc-provisioning/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  20. data/spec/fixtures/both-configs-sans-ad_hoc-provisioning/Podfile +3 -0
  21. data/spec/fixtures/both-configs-sans-ad_hoc-provisioning/enterprise.mobileprovision +0 -0
  22. data/spec/fixtures/both-configs-sans-enterprise-provisioning/Foo.xcodeproj/project.pbxproj +329 -0
  23. data/spec/fixtures/both-configs-sans-enterprise-provisioning/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  24. data/spec/fixtures/both-configs-sans-enterprise-provisioning/Podfile +3 -0
  25. data/spec/fixtures/both-configs-sans-enterprise-provisioning/ad_hoc.mobileprovision +0 -0
  26. data/spec/fixtures/both-configs/Foo.xcodeproj/project.pbxproj +329 -0
  27. data/spec/fixtures/both-configs/Foo.xcodeproj/xcshareddata/xcschemes/Foo.xcscheme +0 -0
  28. data/spec/fixtures/both-configs/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  29. data/spec/fixtures/both-configs/Podfile +3 -0
  30. data/spec/fixtures/both-configs/ad_hoc.mobileprovision +0 -0
  31. data/spec/fixtures/both-configs/enterprise.mobileprovision +0 -0
  32. data/spec/fixtures/empty-podfile/Foo.xcodeproj/project.pbxproj +319 -0
  33. data/spec/fixtures/empty-podfile/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  34. data/spec/fixtures/empty-podfile/Podfile +0 -0
  35. data/spec/fixtures/lowercase/Podfile +3 -0
  36. data/spec/fixtures/lowercase/ad_hoc.mobileprovision +0 -0
  37. data/spec/fixtures/lowercase/foo.xcodeproj/project.pbxproj +319 -0
  38. data/spec/fixtures/lowercase/foo.xcodeproj/xcshareddata/xcschemes/foo.xcscheme +0 -0
  39. data/spec/fixtures/lowercase/foo.xcworkspace/contents.xcworkspacedata +1 -0
  40. data/spec/fixtures/multiple-workspaces/Bar.xcworkspace/contents.xcworkspacedata +1 -0
  41. data/spec/fixtures/multiple-workspaces/Foo.xcodeproj/project.pbxproj +319 -0
  42. data/spec/fixtures/multiple-workspaces/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  43. data/spec/fixtures/multiple-workspaces/Podfile +3 -0
  44. data/spec/fixtures/proper/Foo.xcodeproj/project.pbxproj +319 -0
  45. data/spec/fixtures/proper/Foo.xcodeproj/xcshareddata/xcschemes/Foo.xcscheme +0 -0
  46. data/spec/fixtures/proper/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  47. data/spec/fixtures/proper/Podfile +3 -0
  48. data/spec/fixtures/proper/ad_hoc.mobileprovision +0 -0
  49. data/spec/fixtures/sans-ad_hoc-or-enterprise-config/Foo.xcodeproj/project.pbxproj +309 -0
  50. data/spec/fixtures/sans-ad_hoc-or-enterprise-config/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  51. data/spec/fixtures/sans-ad_hoc-or-enterprise-config/Podfile +3 -0
  52. data/spec/fixtures/sans-podfile/Foo.xcodeproj/project.pbxproj +319 -0
  53. data/spec/fixtures/sans-podfile/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  54. data/spec/fixtures/sans-project/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  55. data/spec/fixtures/sans-project/Podfile +3 -0
  56. data/spec/fixtures/sans-target/Foo.xcodeproj/project.pbxproj +113 -0
  57. data/spec/fixtures/sans-target/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  58. data/spec/fixtures/sans-target/Podfile +3 -0
  59. data/spec/fixtures/sans-workspace/Foo.xcodeproj/project.pbxproj +319 -0
  60. data/spec/fixtures/sans-workspace/Podfile +3 -0
  61. data/spec/fixtures/whitespace-workspace/Foo App.xcodeproj/project.pbxproj +319 -0
  62. data/spec/fixtures/whitespace-workspace/Foo App.xcworkspace/contents.xcworkspacedata +1 -0
  63. data/spec/fixtures/whitespace-workspace/Podfile +3 -0
  64. data/spec/fixtures/workspace-project-mismatch/Bar.xcodeproj/project.pbxproj +319 -0
  65. data/spec/fixtures/workspace-project-mismatch/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  66. data/spec/fixtures/workspace-project-mismatch/Podfile +3 -0
  67. data/spec/fixtures/workspace-projects-mismatch/Bar.xcodeproj/project.pbxproj +319 -0
  68. data/spec/fixtures/workspace-projects-mismatch/Baz.xcodeproj/project.pbxproj +319 -0
  69. data/spec/fixtures/workspace-projects-mismatch/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  70. data/spec/fixtures/workspace-projects-mismatch/Podfile +3 -0
  71. data/spec/fixtures/workspace-target-mismatch/Foo.xcodeproj/project.pbxproj +319 -0
  72. data/spec/fixtures/workspace-target-mismatch/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  73. data/spec/fixtures/workspace-target-mismatch/Podfile +3 -0
  74. data/spec/fixtures/workspace-targets-mismatch/Foo.xcodeproj/project.pbxproj +504 -0
  75. data/spec/fixtures/workspace-targets-mismatch/Foo.xcworkspace/contents.xcworkspacedata +1 -0
  76. data/spec/fixtures/workspace-targets-mismatch/Podfile +3 -0
  77. data/spec/spec_helper.rb +20 -0
  78. metadata +363 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ /vendor/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ source 'https://raw.github.com/detroit-labs/erebor/gems/'
3
+
4
+ # Specify your gem's dependencies in cinder.gemspec
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Detroit Labs
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ Cinder
2
+ ========
3
+
4
+ Continuous Delivery for iOS Apps.
5
+
6
+ Cinder favors convention over configuration. Of the myriad ways to organize an iOS project, Cinder supports a subset that still gets the job done.
7
+
8
+ <a href="http://www.flickr.com/photos/cindy_cinder/6063851300/" title="Flaming Butterfly and Flowers by Cinder's, on Flickr"><img src="http://farm7.staticflickr.com/6204/6063851300_6c81f2360d.jpg" width="150" height="200" alt="Flaming Butterfly and Flowers"></a>
9
+
10
+ iOS projects using Cinder:
11
+
12
+ * have developers up and running after running a single command following `git clone`
13
+ * build your app's ipa whenever code is pushed to your GitHub repo
14
+ * publish successful builds of `master` as versions in [TestFlight](https://testflightapp.com/). <span style="color:#999">The "What's New" shows changes since the last build and since the last release.</span>
15
+ * <span style="color:#999">publish successful builds of *topic branches* as comments in the pull request with links to install a version of the app with that feature</span>
16
+ * <span style="color:#999">comment and close pull requests of failed builds</span>
17
+
18
+ Items in <span style="color:#999">de-emphasized text</span> are still being developed.
19
+
20
+ Usage
21
+ -----
22
+
23
+ * `script/setup` - run this after cloning a project and you're ready to develop in Xcode. Re-run it whenever project dependencies have changed.
24
+
25
+ * `script/cibuild` - is the two-liner shell script which tells the [Janky continuous integration server](https://github.com/github/janky) how to build Ad Hoc or Enterprise versions of your app:
26
+
27
+ #!/bin/bash
28
+ exec "$(dirname $0)/build" --configuration Enterprise
29
+
30
+ * `bin/cinder lint` - will guide you through setting up a new project or modifying an existing project to work with Cinder.
31
+
32
+
33
+ New Project Quickstart
34
+ ----------------------
35
+
36
+ In the top-level of your project run:
37
+
38
+ ruby -e "$(curl -fsSL https://raw.github.com/detroit-labs/cinder/hot)"
39
+
40
+ This is a one-time initialization that only one developer must complete. Afterwards, anyone cloning the project can simply run:
41
+
42
+ script/setup
43
+
44
+ Installation for Existing Projects
45
+ ----------------------------------
46
+
47
+ If your project doesn't use Bundler (e.g. doesn't have a `Gemfile`) then follow **New Project Quickstart** above.
48
+
49
+ Add this line to the top of `Gemfile`:
50
+
51
+ source 'https://raw.github.com/detroit-labs/erebor/gems/'
52
+
53
+ Add this line to the gems portion of `Gemfile`:
54
+
55
+ gem 'cinder'
56
+
57
+ Cinder expects that you use binstubs with Bundler and vendor gems under `.bundle`. Run:
58
+
59
+ bundle install --binstubs --path .bundle
60
+ git add .bundle/config
61
+ echo '/.bundle/ruby/' >>.gitignore
62
+
63
+ Henceforth `bundle install` without flags is sufficient.
64
+
65
+ The `cinder` CLI should now be available under `bin/`. It will guide you the rest of the way:
66
+
67
+ bin/cinder lint
68
+ # 10 fix errors
69
+ # 20 bin/cinder lint
70
+ # 30 if errors GOTO 10
71
+
72
+ Cinder is Opinionated
73
+ -----------------------
74
+
75
+ * Ruby is an integral part of the development, build, and
76
+ release toolchain for native iOS apps
77
+ * Ruby versions are managed by [rbenv](https://github.com/sstephenson/rbenv)
78
+ * Automation always wins
79
+ * Convention over configuration; when there's more than one way to do it, pick one
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run all specs"
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = %w[--color]
7
+ t.verbose = false
8
+ end
9
+
10
+ task :default => :spec
data/bin/cinder ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'commander/import'
4
+
5
+ $:.push File.expand_path("../../lib", __FILE__)
6
+ require 'cinder'
7
+
8
+ HighLine.track_eof = false # Fix for built-in Ruby
9
+
10
+ program :version, Cinder::VERSION
11
+ program :description, 'Continuous Delivery for iOS Apps'
12
+
13
+ program :help, 'Author', 'Tim Taylor <tim@detroitlabs.com>'
14
+ program :help, 'Website', 'https://github.com/detroit-labs/cinder'
15
+ program :help_formatter, :compact
16
+
17
+ default_command :help
18
+
19
+ require 'cinder/commands'
data/cinder.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $:.unshift(lib) unless $:.include?(lib)
4
+
5
+ require 'cinder/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "cinder"
9
+ s.version = Cinder::VERSION
10
+ s.authors = ["Tim Taylor"]
11
+ s.email = ["tim@detroitlabs.com"]
12
+ s.description = "Continuous Delivery for iOS Apps"
13
+ s.summary = "Cinder"
14
+ s.homepage = "https://github.com/detroit-labs/cinder"
15
+ s.license = "MIT"
16
+
17
+ s.add_dependency "commander", "~> 4.1.3"
18
+ s.add_dependency "extlib", "~> 0.9.16"
19
+ s.add_dependency "plist", "~> 3.1.0"
20
+ s.add_dependency "dotenv", "~> 0.5.0"
21
+ s.add_dependency "cocoapods", "~> 0.19.0"
22
+ s.add_dependency "shenzhen", "~> 0.3.0"
23
+ s.add_dependency "rugged", "~> 0.17.0.b8"
24
+
25
+ s.add_development_dependency "bundler", "~> 1.3"
26
+ s.add_development_dependency "rake"
27
+ s.add_development_dependency 'rspec'
28
+ s.add_development_dependency 'fakefs', '~> 0.3.2'
29
+
30
+ s.files = `git ls-files`.split($/)
31
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
32
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
33
+ s.require_paths = ["lib"]
34
+ end
data/lib/cinder.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "cinder/version"
2
+
3
+ module Cinder
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,13 @@
1
+ $:.push File.expand_path('../', __FILE__)
2
+
3
+ require 'dotenv'
4
+ Dotenv.load File.join(ENV['JENKINS_HOME'] || '.', '.env.ios')
5
+
6
+ require 'extlib/string'
7
+ require 'openssl'
8
+ require 'plist'
9
+ require 'external_commands'
10
+
11
+ require 'commands/build'
12
+ require 'commands/setup'
13
+ require 'commands/lint'
@@ -0,0 +1,69 @@
1
+ module Cinder
2
+ command :build do |c|
3
+ c.syntax = 'cinder build [OPTIONS]'
4
+ c.summary = 'Build & publish a release.'
5
+ c.description = 'Build a release. For AdHoc or Enterprise builds against `master`, publish to TestFlight.'
6
+
7
+ c.option '-c', '--configuration CONFIGURATION', 'Configuration used to build. One of AdHoc, AppStore, or Enterprise. Defaults to AdHoc.'
8
+
9
+ c.action do |args, options|
10
+ options.default :configuration => 'AdHoc'
11
+
12
+ name = determine_workspace
13
+ use_keychain options.configuration.snake_case
14
+ set_bundle_version name
15
+ build_ipa options.configuration, name
16
+ publish_ipa name
17
+ say_ok "Did all the things for #{name}"
18
+ end
19
+
20
+ private
21
+
22
+ def self.determine_workspace
23
+ workspaces = Dir["*.xcworkspace"].grep(/^(.*)\.xcworkspace$/){$1}
24
+ say_error "No Xcode workspace found" and abort if workspaces.empty?
25
+ say_error "There can be only one workspace" and abort if workspaces.length > 1
26
+ workspaces.first
27
+ end
28
+
29
+ def self.use_keychain keychain_name
30
+ keychain_file = "#{ENV['HOME']}/Library/Keychains/#{keychain_name}.keychain"
31
+ if File.exists?(keychain_file)
32
+ args = [
33
+ keychain_file,
34
+ "/Library/Keychains/System.keychain"
35
+ ]
36
+ log 'security', "Using #{keychain_name}.keychain"
37
+ # TODO: restore original keychains
38
+ security! "list-keychains -s #{args.join(" ")}"
39
+ security! %{default-keychain -s "#{keychain_file}"}
40
+ security! %{unlock-keychain -p "#{ENV['CI_KEYCHAIN_PASSWORD']}" "#{keychain_file}"}
41
+ end
42
+ end
43
+
44
+ def self.set_bundle_version name
45
+ short_sha1 = git!("rev-parse --short #{ENV['JANKY_SHA1'] || 'HEAD'}").strip
46
+ log 'plist', "Setting CFBundleVersion to `#{short_sha1}'"
47
+ PlistBuddy! %{-c "Set :CFBundleVersion #{short_sha1}" "#{name}/#{name}-Info.plist"}
48
+ end
49
+
50
+ def self.build_ipa configuration, name
51
+ log 'ipa', "Building an ipa with configuration `#{configuration}' and scheme `#{name}'"
52
+ ipa! "build --trace -c #{configuration} -s #{name}"
53
+ end
54
+
55
+ def self.publish_ipa name
56
+ master_sha1 = git!('rev-parse origin/master').strip
57
+ if master_sha1 == ENV['JANKY_SHA1']
58
+ team_token = ENV['CI_TESTFLIGHT_TEAM_TOKEN']
59
+ api_token = ENV['CI_TESTFLIGHT_API_TOKEN']
60
+ build_number = ENV['JANKY_ID']
61
+ build_output_url = "#{ENV['CI_JANKY_BASE_URL']}#{build_number}/output"
62
+ notes = "Automated Build ##{build_number}\n#{build_output_url}"
63
+
64
+ log 'ipa', 'Distribute ipa to TestFlight'
65
+ ipa! %{distribute:testflight --team_token #{team_token} --api_token #{api_token} --replace --lists "#{name}" --notify --notes "#{notes}"}
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,284 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'xcodeproj'
3
+ require 'extlib'
4
+ require 'cocoapods-core'
5
+ require 'rugged'
6
+
7
+ module Cinder
8
+
9
+ class ProvisioningProfile
10
+ attr_reader :distribution
11
+ attr_reader :devices
12
+
13
+ def self.from_file f
14
+ begin
15
+ p7 = OpenSSL::PKCS7.new(File.read(f))
16
+ p7.verify([], OpenSSL::X509::Store.new)
17
+ new Plist::parse_xml(p7.data)
18
+ rescue
19
+ new
20
+ end
21
+ end
22
+
23
+ def initialize profile = nil
24
+ if profile
25
+ @valid = true
26
+ @devices = []
27
+ if profile['ProvisionsAllDevices']
28
+ @distribution = :enterprise
29
+ elsif profile['ProvisionedDevices']
30
+ @distribution = :ad_hoc
31
+ @devices = profile['ProvisionedDevices']
32
+ else
33
+ # FIXME: could be a developer provisioning profile; need a better check
34
+ @distribution = :app_store
35
+ end
36
+ else
37
+ @valid = false
38
+ end
39
+ end
40
+
41
+ def valid?
42
+ @valid == true
43
+ end
44
+
45
+ def ad_hoc?
46
+ @distribution == :ad_hoc
47
+ end
48
+ end
49
+
50
+ class Linter
51
+ DISTRIBUTION_METHODS = {
52
+ :ad_hoc => 'AdHoc',
53
+ :app_store => 'AppStore',
54
+ :enterprise => 'Enterprise'
55
+ }
56
+
57
+ def lint options, args
58
+ abort unless acc = _lint(detect_repository({}))
59
+ say <<-EOS.margin if `git ls-files .cinder`.split($/).empty?
60
+
61
+ Additional Steps
62
+ ================
63
+
64
+ 1. Add ‘deckard’ group to #{acc[:repo_company]}/#{acc[:repo_name]} in GitHub
65
+
66
+ 2. Tell Hubot to set it up in Janky
67
+
68
+ @bot ci setup #{acc[:repo_company]}/#{acc[:repo_name]}
69
+ @bot ci set room #{acc[:repo_name]} #{acc[:name]}
70
+
71
+ 3. Create a distribution list named ‘#{acc[:name]}’ in TestFlight
72
+
73
+ 4. Create `setup` and `build` scripts
74
+
75
+ mkdir -p script
76
+ curl -L http://git.io/dqT6SA >script/.cinder-exec
77
+ ( cd script ; \\
78
+ chmod +x .cinder-exec ; \\
79
+ ln -s .cinder-exec setup ; \\
80
+ ln -s .cinder-exec build )
81
+
82
+ 5. Create `script/cibuild`
83
+
84
+ #!/bin/bash
85
+ exec "$(dirname $0)/build" --configuration #{acc[:build_configs].keys.grep(/ad_hoc|enterprise/).sort.first.to_s.camel_case}
86
+
87
+ 6. Add the following to `.gitignore`
88
+
89
+ /.bundle/ruby/
90
+ /Pods/
91
+ /bin/
92
+
93
+ 7. Add and commit a `.cinder` file to repository to turn off this message
94
+
95
+ EOS
96
+ end
97
+
98
+ def _lint acc = {}
99
+ acc &&= detect_projects acc
100
+ acc &&= detect_workspaces acc
101
+ acc &&= determine_name acc
102
+ acc &&= check_podfile acc
103
+ acc &&= check_workspace acc
104
+ acc &&= check_single_project acc
105
+ acc &&= check_multiple_projects acc
106
+ acc &&= load_project acc
107
+ acc &&= detect_targets acc
108
+ acc &&= check_single_target acc
109
+ acc &&= check_multiple_targets acc
110
+ acc &&= detect_build_configs acc
111
+ acc &&= check_app_store_config acc
112
+ acc &&= check_testing_configs acc
113
+ acc &&= detect_provisioning_profiles acc
114
+ acc &&= check_provisioning_profiles acc
115
+ acc &&= check_scheme acc
116
+
117
+ say_ok 'OK to go' if acc
118
+ acc
119
+ end
120
+
121
+ private
122
+
123
+ def detect_repository acc
124
+ begin
125
+ path = Rugged::Repository.discover
126
+ rescue
127
+ say_error "Must be in a git repository" and return nil
128
+ end
129
+ acc[:repo] = repo = Rugged::Repository.new path
130
+ acc[:root] = root = File.expand_path '..', repo.path
131
+
132
+ say_error "Must be at the root of the project at `#{root}'" and return nil unless root == Dir.getwd
133
+ upstream = repo.remotes.grep(/upstream/).first
134
+ upstream ||= repo.remotes.grep(/origin/).first
135
+ upstream &&= Rugged::Remote.lookup repo, upstream
136
+ if upstream.url =~ %r{github.com[:/]([\w-]+)/([\w-]+).git}
137
+ acc[:repo_company], acc[:repo_name] = $1, $2
138
+ end
139
+ say_error "Must have `upstream' or `origin' remote on GitHub" and return nil unless acc[:repo_name]
140
+ acc
141
+ end
142
+
143
+ def detect_projects acc
144
+ acc[:projects] = projects = Dir["*.xcodeproj"].map {|f| File.basename(f)}.grep(/^(.*)\.xcodeproj$/){$1}
145
+ say_error "No Xcode project found" and return nil if projects.empty?
146
+ acc
147
+ end
148
+
149
+ def detect_workspaces acc
150
+ acc[:workspaces] = Dir["*.xcworkspace"].map {|f| File.basename(f)}.grep(/^(.*)\.xcworkspace$/){$1}
151
+ acc
152
+ end
153
+
154
+ def determine_name acc
155
+ if acc[:workspaces].length == 1
156
+ acc[:name] = acc[:workspaces].first
157
+ elsif acc[:projects].length == 1
158
+ acc[:name] = acc[:projects].first
159
+ end
160
+ acc
161
+ end
162
+
163
+ def check_podfile acc
164
+ result = acc
165
+ file = Dir['Podfile'].first
166
+ say_error "No CocoaPods Podfile found" and return nil unless file
167
+ podfile = Pod::Podfile.from_file file
168
+ target = podfile.root_target_definitions.first
169
+ platform = target.platform.name if target.platform
170
+ say_error 'CocoaPods platform must be iOS' and result = nil unless platform == :ios
171
+ say_error 'Must have at least one CocoaPods dependency' and result = nil if target.dependencies.empty?
172
+ result
173
+ end
174
+
175
+ def check_workspace acc
176
+ workspaces = Dir["*.xcworkspace"].map {|f| File.basename(f)}.grep(/^(.*)\.xcworkspace$/){$1}
177
+ say_error "No Xcode workspace found" and return nil if workspaces.empty?
178
+ say_error "There can be only one Xcode workspace" and return nil if workspaces.length > 1
179
+ acc[:workspace] = workspace = workspaces.first
180
+ say_error "Workspace name `#{workspace}' must not contain whitespace" and return nil if workspace =~ /\s/
181
+ say_warning "Workspace name `#{workspace}' should be CamelCase" unless workspace =~ /^[[:upper:]]\S*(?:[[:upper:]]\s*)*$/
182
+ acc
183
+ end
184
+
185
+ def check_single_project acc
186
+ return acc if acc[:projects].length > 1
187
+
188
+ acc[:project] = acc[:projects].find { |p| p == acc[:workspace] }
189
+ say_error "Xcode project name `#{acc[:projects].first}' must match workspace name `#{acc[:workspace]}'" and return nil unless acc[:project]
190
+ acc
191
+ end
192
+
193
+ def check_multiple_projects acc
194
+ return acc if acc[:projects].length == 1
195
+
196
+ acc[:project] = acc[:projects].find { |p| p == acc[:workspace] }
197
+ say_error "One Xcode project name must match workspace name `#{acc[:workspace]}'" and return nil unless acc[:project]
198
+ acc
199
+ end
200
+
201
+ def load_project acc
202
+ acc[:xcodeproj] = Xcodeproj::Project.new "#{acc[:project]}.xcodeproj"
203
+ acc
204
+ end
205
+
206
+ def detect_targets acc
207
+ acc[:targets] = acc[:xcodeproj].targets
208
+ say_error "Xcode project must have at least one target" and return nil if acc[:targets].empty?
209
+ acc
210
+ end
211
+
212
+ def check_single_target acc
213
+ return acc if acc[:targets].length > 1
214
+
215
+ acc[:target] = target = acc[:targets].first
216
+ say_error "Target name `#{target.name}' must match workspace name `#{acc[:workspace]}'" and return nil unless target.name == acc[:workspace]
217
+ acc
218
+ end
219
+
220
+ def check_multiple_targets acc
221
+ return acc if acc[:targets].length == 1
222
+
223
+ acc[:target] = acc[:targets].find { |t| t.name == acc[:workspace] }
224
+ say_error "One Xcode target name must match workspace name `#{acc[:workspace]}'" and return nil unless acc[:target]
225
+ acc
226
+ end
227
+
228
+ def detect_build_configs acc
229
+ a = acc[:xcodeproj].build_configurations
230
+ acc[:build_configs] = a.find_all { |c| c.name =~ /^(?:AdHoc|AppStore|Enterprise)$/ }.each_with_object({}) { |c,h| h[c.name.snake_case.to_sym] = c }
231
+ acc
232
+ end
233
+
234
+ def check_testing_configs acc
235
+ configs = acc[:build_configs].find_all { |k,v| k == :ad_hoc || k == :enterprise }
236
+ say_error "Must have `AdHoc', `Enterprise', or both build configurations" and return nil if configs.empty?
237
+ acc
238
+ end
239
+
240
+ def check_app_store_config acc
241
+ say_warning "Should have `AppStore' build configuration" unless acc[:build_configs][:app_store]
242
+ acc
243
+ end
244
+
245
+ def detect_provisioning_profiles acc
246
+ a = Dir['*.mobileprovision'].grep(/^(ad_hoc|app_store|enterprise)\./){$1.to_sym}
247
+ acc[:provisioning_profiles] = a.each_with_object({}) { |p,h| h[p] = "#{p}.mobileprovision" }
248
+ acc
249
+ end
250
+
251
+ def check_provisioning_profiles acc
252
+ result = acc
253
+
254
+ acc[:build_configs].each do |name, config|
255
+ file = acc[:provisioning_profiles][name]
256
+ if file
257
+ profile = ProvisioningProfile.from_file file
258
+ if profile.valid?
259
+ say_error "`#{file}' appears to be an #{DISTRIBUTION_METHODS[profile.distribution]} provisioning profile" and result = nil unless name == profile.distribution
260
+ say_error "AdHoc provisioning must have at least 1 device provisioned" and result = nil if profile.ad_hoc? && profile.devices.empty?
261
+ else
262
+ say_error "Invalid provisioning profile `#{file}'" and result = nil
263
+ end
264
+ else
265
+ say_error "`#{config.name}' build configuration must have an `#{name}.mobileprovision' in project root" and result = nil
266
+ end
267
+ end
268
+ result
269
+ end
270
+
271
+ def check_scheme acc
272
+ schemes = Dir["#{acc[:name]}.xcodeproj/xcshareddata/xcschemes/#{acc[:name]}.xcscheme"].grep(/^(.*)\.xcscheme$/){$1}
273
+ say_error "Must have a shared Xcode scheme named `#{acc[:name]}'" and return nil if schemes.empty?
274
+ acc
275
+ end
276
+ end
277
+
278
+ command :lint do |c|
279
+ c.syntax = 'cinder lint'
280
+ c.summary = 'Check for CI problems'
281
+ c.description = 'Report if your project is missing or deviating from Cinder conventions.'
282
+ c.action Linter, :lint
283
+ end
284
+ end