xcoder 0.0.21 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,6 +2,10 @@ require 'plist'
2
2
  require 'pp'
3
3
 
4
4
  module Xcode
5
+
6
+ #
7
+ # @see https://developer.apple.com/library/ios/#documentation/general/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html
8
+ #
5
9
  class InfoPlist
6
10
  def initialize(config, plist_location)
7
11
  @config = config
@@ -0,0 +1,20 @@
1
+ require 'json'
2
+ require 'plist'
3
+
4
+ module Xcode
5
+
6
+ module PLUTILProjectParser
7
+ extend self
8
+
9
+ #
10
+ # Using the sytem tool plutil, the specified project file is parsed and
11
+ # converted to XML, and then converted into a ruby hash object.
12
+ #
13
+ def parse path
14
+ xml = `plutil -convert xml1 -o - "#{path}"`
15
+ Plist::parse_xml(xml)
16
+ end
17
+
18
+ end
19
+
20
+ end
data/lib/xcode/project.rb CHANGED
@@ -1,43 +1,132 @@
1
- require 'json'
1
+ require 'plist'
2
+ require 'xcode/parsers/plutil_project_parser'
3
+ require 'xcode/resource'
2
4
  require 'xcode/target'
3
5
  require 'xcode/configuration'
4
6
  require 'xcode/scheme'
5
- require 'plist'
7
+ require 'xcode/group'
8
+ require 'xcode/file_reference'
9
+ require 'xcode/registry'
10
+ require 'xcode/build_phase'
11
+ require 'xcode/variant_group'
6
12
 
7
13
  module Xcode
8
14
  class Project
9
- attr_reader :name, :targets, :sdk, :path, :schemes, :groups
15
+
16
+ attr_reader :name, :sdk, :path, :schemes, :registry
17
+
18
+ #
19
+ # Initialized with a specific path and sdk.
20
+ #
21
+ # This initialization is not often used. Instead projects are generated
22
+ # through the Xcode#project method.
23
+ #
24
+ # @see Xcode
25
+ #
26
+ # @param [String] path of the project to open.
27
+ # @param [String] sdk the sdk value of the project. This will default to
28
+ # `iphoneos`.
29
+ #
10
30
  def initialize(path, sdk=nil)
11
31
  @sdk = sdk || "iphoneos" # FIXME: should support OSX/simulator too
12
32
  @path = File.expand_path path
13
- @targets = []
14
33
  @schemes = []
15
34
  @groups = []
16
35
  @name = File.basename(@path).gsub(/\.xcodeproj/,'')
17
-
18
- parse_pbxproj
19
- parse_schemes
20
- # parse_configurations
36
+
37
+ # Parse the Xcode project file and create the registry
38
+
39
+ @registry = parse_pbxproj
40
+ @project = Xcode::Resource.new registry.root, @registry
41
+
42
+ @schemes = parse_schemes
21
43
  end
22
44
 
23
- def group(name)
24
-
45
+ #
46
+ # Returns the main group of the project where all the files reside.
47
+ #
48
+ # @return [PBXGroup]
49
+ # @see PBXGroup
50
+ #
51
+ def groups
52
+ @project.mainGroup
53
+ end
54
+
55
+ #
56
+ # Save the current project at the current path that it exists.
57
+ #
58
+ def save!
59
+ save @path
25
60
  end
26
61
 
27
- def save
28
- # Save modified groups/f
62
+ #
63
+ # Saves the current project at the specified path.
64
+ #
65
+ # @note currently this does not support saving the workspaces associated
66
+ # with the project to their new location.
67
+ #
68
+ # @param [String] path the path to save the project
69
+ #
70
+ def save(path)
71
+ Dir.mkdir(path) unless File.exists?(path)
72
+
73
+ project_filepath = "#{path}/project.pbxproj"
74
+
75
+ # @toodo Save the workspace when the project is saved
76
+ # FileUtils.cp_r "#{path}/project.xcworkspace", "#{path}/project.xcworkspace"
77
+
78
+ File.open(project_filepath,'w') do |file|
79
+
80
+ # The Hash#to_xcplist saves a semi-colon at the end which needs to be removed
81
+ # to ensure the project file can be opened.
82
+
83
+ file.puts %{// !$*UTF8*$!"\n#{@registry.to_xcplist.gsub(/\};\s*\z/,'}')}}
84
+
85
+ end
29
86
  end
30
87
 
88
+
89
+ #
90
+ # Return the scheme with the specified name. Raises an error if no schemes
91
+ # match the specified name.
92
+ #
93
+ # @note if two schemes match names, the first matching scheme is return.
94
+ #
95
+ # @param [String] name of the specific scheme
96
+ # @return [Scheme] the specific scheme that matches the name specified
97
+ #
31
98
  def scheme(name)
32
99
  scheme = @schemes.select {|t| t.name == name.to_s}.first
33
100
  raise "No such scheme #{name}, available schemes are #{@schemes.map {|t| t.name}.join(', ')}" if scheme.nil?
34
101
  yield scheme if block_given?
35
102
  scheme
36
103
  end
37
-
104
+
105
+ #
106
+ # All the targets specified within the project.
107
+ #
108
+ # @return [Array<PBXNativeTarget>] an array of all the available targets for
109
+ # the specific project.
110
+ #
111
+ def targets
112
+ @project.targets.map do |target|
113
+ target.project = self
114
+ target
115
+ end
116
+ end
117
+
118
+ #
119
+ # Return the target with the specified name. Raises an error if no targets
120
+ # match the specified name.
121
+ #
122
+ # @note if two targets match names, the first matching target is returned.
123
+ #
124
+ # @param [String] name of the specific target
125
+ # @return [PBXNativeTarget] the specific target that matches the name specified
126
+ #
38
127
  def target(name)
39
- target = @targets.select {|t| t.name == name.to_s}.first
40
- raise "No such target #{name}, available targets are #{@targets.map {|t| t.name}.join(', ')}" if target.nil?
128
+ target = targets.select {|t| t.name == name.to_s}.first
129
+ raise "No such target #{name}, available targets are #{targets.map {|t| t.name}.join(', ')}" if target.nil?
41
130
  yield target if block_given?
42
131
  target
43
132
  end
@@ -59,32 +148,42 @@ module Xcode
59
148
 
60
149
  private
61
150
 
151
+ #
152
+ # Parse all the scheme files that can be found within the project. Schemes
153
+ # can be defined as `shared` schemes and then `user` specific schemes. Parsing
154
+ # the schemes will load the shared ones and then the current acting user's
155
+ # schemes.
156
+ #
62
157
  def parse_schemes
63
- # schemes are in project/**/xcschemes/*.xcscheme
64
- Dir["#{@path}/**/xcschemes/*.xcscheme"].each do |scheme|
65
- @schemes << Xcode::Scheme.new(self, scheme)
158
+ shared_schemes = Dir["#{@path}/xcshareddata/xcschemes/*.xcscheme"]
159
+ user_specific_schemes = Dir["#{@path}/xcuserdata/#{ENV['USER']}.xcuserdatad/xcschemes/*.xcscheme"]
160
+
161
+ (shared_schemes + user_specific_schemes).map do |scheme|
162
+ Xcode::Scheme.new(self, scheme)
66
163
  end
67
164
  end
68
165
 
166
+ #
167
+ # Using the sytem tool plutil, the specified project file is parsed and
168
+ # converted to JSON, which is then converted to a hash object.
169
+ #
170
+ # This content contains all the data within the project file and is used
171
+ # to create the Registry.
172
+ #
173
+ # @return [Resource] a resource mapped to the root resource within the project
174
+ # this is generally the project file which contains details about the main
175
+ # group, targets, etc.
176
+ #
177
+ # @see Registry
178
+ #
69
179
  def parse_pbxproj
70
- xml = `plutil -convert xml1 -o - "#{@path}/project.pbxproj"`
71
- # json = JSON.parse()
72
- json = Plist::parse_xml(xml)
180
+ registry = Xcode::PLUTILProjectParser.parse "#{@path}/project.pbxproj"
73
181
 
74
- root = json['objects'][json['rootObject']]
75
-
76
- root['targets'].each do |target_id|
77
- target = Xcode::Target.new(self, json['objects'][target_id])
78
-
79
- buildConfigurationList = json['objects'][target_id]['buildConfigurationList']
80
- buildConfigurations = json['objects'][buildConfigurationList]['buildConfigurations']
81
-
82
- buildConfigurations.each do |buildConfiguration|
83
- target.configs << Xcode::Configuration.new(target, json['objects'][buildConfiguration])
84
- end
85
-
86
- @targets << target
182
+ class << registry
183
+ include Xcode::Registry
87
184
  end
185
+
186
+ registry
88
187
  end
89
188
 
90
189
  end
@@ -0,0 +1,120 @@
1
+ module Xcode
2
+
3
+ #
4
+ # The Registry represents the parsed data from the Xcode Project file. Namely
5
+ # the registry is a Hash that provides additional functionality to allow the
6
+ # the ability to query, add, and remove resources from the object hash.
7
+ #
8
+ # Opening the Xcode project file in a text-editor you'll notice that it is a
9
+ # big hash/plist with a file core keys. The most important key is the 'objects'
10
+ # dictionary which maintains the master-list of Identifiers to properties. All
11
+ # objects are represented here and all other resources use the reference
12
+ # to make the connection to the objects.
13
+ #
14
+ # @see Project
15
+ #
16
+ module Registry
17
+
18
+ #
19
+ # This method is used internally to determine if the value that is being
20
+ # retrieved is an identifier.
21
+ #
22
+ # @todo this should likely be moved to the Regsitry which knows much more
23
+ # about identifiers and what makes them valid.
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 }[isa]
48
+
49
+ end
50
+
51
+ #
52
+ # This is the root object of the project. This is generally an identifier
53
+ # pointing to a project.
54
+ #
55
+ def root
56
+ self['rootObject']
57
+ end
58
+
59
+
60
+ #
61
+ # This is a hash of all the objects within the project. The keys are the
62
+ # unique identifiers which are 24 length hexadecimal strings. The values
63
+ # are the objects that the keys represent.
64
+ #
65
+ # @return [Hash] that contains all the objects in the project.
66
+ #
67
+ def objects
68
+ self['objects']
69
+ end
70
+
71
+ #
72
+ #
73
+ # @return [String] the object associated with this identifier; nil if no
74
+ # object matches the identifier.
75
+ #
76
+ def object(identifier)
77
+ objects[identifier]
78
+ end
79
+
80
+ #
81
+ # Provides a method to generically add objects to the registry. This will
82
+ # create a unqiue identifier and add the specified parameters to the
83
+ # registry. As all objecst within a the project maintain a reference to this
84
+ # registry they can immediately query for newly created items.
85
+ #
86
+ # @note generally this method should not be called directly and instead
87
+ # resources should provide the ability to assist with generating the
88
+ # correct objects for the registry.
89
+ #
90
+ # @param [Hash] object_properties a hash that contains all the properties
91
+ # that are known for the particular item.
92
+ #
93
+ def add_object(object_properties)
94
+ # define a new group within the object list
95
+ # add it as a child
96
+
97
+ range = ('A'..'F').to_a + (0..9).to_a
98
+
99
+ new_identifier = 24.times.inject("") {|ident| "#{ident}#{range.sample}" }
100
+
101
+ # TODO ensure identifier does not collide with other identifiers
102
+
103
+ objects[new_identifier] = object_properties
104
+
105
+ new_identifier
106
+ end
107
+
108
+ #
109
+ # @note removing an item from the regitry does not remove all references
110
+ # to the item within the project. At this time, this could leave resources
111
+ # with references to resources that are invalid.
112
+ #
113
+ # @param [String] identifier of the object to remove from the registry.
114
+ #
115
+ def remove_object(identifier)
116
+ objects.delete identifier
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,187 @@
1
+
2
+ module Xcode
3
+
4
+ #
5
+ # Resources are not represented as a true entity within an Xcode project.
6
+ # When traversing through groups, targets, configurations, etc. you will find
7
+ # yourself interacting with these objects. As they represent a class that
8
+ # acts as a shim around the hash data is parsed from the project.
9
+ #
10
+ # A resource do some things that should be explained:
11
+ #
12
+ # When a resource is created it requires an identifier and an instance of
13
+ # of the Registry. It finds the properties hash of that given identifier
14
+ # within the registry and creates a bunch of read-only methods that allow for
15
+ # easy access to those elements. This is not unlike an OpenStruct.
16
+ #
17
+ # @example of accessing the contents of a file reference
18
+ #
19
+ # file_resource.properties # =>
20
+ #
21
+ # { 'isa' => 'PBXFileReference',
22
+ # 'lastKnownFileType' => 'sourcecode.c.h',
23
+ # 'path' => IOSAppDelegate.h',
24
+ # 'sourceTree' => "<group>" }
25
+ #
26
+ # file_resource.isa # => 'PBXFileReference'
27
+ # file_resource.sourceTree # => "<group>"
28
+ #
29
+ #
30
+ # To provide additional convenience when traversing through the
31
+ # various objects, the read-only method will check to see if the value
32
+ # being returned matches that of a unique identifier. If it does, instead of
33
+ # providing that identifier as a result and then having additional code to
34
+ # perform the look up, it does it automatically.
35
+ #
36
+ # @example of how this would have to been done without this indirection
37
+ #
38
+ # project = Xcode.project('MyProject')
39
+ # main_group = project.groups
40
+ # child_identifier = group.children.first
41
+ # subgroup = project.registry['objects'][child_identifier]
42
+ #
43
+ # @example of hot this works currently because of this indirection
44
+ #
45
+ # group = Xcode.project('MyProject.xcodeproj').mainGroup
46
+ # subgroup = group.group('Models')
47
+ #
48
+ #
49
+ # Next, as most every one of these objects is a Hash that contain the properties
50
+ # instead of objects it would likely be better to encapsulate these resources
51
+ # within specific classes that provide additional functionality. So that when
52
+ # a group resource or a file resource is returned you can perform unique
53
+ # functionality with it automatically.
54
+ #
55
+ # This is done by using the 'isa' property field which contains the type of
56
+ # content object. Instead of creating an object and encapsulating if a module
57
+ # that matches the name of the 'isa', that module of functionality is
58
+ # automatically mixed-in to provide that functionality.
59
+ #
60
+ class Resource
61
+
62
+ # The unique identifier for this resource
63
+ attr_accessor :identifier
64
+
65
+ # The properties hash that is known about the resource
66
+ attr_accessor :properties
67
+
68
+ # The registry of all objects within the project file which all resources
69
+ # have a reference to so that they can retrieve any items they may own that
70
+ # are simply referenced by identifiers.
71
+ attr_accessor :registry
72
+
73
+ #
74
+ # Definiing a property allows the creation of an alias to the actual value.
75
+ # This level of indirection allows for the replacement of values which are
76
+ # identifiers with a resource representation of it.
77
+ #
78
+ # @note This is used internally by the resource when it is created.
79
+ #
80
+ # @param [String] name of the property
81
+ # @param [String] value of the property
82
+ #
83
+ def define_property name, value
84
+
85
+ # Save the properties within the resource within a custom hash. This
86
+ # provides access to them without the indirection that we are about to
87
+ # set up.
88
+
89
+ @properties[name] = value
90
+
91
+ # Generate a getter method for this property based on the given name.
92
+
93
+ self.class.send :define_method, name do
94
+
95
+ # Retrieve the value that is current stored for this name.
96
+
97
+ raw_value = @properties[name]
98
+
99
+ # If the value is an array then we want to examine each element within
100
+ # the array to see if any of them are identifiers that we should replace
101
+ # finally returning all of the items as their resource representations
102
+ # or as their raw values.
103
+ #
104
+ # If the value is not an array then we want to examine that item and
105
+ # return the resource representation or the raw value.
106
+
107
+ if raw_value.is_a?(Array)
108
+
109
+ Array(raw_value).map do |sub_value|
110
+
111
+ if Registry.is_identifier? sub_value
112
+ Resource.new sub_value, @registry
113
+ else
114
+ sub_value
115
+ end
116
+ end
117
+
118
+ else
119
+
120
+ if Registry.is_identifier? raw_value
121
+ Resource.new raw_value, @registry
122
+ else
123
+ raw_value
124
+ end
125
+
126
+ end
127
+
128
+ end
129
+
130
+ end
131
+
132
+ #
133
+ # Within the code, a single resource is created and that is with the root
134
+ # projet object. All other resources are created through the indirecation of
135
+ # the above property methods.
136
+ #
137
+ # @param [String] identifier the unique identifier for this resource.
138
+ # @param [Types] details Description
139
+ #
140
+ def initialize identifier, details
141
+ @registry = details
142
+ @properties = {}
143
+ @identifier = identifier
144
+
145
+ # Create property methods for all of the key-value pairs found in the
146
+ # registry for specified identifier.
147
+
148
+ Array(details.object(@identifier)).each do |key,value|
149
+ send :define_property, key, value
150
+ end
151
+
152
+ #
153
+ # Based on the `isa` property find if there is a constant within
154
+ # the Xcode module that matches and if it does, then we want to
155
+ # automatically include module into the Resource object.
156
+ #
157
+ constant = Registry.isa_to_module(isa)
158
+
159
+ self.extend(constant) if constant
160
+
161
+ end
162
+
163
+ #
164
+ # @return [String] a representation with the identifier and the properties
165
+ # for this resource.
166
+ #
167
+ def to_s
168
+ "#{isa} #{@identifier} #{@properties}"
169
+ end
170
+
171
+ #
172
+ # This will generate the resource in the format that is supported by the
173
+ # Xcode project file. Which requires each key value pair to be represented.
174
+ #
175
+ # @return [String] a string representation of the object so that it can
176
+ # be persisted to an Xcode project file.
177
+ #
178
+ def to_xcplist
179
+ %{
180
+ #{@identifier} = { #{ @properties.map {|k,v| "#{k} = \"#{v}\"" }.join("; ") } }
181
+
182
+ }
183
+ end
184
+
185
+ end
186
+
187
+ end