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,335 @@
1
+ require 'plist'
2
+ require 'xcode/parsers/plutil_project_parser'
3
+ require 'xcode/resource'
4
+ require 'xcode/target'
5
+ require 'xcode/configuration'
6
+ require 'xcode/scheme'
7
+ require 'xcode/group'
8
+ require 'xcode/file_reference'
9
+ require 'xcode/registry'
10
+ require 'xcode/build_phase'
11
+ require 'xcode/variant_group'
12
+ require 'xcode/configuration_list'
13
+
14
+ module Xcode
15
+ class Project
16
+
17
+ attr_reader :name, :sdk, :path, :schemes, :registry
18
+
19
+ #
20
+ # Initialized with a specific path and sdk.
21
+ #
22
+ # This initialization is not often used. Instead projects are generated
23
+ # through the Xcode#project method.
24
+ #
25
+ # @see Xcode
26
+ #
27
+ # @param [String] path of the project to open.
28
+ # @param [String] sdk the sdk value of the project. This will default to
29
+ # `iphoneos`.
30
+ #
31
+ def initialize(path, sdk=nil)
32
+ @sdk = sdk || "iphoneos" # FIXME: should support OSX/simulator too
33
+ @path = File.expand_path path
34
+ @schemes = []
35
+ @groups = []
36
+ @name = File.basename(@path).gsub(/\.xcodeproj/,'')
37
+
38
+ # Parse the Xcode project file and create the registry
39
+
40
+ @registry = parse_pbxproj
41
+ @project = Xcode::Resource.new registry.root, @registry
42
+
43
+ @schemes = parse_schemes
44
+ end
45
+
46
+ #
47
+ # Returns the main group of the project where all the files reside.
48
+ #
49
+ # @todo this really could use a better name then groups as it is the mainGroup
50
+ # but it should likely be something like main_group, root or something
51
+ # else that conveys that this is the project root for files, and such.
52
+ #
53
+ # @return [Group] the main group, the heart of the action of the file
54
+ # explorer for the Xcode project. From here all other groups and items
55
+ # may be found.
56
+ #
57
+ def groups
58
+ @project.main_group
59
+ end
60
+
61
+ #
62
+ # Returns the group specified. If any part of the group does not exist along
63
+ # the path the group is created. Also paths can be specified to make the
64
+ # traversing of the groups easier.
65
+ #
66
+ # @example a group path that contains a traversal to sub-groups
67
+ #
68
+ # project.group('Vendor/MyCode/Support Files')
69
+ # # is equivalent to...
70
+ # project.group('Vendor').first.group('MyCode').first.group('Supporting Files')
71
+ #
72
+ # @note this path functionality current is only exercised from the project level
73
+ # all groups will treat the path division `/` as simply a character.
74
+ #
75
+ # @note this will attempt to find the paths specified, if it fails to find them
76
+ # it will create one and then continue traversing.
77
+ #
78
+ # @param [String] name the group name to find/create
79
+ #
80
+ def group(name,&block)
81
+
82
+ current_group = @project.main_group
83
+
84
+ # @todo consider this traversing and find/create as a normal procedure when
85
+ # traversing the project.
86
+
87
+ name.split("/").each do |path_component|
88
+ found_group = current_group.group(path_component).first
89
+ found_group = current_group.create_group(path_component) unless found_group
90
+ current_group = found_group
91
+ end
92
+
93
+ current_group.instance_eval(&block) if block_given?
94
+
95
+ current_group
96
+ end
97
+
98
+ #
99
+ # Return the file that matches the specified path. This will traverse
100
+ # the project's groups and find the file at the end of the path.
101
+ #
102
+ # @param [String] name_with_path the path to the file
103
+ # @return [FileReference] the file that matches the name, nil if no file
104
+ # matches the path.
105
+ def file(name_with_path)
106
+ path, name = File.split(name_with_path)
107
+ group(path).file(name).first
108
+ end
109
+
110
+ #
111
+ # Most Xcode projects have a products group where products are placed. This
112
+ # will generate an exception if there is no products group.
113
+ #
114
+ # @return [Group] the 'Products' group of the project.
115
+ def products_group
116
+ groups.group('Products').first
117
+ end
118
+
119
+ #
120
+ # Most Xcode projects have a Frameworks gorup where all the imported
121
+ # frameworks are shown. This will generate an exception if there is no
122
+ # Frameworks group.
123
+ #
124
+ # @return [Group] the 'Frameworks' group of the projet.
125
+ def frameworks_group
126
+ groups.group('Frameworks').first
127
+ end
128
+
129
+ #
130
+ # This will convert the current project file into a supported Xcode Plist
131
+ # format. This format is not json or a traditional plist so several core
132
+ # Ruby objects gained the #to_xcplist method to save it properly.
133
+ #
134
+ # Specifically this will add the necessary file header information and the
135
+ # surrounding mustache braces around the xcode plist format of the registry.
136
+ #
137
+ # @return [String] Xcode Plist format of the project.
138
+ def to_xcplist
139
+
140
+ # @note The Hash#to_xcplist, which the Registry will save out as xcode,
141
+ # saves a semi-colon at the end which needs to be removed to ensure
142
+ # the project file can be opened.
143
+
144
+ %{// !$*UTF8*$!"\n#{@registry.to_xcplist.gsub(/\};\s*\z/,'}')}}
145
+ end
146
+
147
+ #
148
+ # Save the current project at the current path that it exists.
149
+ #
150
+ def save!
151
+ save @path
152
+ end
153
+
154
+ #
155
+ # Saves the current project at the specified path.
156
+ #
157
+ # @note currently this does not support saving the workspaces associated
158
+ # with the project to their new location.
159
+ #
160
+ # @param [String] path the path to save the project
161
+ #
162
+ def save(path)
163
+ Dir.mkdir(path) unless File.exists?(path)
164
+
165
+ project_filepath = "#{path}/project.pbxproj"
166
+
167
+ # @toodo Save the workspace when the project is saved
168
+ # FileUtils.cp_r "#{path}/project.xcworkspace", "#{path}/project.xcworkspace"
169
+
170
+ File.open(project_filepath,'w') do |file|
171
+ file.puts to_xcplist
172
+ end
173
+ end
174
+
175
+ #
176
+ # Return the scheme with the specified name. Raises an error if no schemes
177
+ # match the specified name.
178
+ #
179
+ # @note if two schemes match names, the first matching scheme is returned.
180
+ #
181
+ # @param [String] name of the specific scheme
182
+ # @return [Scheme] the specific scheme that matches the name specified
183
+ #
184
+ def scheme(name)
185
+ scheme = @schemes.select {|t| t.name == name.to_s}.first
186
+ raise "No such scheme #{name}, available schemes are #{@schemes.map {|t| t.name}.join(', ')}" if scheme.nil?
187
+ yield scheme if block_given?
188
+ scheme
189
+ end
190
+
191
+ #
192
+ # All the targets specified within the project.
193
+ #
194
+ # @return [Array<PBXNativeTarget>] an array of all the available targets for
195
+ # the specific project.
196
+ #
197
+ def targets
198
+ @project.targets.map do |target|
199
+ target.project = self
200
+ target
201
+ end
202
+ end
203
+
204
+ #
205
+ # Return the target with the specified name. Raises an error if no targets
206
+ # match the specified name.
207
+ #
208
+ # @note if two targets match names, the first matching target is returned.
209
+ #
210
+ # @param [String] name of the specific target
211
+ # @return [PBXNativeTarget] the specific target that matches the name specified
212
+ #
213
+ def target(name)
214
+ target = targets.select {|t| t.name == name.to_s}.first
215
+ raise "No such target #{name}, available targets are #{targets.map {|t| t.name}.join(', ')}" if target.nil?
216
+ yield target if block_given?
217
+ target
218
+ end
219
+
220
+ #
221
+ # Creates a new target within the Xcode project. This will by default not
222
+ # generate all the additional build phases, configurations, and files
223
+ # that create a project.
224
+ #
225
+ # @todo generate a create target with sensible defaults, similar to how
226
+ # it is done through Xcode itself.
227
+ #
228
+ # @todo based on the specified type of target, default build phases and
229
+ # configuration should be created for the target similar to what is
230
+ # supported in xcode. Currently even now the :ios target does not
231
+ # generate the deafult build_phases for you and requires you to make those.
232
+ #
233
+ # @param [String] name the name to provide to the target. This will also
234
+ # be the value that other defaults will be based on.
235
+ #
236
+ def create_target(name,type=:ios)
237
+
238
+ target = @registry.add_object Target.send(type)
239
+ @project.properties['targets'] << target.identifier
240
+
241
+ target.name = name
242
+
243
+ build_configuration_list = @registry.add_object(ConfigurationList.configration_list)
244
+ target.build_configuration_list = build_configuration_list.identifier
245
+
246
+ target.project = self
247
+
248
+ yield target if block_given?
249
+
250
+ target.save!
251
+ end
252
+
253
+ #
254
+ # Remove a target from the Xcode project.
255
+ #
256
+ # @note this will remove the first target that matches the specified name.
257
+ #
258
+ # @note this will remove only the project entry at the moment and not the
259
+ # the files that may be associated with the target. All build phases,
260
+ # build files, and configurations will automatically be cleaned up when
261
+ # Xcode is opened.
262
+ #
263
+ # @param [String] name the name of the target to remove from the Xcode
264
+ # project.
265
+ #
266
+ def remove_target(name)
267
+ found_target = targets.find {|target| target.name == name }
268
+ if found_target
269
+ @project.properties['targets'].delete found_target.identifier
270
+ @registry.remove_object found_target.identifier
271
+ end
272
+ end
273
+
274
+
275
+
276
+ #
277
+ # Prints to STDOUT a description of this project's targets, configuration and schemes.
278
+ #
279
+ def describe
280
+ puts "Project #{name} contains"
281
+ targets.each do |t|
282
+ puts " + target:#{t.name}"
283
+ t.configs.each do |c|
284
+ puts " + config:#{c.name}"
285
+ end
286
+ end
287
+ schemes.each do |s|
288
+ puts " + scheme #{s.name}"
289
+ puts " + Launch action => target:#{s.launch.target.name}, config:#{s.launch.name}" unless s.launch.nil?
290
+ puts " + Test action => target:#{s.test.target.name}, config:#{s.test.name}" unless s.test.nil?
291
+ end
292
+ end
293
+
294
+ private
295
+
296
+ #
297
+ # Parse all the scheme files that can be found within the project. Schemes
298
+ # can be defined as `shared` schemes and then `user` specific schemes. Parsing
299
+ # the schemes will load the shared ones and then the current acting user's
300
+ # schemes.
301
+ #
302
+ def parse_schemes
303
+ shared_schemes = Dir["#{@path}/xcshareddata/xcschemes/*.xcscheme"]
304
+ user_specific_schemes = Dir["#{@path}/xcuserdata/#{ENV['USER']}.xcuserdatad/xcschemes/*.xcscheme"]
305
+
306
+ (shared_schemes + user_specific_schemes).map do |scheme|
307
+ Xcode::Scheme.new(self, scheme)
308
+ end
309
+ end
310
+
311
+ #
312
+ # Using the sytem tool plutil, the specified project file is parsed and
313
+ # converted to JSON, which is then converted to a hash object.
314
+ #
315
+ # This content contains all the data within the project file and is used
316
+ # to create the Registry.
317
+ #
318
+ # @return [Resource] a resource mapped to the root resource within the project
319
+ # this is generally the project file which contains details about the main
320
+ # group, targets, etc.
321
+ #
322
+ # @see Registry
323
+ #
324
+ def parse_pbxproj
325
+ registry = Xcode::PLUTILProjectParser.parse "#{@path}/project.pbxproj"
326
+
327
+ class << registry
328
+ include Xcode::Registry
329
+ end
330
+
331
+ registry
332
+ end
333
+
334
+ end
335
+ end
@@ -0,0 +1,53 @@
1
+ module Xcode
2
+ class ProvisioningProfile
3
+ attr_reader :path, :name, :uuid, :identifiers
4
+ def initialize(path)
5
+
6
+ raise "Provisioning profile '#{path}' does not exist" unless File.exists? path
7
+
8
+ @path = path
9
+ @identifiers = []
10
+
11
+ # TODO: im sure this could be done in a nicer way. maybe read out the XML-like stuff and use the plist -> json converter
12
+ uuid = nil
13
+ File.open(path, "rb") do |f|
14
+ input = f.read
15
+ input=~/<key>UUID<\/key>.*?<string>(.*?)<\/string>/im
16
+ @uuid = $1.strip
17
+
18
+ input=~/<key>Name<\/key>.*?<string>(.*?)<\/string>/im
19
+ @name = $1.strip
20
+
21
+ input=~/<key>ApplicationIdentifierPrefix<\/key>.*?<array>(.*?)<\/array>/im
22
+ $1.split(/<string>/).each do |id|
23
+ next if id.nil? or id.strip==""
24
+ @identifiers << id.gsub(/<\/string>/,'').strip
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ def self.profiles_path
31
+ File.expand_path "~/Library/MobileDevice/Provisioning\\ Profiles/"
32
+ end
33
+
34
+ def install_path
35
+ "#{ProvisioningProfile.profiles_path}/#{self.uuid}.mobileprovision"
36
+ end
37
+
38
+ def install
39
+ Xcode::Shell.execute("cp #{self.path} #{self.install_path}")
40
+ end
41
+
42
+ def uninstall
43
+ Xcode::Shell.execute("rm -f #{self.install_path}")
44
+ end
45
+
46
+ def self.installed_profiles
47
+ Dir["#{self.profiles_path}/*.mobileprovision"].map do |file|
48
+ ProvisioningProfile.new(file)
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,168 @@
1
+ require 'xcode/simple_identifier_generator'
2
+
3
+ module Xcode
4
+
5
+ #
6
+ # The Registry represents the parsed data from the Xcode Project file. Namely
7
+ # the registry is a Hash that provides additional functionality to allow the
8
+ # the ability to query, add, and remove resources from the object hash.
9
+ #
10
+ # Opening the Xcode project file in a text-editor you'll notice that it is a
11
+ # big hash/plist with a file core keys. The most important key is the 'objects'
12
+ # dictionary which maintains the master-list of Identifiers to properties. All
13
+ # objects are represented here and all other resources use the reference
14
+ # to make the connection to the objects.
15
+ #
16
+ # @see Project
17
+ #
18
+ module Registry
19
+
20
+ #
21
+ # This method is used internally to determine if the value that is being
22
+ # retrieved is an identifier.
23
+ #
24
+ # @param [String] value is the specified value in the form of an identifier
25
+ #
26
+ def self.is_identifier? value
27
+ value =~ /^[0-9A-F]{24}$/
28
+ end
29
+
30
+ #
31
+ # Objects within the registry contain an `isa` property, which translates
32
+ # to modules which can be mixed in to provide additional functionality.
33
+ #
34
+ # @param [String] isa the type of the object.
35
+ #
36
+ def self.isa_to_module isa
37
+
38
+ { 'XCBuildConfiguration' => Configuration,
39
+ 'PBXFileReference' => FileReference,
40
+ 'PBXGroup' => Group,
41
+ 'PBXNativeTarget' => Target,
42
+ 'PBXAggregateTarget' => Target,
43
+ 'PBXFrameworksBuildPhase' => BuildPhase,
44
+ 'PBXSourcesBuildPhase' => BuildPhase,
45
+ 'PBXResourcesBuildPhase' => BuildPhase,
46
+ 'PBXBuildFile' => BuildFile,
47
+ 'PBXVariantGroup' => VariantGroup,
48
+ 'XCConfigurationList' => ConfigurationList,
49
+ 'PBXVariantGroup' => VariantGroup }[isa]
50
+ end
51
+
52
+ #
53
+ # This is the root object of the project. This is generally an identifier
54
+ # pointing to a project.
55
+ #
56
+ def root
57
+ self['rootObject']
58
+ end
59
+
60
+
61
+ #
62
+ # This is a hash of all the objects within the project. The keys are the
63
+ # unique identifiers which are 24 length hexadecimal strings. The values
64
+ # are the objects that the keys represent.
65
+ #
66
+ # @return [Hash] that contains all the objects in the project.
67
+ #
68
+ def objects
69
+ self['objects']
70
+ end
71
+
72
+ #
73
+ # Retrieve a Resource for the given identifier.
74
+ #
75
+ # @param [String] identifier the unique identifier for the resource you are
76
+ # attempting to find.
77
+ # @return [Resource] the Resource object the the data properties that would
78
+ # be stored wihin it.
79
+ #
80
+ def object(identifier)
81
+ Resource.new identifier, self
82
+ end
83
+
84
+ #
85
+ # Retrieve the properties Hash for the given identifer.
86
+ #
87
+ # @param [String] identifier the unique identifier for the resource you
88
+ # are attempting to find.
89
+ #
90
+ # @return [Hash] the raw, properties hash for the particular resource; nil
91
+ # if nothing matches the identifier.
92
+ #
93
+ def properties(identifier)
94
+ objects[identifier]
95
+ end
96
+
97
+ MAX_IDENTIFIER_GENERATION_ATTEMPTS = 10
98
+
99
+ #
100
+ # Provides a method to generically add objects to the registry. This will
101
+ # create a unqiue identifier and add the specified parameters to the
102
+ # registry. As all objecst within a the project maintain a reference to this
103
+ # registry they can immediately query for newly created items.
104
+ #
105
+ # @note generally this method should not be called directly and instead
106
+ # resources should provide the ability to assist with generating the
107
+ # correct objects for the registry.
108
+ #
109
+ # @param [Hash] object_properties a hash that contains all the properties
110
+ # that are known for the particular item.
111
+ #
112
+ def add_object(object_properties)
113
+
114
+ new_identifier = SimpleIdentifierGenerator.generate
115
+
116
+ # Ensure that the identifier generated is unique
117
+
118
+ identifier_generation_count = 0
119
+
120
+ while objects.key?(new_identifier)
121
+
122
+ new_identifier = SimpleIdentifierGenerator.generate
123
+
124
+ # Increment our identifier generation count and if we reach our max raise
125
+ # an exception as something has gone horribly wrong.
126
+
127
+ identifier_generation_count += 1
128
+ if identifier_generation_count > MAX_IDENTIFIER_GENERATION_ATTEMPTS
129
+ raise "Unable to generate a unique identifier for object: #{object_properties}"
130
+ end
131
+ end
132
+
133
+ new_identifier = SimpleIdentifierGenerator.generate if objects.key?(new_identifier)
134
+
135
+
136
+ objects[new_identifier] = object_properties
137
+
138
+ Resource.new new_identifier, self
139
+ end
140
+
141
+ #
142
+ # Replace an existing object that shares that same identifier. This is how
143
+ # a Resource is saved back into the registry. So that it will be known to
144
+ # all other objects that it has changed.
145
+ #
146
+ # @see Resource#save!
147
+ #
148
+ # @param [Resource] resource the resource that you want to set at the specified
149
+ # identifier. If an object exists at that identifier already it will be
150
+ # replaced.
151
+ #
152
+ def set_object(resource)
153
+ objects[resource.identifier] = resource.properties
154
+ end
155
+
156
+ #
157
+ # @note removing an item from the regitry does not remove all references
158
+ # to the item within the project. At this time, this could leave resources
159
+ # with references to resources that are invalid.
160
+ #
161
+ # @param [String] identifier of the object to remove from the registry.
162
+ #
163
+ def remove_object(identifier)
164
+ objects.delete identifier
165
+ end
166
+
167
+ end
168
+ end