xcoder 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ spec/TestProject/TestProject.xcodeproj/xcuserdata
6
+ spec/TestProject/TestProject.xcodeproj/project.xcworkspace/
7
+ spec/TestProject/build
8
+ spec/test-reports
data/.rvmrc ADDED
@@ -0,0 +1,4 @@
1
+ rvm use 1.9.2@xcoder
2
+ rvm_install_on_use_flag=1
3
+ rvm_project_rvmrc=1
4
+ rvm_gemset_create_on_use_flag=1
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+ gem 'builder'
5
+ gem 'json'
6
+ gem 'plist'
7
+ gem 'rest-client'
8
+
9
+ group :test do
10
+ gem 'rspec'
11
+ gem 'guard'
12
+ gem 'guard-rspec'
13
+ gem 'ruby-debug19'
14
+ end
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2, :cli => "--color --format d --tag ~integration" do
5
+
6
+ watch(%r{^spec/.+_spec\.rb$})
7
+ # As the registry and resource file affect most every file, the entire
8
+ # suite should be run when they are changed
9
+ watch(%r{^lib/xcode/(?:registry|resource)\.rb$}) { "spec" }
10
+ watch(%r{^lib/xcode/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch('spec/spec_helper.rb') { "spec" }
12
+
13
+ end
14
+
@@ -0,0 +1,142 @@
1
+ # XCoder
2
+
3
+ A ruby wrapper around various xcode tools and project, schemes and workspace configuration files
4
+
5
+ ## Example Usage
6
+
7
+ You will need to install the gem:
8
+
9
+ gem install xcoder
10
+
11
+ and then require the gem in your project/rakefile/etc
12
+
13
+ require 'xcoder'
14
+
15
+ ### Load a project
16
+
17
+ project = Xcode.project('MyProject') # Can be the name, the file (e.g. MyProject.xcodeproj) or the path
18
+
19
+ ### Finding all projects from the current directory down
20
+
21
+ Xcode.find_projects.each {|p| puts p.name }
22
+
23
+ ### Find a configuration for a target on a project
24
+
25
+ config = Xcode.project(:MyProject).target(:Target).config(:Debug) # returns an Xcode::Configuration object
26
+
27
+ ### Building a configuration
28
+
29
+ builder = config.builder
30
+ builder.profile = 'Profiles/MyAdHoc.mobileprovision' # This will remove old profiles and install the profile
31
+ builder.identity = 'iPhone Developer: Ray Hilton' # The name of the identity to use to sign the IPA (optional)
32
+ builder.build
33
+
34
+ ### Working with Keychains
35
+
36
+ You will not normally need to worry about manipulating keychains unless you want to automate importing of certificates (in a CI system with many clients) or opening of specific keychains for different builds (the old two-certs-with-same-identity-name workaround).
37
+
38
+ You can either use the user's login keychain, another named keychain, or simply use a temporary keychain that will be blown away after the build.
39
+
40
+ #### Creating a temporary keychain
41
+
42
+ Xcode::Keychain.temp_keychain('ProjectKeychain.keychain') do |keychain|
43
+ # import certs into the keychain
44
+ # perform builds within this keychain's context
45
+ end # Keychain is deleted
46
+
47
+ #### Importing a certificate
48
+
49
+ You can import a certificate from a .p12 file into a keychain. Here we simply create a temporary keychain, import a certificate, set the identity onto the builder and then perform a build.
50
+
51
+ keychain.import 'Certs/MyCert.p12', 'mycertpassword'
52
+ builder.keychain = keychain # Tell the builder to use the temp keychain
53
+ builder.identity = keychain.identities.first # Get the first (only) identity name from the keychain
54
+
55
+ ### Packaging a built .app
56
+
57
+ After performing the above build, you can create a versioned, well named .ipa and .dSYM.zip
58
+
59
+ builder.package
60
+
61
+ This will produce something like: MyProject-Debug-1.0.ipa and MyProject-Debug-1.0.dSYM.zip
62
+
63
+ ### Incrementing the build number
64
+
65
+ config.info_plist do |info|
66
+ info.version = info.version.to_i + 1
67
+ info.save
68
+ end
69
+
70
+ ### Working with workspaces
71
+
72
+ Loading workspaces can be done in a similar way to projects:
73
+
74
+ Xcode.workspaces.each do |w|
75
+ w.describe # prints a recursive description of the
76
+ # structure of the workspace and its projects
77
+ end
78
+
79
+ Or, if you know the name:
80
+
81
+ workspace = Xcode.workspace('MyWorkspace') # Can be the name, the file (e.g. MyWorkspace.xcworkspace) or the path
82
+
83
+
84
+ ### Schemes
85
+
86
+ There is basic support for schemes, you can enumerate them from a project like so:
87
+
88
+ project.schemes.each do |s|
89
+ s.builder.build
90
+ end
91
+
92
+ Or, access them by name:
93
+
94
+ builder = project.scheme('MyScheme').builder
95
+
96
+ Note: The builder behaves the same as the builder for the target/config approach and will force xcodebuild to use the local build/ directory (as per xcode3) rather than a generated temporary directory in DerivedData. This may or may not be a good thing.
97
+
98
+ Note: Shared schemes and user (current logged in user) specific schemes are both loaded. They may share names and other similarities that make them hard to distinguish. Currently the priority loading order is shared schemes and then user specific schemes.
99
+
100
+ ### Provisioning profiles
101
+
102
+ The library provides a mechanism to install/uninstall a provisioning profile. This normally happens as part of a build (if a profile is provided to the builder, see above), but you can do this manually:
103
+
104
+ Xcode::ProvisioningProfile.new("Myprofile.mobileprovision").install # installs profile into ~/Library
105
+
106
+ Or enumerate installed profiles:
107
+
108
+ Xcode::ProvisioningProfile.installed_profiles.each do |p|
109
+ p.uninstall # Removes the profile from ~/Library/
110
+ end
111
+
112
+ ### Testflight
113
+
114
+ The common output of this build/package process is to upload to testflight. This is pretty simple with xcoder:
115
+
116
+ builder.testflight(API_TOKEN, TEAM_TOKEN) do |tf|
117
+ tf.notes = "some release notes"
118
+ tf.notify = true # Whether to send a notification to users, default is true
119
+ tf.lists << "AList" # The lists to distribute the build to
120
+ end
121
+
122
+ You can also optionally set a .proxy= property or just set the HTTP_PROXY environment variable.
123
+
124
+ ### OCUnit to JUnit reports
125
+
126
+ You can invoke your test target/bundle from the builder
127
+
128
+ builder.test do |report|
129
+ report.write 'test-reports', :junit
130
+ end
131
+
132
+ This will invoke the test target, capture the output and write the junit reports to the test-reports directory. Currently only junit is supported.
133
+
134
+ ## Tests
135
+
136
+ There are some basic RSpec tests in the project which I suspect /wont/ work on machines without my identity installed.
137
+
138
+ Currently these tests only assert the basic project file parsing and build code and do not perform file modification tests (e.g. for info plists) or provisioning profile/keychain importing
139
+
140
+ ## Feedback
141
+
142
+ Please raise issues if you find defects or have a feature request.
@@ -0,0 +1,28 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => [:specs, :build]
4
+
5
+ task :specs do
6
+ system "rspec --color --format d --tag ~integration"
7
+ end
8
+
9
+ task :integration do
10
+ system "rspec --color --format d --tag integration"
11
+ end
12
+
13
+
14
+ task :reset => ['test_project:reset']
15
+
16
+ namespace :test_project do
17
+
18
+ task :reset do
19
+ puts "Reseting the TestProject Project File"
20
+ system "git co -- spec/TestProject"
21
+ puts "Removing any User schemes generated for in the project"
22
+ system "rm -rf spec/TestProject/TestProject.xcodeproj/xcuserdata"
23
+ puts "Removing any installed files"
24
+ system "git clean -df spec/TestProject"
25
+ end
26
+
27
+ end
28
+
@@ -0,0 +1,22 @@
1
+ module Xcode
2
+
3
+ #
4
+ # PBXBuildFile are entries within the project that create a link between the
5
+ # file and the PBXFileReference. One is created for each file added to a build
6
+ # target.
7
+ #
8
+ module BuildFile
9
+
10
+ #
11
+ # Create the properties hash for a build file with the given file reference
12
+ # identifier.
13
+ #
14
+ # @param [String] file_identifier the unique identifier for the file
15
+ # @return [Hash] the properties hash for a default BuildFile.
16
+ #
17
+ def self.with_properties file_identifier
18
+ { 'isa' => "PBXBuildFile", 'fileRef' => file_identifier }
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,92 @@
1
+ module Xcode
2
+
3
+ module BuildPhase
4
+
5
+ #
6
+ # @return [BuildPhase] the framework specific build phase of the target.
7
+ #
8
+ # @example
9
+ #
10
+ # 7165D44D146B4EA100DE2F0E /* Frameworks */ = {
11
+ # isa = PBXFrameworksBuildPhase;
12
+ # buildActionMask = 2147483647;
13
+ # files = (
14
+ # 7165D455146B4EA100DE2F0E /* UIKit.framework in Frameworks */,
15
+ # 7165D457146B4EA100DE2F0E /* Foundation.framework in Frameworks */,
16
+ # 7165D459146B4EA100DE2F0E /* CoreGraphics.framework in Frameworks */,
17
+ # );
18
+ # runOnlyForDeploymentPostprocessing = 0;
19
+ # };
20
+ #
21
+ def self.framework
22
+ { 'isa' => 'PBXFrameworksBuildPhase',
23
+ 'buildActionMask' => '2147483647',
24
+ 'files' => [],
25
+ 'runOnlyForDeploymentPostprocessing' => '0' }
26
+ end
27
+
28
+ #
29
+ # @return [BuildPhase] the sources specific build phase of the target.
30
+ #
31
+ def self.sources
32
+ { 'isa' => 'PBXSourcesBuildPhase',
33
+ 'buildActionMask' => '2147483647',
34
+ 'files' => [],
35
+ 'runOnlyForDeploymentPostprocessing' => '0' }
36
+ end
37
+
38
+ #
39
+ # @return [BuildPhase] the resources specific build phase of the target.
40
+ #
41
+ def self.resources
42
+ { 'isa' => 'PBXResourcesBuildPhase',
43
+ 'buildActionMask' => '2147483647',
44
+ 'files' => [],
45
+ 'runOnlyForDeploymentPostprocessing' => '0' }
46
+ end
47
+
48
+ #
49
+ # Return the files that are referenced by the build files. This traverses
50
+ # the level of indirection to make it easier to get to the FileReference.
51
+ #
52
+ # Another method, file, exists which will return the BuildFile references.
53
+ #
54
+ # @return [Array<FileReference>] the files referenced by the build files.
55
+ #
56
+ def build_files
57
+ files.map {|file| file.file_ref }
58
+ end
59
+
60
+ #
61
+ # Find the first file that has the name or path that matches the specified
62
+ # parameter.
63
+ #
64
+ # @param [String] name the name or the path of the file.
65
+ # @return [FileReference] the file referenced that matches the name or path;
66
+ # nil if no file is found.
67
+ #
68
+ def build_file(name)
69
+ build_files.find {|file| file.name == name or file.path == name }
70
+ end
71
+
72
+ #
73
+ # Add the specified file to the Build Phase.
74
+ #
75
+ # First a BuildFile entry is created for the file and then the build file
76
+ # entry is added to the particular build phase. A BuildFile identifier must
77
+ # exist for each target.
78
+ #
79
+ # @param [FileReference] file the FileReference Resource to add to the build
80
+ # phase.
81
+ #
82
+ def add_build_file(file)
83
+ find_file_by = file.name || file.path
84
+ unless build_file(find_file_by)
85
+ new_build_file = @registry.add_object BuildFile.with_properties(file.identifier)
86
+ @properties['files'] << new_build_file.identifier
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,190 @@
1
+ require 'xcode/shell'
2
+ require 'xcode/provisioning_profile'
3
+ require 'xcode/test/ocunit_report_parser.rb'
4
+ require 'xcode/testflight'
5
+
6
+ module Xcode
7
+ class Builder
8
+ attr_accessor :profile, :identity, :build_path, :keychain, :sdk
9
+
10
+ def initialize(config)
11
+ if config.is_a? Xcode::Scheme
12
+ @scheme = config
13
+ config = config.launch
14
+ end
15
+
16
+ puts "CONFIG: #{config}"
17
+ @target = config.target
18
+ @sdk = @target.project.sdk
19
+ @config = config
20
+ @build_path = "#{File.dirname(@target.project.path)}/build/"
21
+ end
22
+
23
+ def install_profile
24
+ return nil if @profile.nil?
25
+ # TODO: remove other profiles for the same app?
26
+ p = ProvisioningProfile.new(@profile)
27
+
28
+ ProvisioningProfile.installed_profiles.each do |installed|
29
+ if installed.identifiers==p.identifiers and installed.uuid==p.uuid
30
+ installed.uninstall
31
+ end
32
+ end
33
+
34
+ p.install
35
+ p
36
+ end
37
+
38
+ def build(sdk=@sdk)
39
+ cmd = build_command(@sdk)
40
+ Xcode::Shell.execute(cmd)
41
+ end
42
+
43
+ def test
44
+ cmd = build_command('iphonesimulator')
45
+ cmd << "TEST_AFTER_BUILD=YES"
46
+ cmd << "TEST_HOST=''"
47
+
48
+ parser = Xcode::Test::OCUnitReportParser.new
49
+ Xcode::Shell.execute(cmd, false) do |line|
50
+ puts line
51
+ parser << line
52
+ end
53
+
54
+ yield(parser) if block_given?
55
+
56
+ exit parser.exit_code if parser.exit_code!=0
57
+
58
+ parser
59
+ end
60
+
61
+ def testflight(api_token, team_token)
62
+ raise "Can't find #{ipa_path}, do you need to call builder.build?" unless File.exists? ipa_path
63
+ raise "Can't fins #{dsym_zip_path}, do you need to call builder.build?" unless File.exists? dsym_zip_path
64
+
65
+ testflight = Xcode::Testflight.new(api_token, team_token)
66
+ yield(testflight) if block_given?
67
+ testflight.upload(ipa_path, dsym_zip_path)
68
+ end
69
+
70
+ def clean
71
+ cmd = []
72
+ cmd << "xcodebuild"
73
+ cmd << "-project \"#{@target.project.path}\""
74
+
75
+ cmd << "-scheme #{@scheme.name}" unless @scheme.nil?
76
+ cmd << "-target \"#{@target.name}\"" if @scheme.nil?
77
+ cmd << "-configuration \"#{@config.name}\"" if @scheme.nil?
78
+
79
+ cmd << "OBJROOT=\"#{@build_path}\""
80
+ cmd << "SYMROOT=\"#{@build_path}\""
81
+ cmd << "clean"
82
+ Xcode::Shell.execute(cmd)
83
+
84
+ @built = false
85
+ @packaged = false
86
+ # FIXME: Totally not safe
87
+ # cmd = []
88
+ # cmd << "rm -Rf #{build_path}"
89
+ # Xcode::Shell.execute(cmd)
90
+ end
91
+
92
+ def sign
93
+ cmd = []
94
+ cmd << "codesign"
95
+ cmd << "--force"
96
+ cmd << "--sign \"#{@identity}\""
97
+ cmd << "--resource-rules=\"#{app_path}/ResourceRules.plist\""
98
+ cmd << "--entitlements \"#{entitlements_path}\""
99
+ cmd << "\"#{ipa_path}\""
100
+ Xcode::Shell.execute(cmd)
101
+ end
102
+
103
+ def package
104
+ raise "Can't find #{app_path}, do you need to call builder.build?" unless File.exists? app_path
105
+
106
+ #package IPA
107
+ cmd = []
108
+ cmd << "xcrun"
109
+ cmd << "-sdk #{@target.project.sdk.nil? ? "iphoneos" : @target.project.sdk}"
110
+ cmd << "PackageApplication"
111
+ cmd << "-v \"#{app_path}\""
112
+ cmd << "-o \"#{ipa_path}\""
113
+
114
+ # cmd << "OTHER_CODE_SIGN_FLAGS=\"--keychain #{@keychain.path}\"" unless @keychain.nil?
115
+ #
116
+ # unless @identity.nil?
117
+ # cmd << "--sign \"#{@identity}\""
118
+ # end
119
+
120
+ unless @profile.nil?
121
+ cmd << "--embed \"#{@profile}\""
122
+ end
123
+
124
+ Xcode::Shell.execute(cmd)
125
+
126
+ # package dSYM
127
+ cmd = []
128
+ cmd << "zip"
129
+ cmd << "-r"
130
+ cmd << "-T"
131
+ cmd << "-y \"#{dsym_zip_path}\""
132
+ cmd << "\"#{dsym_path}\""
133
+ Xcode::Shell.execute(cmd)
134
+
135
+ end
136
+
137
+ def configuration_build_path
138
+ "#{build_path}/#{@config.name}-#{@sdk}"
139
+ end
140
+
141
+ def entitlements_path
142
+ "#{build_path}/#{@target.name}.build/#{name}-#{@target.project.sdk}/#{@target.name}.build/#{@config.product_name}.xcent"
143
+ end
144
+
145
+ def app_path
146
+ "#{configuration_build_path}/#{@config.product_name}.app"
147
+ end
148
+
149
+ def product_version_basename
150
+ version = @config.info_plist.version
151
+ version = "SNAPSHOT" if version.nil? or version==""
152
+ "#{configuration_build_path}/#{@config.product_name}-#{@config.name}-#{version}"
153
+ end
154
+
155
+ def ipa_path
156
+ "#{product_version_basename}.ipa"
157
+ end
158
+
159
+ def dsym_path
160
+ "#{app_path}.dSYM"
161
+ end
162
+
163
+ def dsym_zip_path
164
+ "#{product_version_basename}.dSYM.zip"
165
+ end
166
+
167
+
168
+ private
169
+
170
+ def build_command(sdk=@sdk)
171
+ profile = install_profile
172
+ cmd = []
173
+ cmd << "xcodebuild"
174
+ cmd << "-sdk #{sdk}" unless sdk.nil?
175
+ cmd << "-project \"#{@target.project.path}\""
176
+
177
+ cmd << "-scheme #{@scheme.name}" unless @scheme.nil?
178
+ cmd << "-target \"#{@target.name}\"" if @scheme.nil?
179
+ cmd << "-configuration \"#{@config.name}\"" if @scheme.nil?
180
+
181
+ cmd << "OTHER_CODE_SIGN_FLAGS=\"--keychain #{@keychain.path}\"" unless @keychain.nil?
182
+ cmd << "CODE_SIGN_IDENTITY=\"#{@identity}\"" unless @identity.nil?
183
+ cmd << "OBJROOT=\"#{@build_path}\""
184
+ cmd << "SYMROOT=\"#{@build_path}\""
185
+ cmd << "PROVISIONING_PROFILE=#{profile.uuid}" unless profile.nil?
186
+ cmd
187
+ end
188
+
189
+ end
190
+ end