xcoder 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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