xcodeproj 0.1.0 → 0.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ #ifndef EXTCONF_H
2
+ #define EXTCONF_H
3
+ #define HAVE_FRAMEWORK_COREFOUNDATION 1
4
+ #define HAVE_COREFOUNDATION_COREFOUNDATION_H 1
5
+ #define HAVE_COREFOUNDATION_CFSTREAM_H 1
6
+ #define HAVE_COREFOUNDATION_CFPROPERTYLIST_H 1
7
+ #define HAVE_RUBY_ST_H 1
8
+ #endif
@@ -37,6 +37,7 @@ end
37
37
  have_header 'CoreFoundation/CoreFoundation.h'
38
38
  have_header 'CoreFoundation/CFStream.h'
39
39
  have_header 'CoreFoundation/CFPropertyList.h'
40
+ have_header 'ruby/st.h'
40
41
 
41
42
  create_header
42
43
  create_makefile 'xcodeproj_ext'
@@ -4,7 +4,10 @@
4
4
  #include "extconf.h"
5
5
 
6
6
  #include "ruby.h"
7
+ #if HAVE_RUBY_ST_H
7
8
  #include "ruby/st.h"
9
+ #endif
10
+
8
11
  #include "CoreFoundation/CoreFoundation.h"
9
12
  #include "CoreFoundation/CFStream.h"
10
13
  #include "CoreFoundation/CFPropertyList.h"
@@ -29,7 +32,14 @@ str_to_cfstr(VALUE str) {
29
32
  return CFStringCreateWithCString(NULL, RSTRING_PTR(rb_String(str)), kCFStringEncodingUTF8);
30
33
  }
31
34
 
32
-
35
+ /* Generates a UUID. The original version is truncated, so this is not 100%
36
+ * guaranteed to be unique. However, the `PBXObject#generate_uuid` method
37
+ * checks that the UUID does not exist yet, in the project, before using it.
38
+ *
39
+ * @note Meant for internal use only.
40
+ *
41
+ * @return [String] A 24 characters long UUID.
42
+ */
33
43
  static VALUE
34
44
  generate_uuid(void) {
35
45
  CFUUIDRef uuid = CFUUIDCreate(NULL);
@@ -69,11 +79,22 @@ hash_set(const void *keyRef, const void *valueRef, void *hash) {
69
79
  value = rb_ary_new();
70
80
  CFIndex i, count = CFArrayGetCount(valueRef);
71
81
  for (i = 0; i < count; i++) {
72
- CFStringRef x = CFArrayGetValueAtIndex(valueRef, i);
73
- if (CFGetTypeID(x) == CFStringGetTypeID()) {
74
- rb_ary_push(value, cfstr_to_str(x));
82
+ CFTypeRef elementRef = CFArrayGetValueAtIndex(valueRef, i);
83
+ CFTypeID elementType = CFGetTypeID(elementRef);
84
+ if (elementType == CFStringGetTypeID()) {
85
+ rb_ary_push(value, cfstr_to_str(elementRef));
86
+
87
+ } else if (elementType == CFDictionaryGetTypeID()) {
88
+ VALUE hashElement = rb_hash_new();
89
+ CFDictionaryApplyFunction(elementRef, hash_set, (void *)hashElement);
90
+ rb_ary_push(value, hashElement);
91
+
75
92
  } else {
76
- rb_raise(rb_eTypeError, "Plist array value contains a object type unsupported by Xcodeproj.");
93
+ CFStringRef descriptionRef = CFCopyDescription(elementRef);
94
+ // obviously not optimial, but we're raising here, so it doesn't really matter
95
+ VALUE description = cfstr_to_str(descriptionRef);
96
+ rb_raise(rb_eTypeError, "Plist array value contains a object type unsupported by Xcodeproj. In: `%s'", RSTRING_PTR(description));
97
+ CFRelease(descriptionRef);
77
98
  }
78
99
  }
79
100
 
@@ -94,15 +115,26 @@ dictionary_set(st_data_t key, st_data_t value, CFMutableDictionaryRef dict) {
94
115
  0,
95
116
  &kCFTypeDictionaryKeyCallBacks,
96
117
  &kCFTypeDictionaryValueCallBacks);
97
- st_foreach(RHASH_TBL(value), dictionary_set, (st_data_t)valueRef);
118
+ rb_hash_foreach(value, dictionary_set, (st_data_t)valueRef);
98
119
 
99
120
  } else if (TYPE(value) == T_ARRAY) {
100
121
  long i, count = RARRAY_LEN(value);
101
122
  valueRef = CFArrayCreateMutable(NULL, count, &kCFTypeArrayCallBacks);
102
123
  for (i = 0; i < count; i++) {
103
- CFStringRef x = str_to_cfstr(RARRAY_PTR(value)[i]);
104
- CFArrayAppendValue((CFMutableArrayRef)valueRef, x);
105
- CFRelease(x);
124
+ VALUE element = RARRAY_PTR(value)[i];
125
+ CFTypeRef elementRef = NULL;
126
+ if (TYPE(element) == T_HASH) {
127
+ elementRef = CFDictionaryCreateMutable(NULL,
128
+ 0,
129
+ &kCFTypeDictionaryKeyCallBacks,
130
+ &kCFTypeDictionaryValueCallBacks);
131
+ rb_hash_foreach(element, dictionary_set, (st_data_t)elementRef);
132
+ } else {
133
+ // otherwise coerce to string
134
+ elementRef = str_to_cfstr(element);
135
+ }
136
+ CFArrayAppendValue((CFMutableArrayRef)valueRef, elementRef);
137
+ CFRelease(elementRef);
106
138
  }
107
139
 
108
140
  } else {
@@ -130,7 +162,19 @@ str_to_url(VALUE path) {
130
162
  }
131
163
 
132
164
 
133
- // TODO handle errors
165
+ /* @overload read_plist(path)
166
+ *
167
+ * Reads from the specified path and de-serializes the property list.
168
+ *
169
+ * @note This does not yet support all possible types that can exist in a valid property list.
170
+ *
171
+ * @note This currently only assumes to be given an Xcode project document.
172
+ * This means that it only accepts dictionaries, arrays, and strings in
173
+ * the document.
174
+ *
175
+ * @param [String] path The path to the property list file.
176
+ * @return [Hash] The dictionary contents of the document.
177
+ */
134
178
  static VALUE
135
179
  read_plist(VALUE self, VALUE path) {
136
180
  CFPropertyListRef dict;
@@ -159,6 +203,20 @@ read_plist(VALUE self, VALUE path) {
159
203
  return hash;
160
204
  }
161
205
 
206
+ /* @overload write_plist(hash, path)
207
+ *
208
+ * Writes the serialized contents of a property list to the specified path.
209
+ *
210
+ * @note This does not yet support all possible types that can exist in a valid property list.
211
+ *
212
+ * @note This currently only assumes to be given an Xcode project document.
213
+ * This means that it only accepts dictionaries, arrays, and strings in
214
+ * the document.
215
+ *
216
+ * @param [Hash] hash The property list to serialize.
217
+ * @param [String] path The path to the property list file.
218
+ * @return [true, false] Wether or not saving was successful.
219
+ */
162
220
  static VALUE
163
221
  write_plist(VALUE self, VALUE hash, VALUE path) {
164
222
  VALUE h = rb_check_convert_type(hash, T_HASH, "Hash", "to_hash");
@@ -171,7 +229,7 @@ write_plist(VALUE self, VALUE hash, VALUE path) {
171
229
  &kCFTypeDictionaryKeyCallBacks,
172
230
  &kCFTypeDictionaryValueCallBacks);
173
231
 
174
- st_foreach(RHASH_TBL(h), dictionary_set, (st_data_t)dict);
232
+ rb_hash_foreach(h, dictionary_set, (st_data_t)dict);
175
233
 
176
234
  CFURLRef fileURL = str_to_url(path);
177
235
  CFWriteStreamRef stream = CFWriteStreamCreateWithFile(NULL, fileURL);
data/lib/xcodeproj.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Xcodeproj
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0.rc1'
3
3
 
4
4
  autoload :Config, 'xcodeproj/config'
5
5
  autoload :Project, 'xcodeproj/project'
@@ -1,31 +1,163 @@
1
1
  module Xcodeproj
2
+ # This class holds the data for a Xcode build settings file (xcconfig) and
3
+ # serializes it.
2
4
  class Config
3
- def initialize(xcconfig = {})
5
+ # Returns a new instance of Config
6
+ #
7
+ # @param [Hash, File, String] xcconfig_hash_or_file Initial data.
8
+ require 'set'
9
+
10
+ attr_accessor :attributes, :frameworks ,:libraries
11
+
12
+ def initialize(xcconfig_hash_or_file = {})
4
13
  @attributes = {}
5
- merge!(xcconfig)
14
+ @includes = []
15
+ @frameworks, @libraries = Set.new, Set.new
16
+ merge!(extract_hash(xcconfig_hash_or_file))
6
17
  end
7
18
 
19
+ # @return [Hash] The internal data.
8
20
  def to_hash
9
- @attributes
21
+ hash = @attributes.dup
22
+ flags = hash['OTHER_LDFLAGS'] || ''
23
+ flags = flags.dup.strip
24
+ flags << libraries.to_a.sort.reduce('') {| memo, l | memo << " -l#{l}" }
25
+ flags << frameworks.to_a.sort.reduce('') {| memo, f | memo << " -framework #{f}" }
26
+ hash['OTHER_LDFLAGS'] = flags.strip
27
+ hash.delete('OTHER_LDFLAGS') if flags.strip.empty?
28
+ hash
10
29
  end
11
30
 
31
+ def ==(other)
32
+ other.respond_to?(:to_hash) && other.to_hash == self.to_hash
33
+ end
34
+
35
+ # @return [Array] Config's include file list
36
+ # @example
37
+ #
38
+ # Consider following xcconfig file:
39
+ #
40
+ # #include "SomeConfig"
41
+ # Key1 = Value1
42
+ # Key2 = Value2
43
+ #
44
+ # config.includes # => [ "SomeConfig" ]
45
+ def includes
46
+ @includes
47
+ end
48
+
49
+ # Merges the given xcconfig hash or Config into the internal data.
50
+ #
51
+ # If a key in the given hash already exists, in the internal data, then its
52
+ # value is appended to the value in the internal data.
53
+ #
54
+ # @example
55
+ #
56
+ # config = Config.new('PODS_ROOT' => '"$(SRCROOT)/Pods"', 'OTHER_LDFLAGS' => '-lxml2')
57
+ # config.merge!('OTHER_LDFLAGS' => '-lz', 'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/Headers"')
58
+ # config.to_hash # => { 'PODS_ROOT' => '"$(SRCROOT)/Pods"', 'OTHER_LDFLAGS' => '-lxml2 -lz', 'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/Headers"' }
59
+ #
60
+ # @param [Hash, Config] xcconfig The data to merge into the internal data.
12
61
  def merge!(xcconfig)
13
- xcconfig.to_hash.each do |key, value|
14
- if existing_value = @attributes[key]
15
- @attributes[key] = "#{existing_value} #{value}"
16
- else
17
- @attributes[key] = value
18
- end
62
+ if xcconfig.is_a? Config
63
+ @attributes.merge!(xcconfig.attributes) { |key, v1, v2| "#{v1} #{v2}" }
64
+ @libraries.merge xcconfig.libraries
65
+ @frameworks.merge xcconfig.frameworks
66
+ else
67
+ @attributes.merge!(xcconfig.to_hash) { |key, v1, v2| "#{v1} #{v2}" }
68
+ # Parse frameworks and libraries. Then remove the from the linker flags
69
+ flags = @attributes['OTHER_LDFLAGS']
70
+ return unless flags
71
+
72
+ frameworks = flags.scan(/-framework\s+([^\s]+)/).map { |m| m[0] }
73
+ libraries = flags.scan(/-l ?([^\s]+)/).map { |m| m[0] }
74
+ @frameworks.merge frameworks
75
+ @libraries.merge libraries
76
+
77
+ new_flags = flags.dup
78
+ frameworks.each {|f| new_flags.gsub!("-framework #{f}", "") }
79
+ libraries.each {|l| new_flags.gsub!("-l#{l}", ""); new_flags.gsub!("-l #{l}", "") }
80
+ @attributes['OTHER_LDFLAGS'] = new_flags.gsub("\w*", ' ').strip
19
81
  end
20
82
  end
21
83
  alias_method :<<, :merge!
22
84
 
85
+ def merge(config)
86
+ self.dup.tap { |x|x.merge!(config) }
87
+ end
88
+
89
+ def dup
90
+ Xcodeproj::Config.new(self.to_hash.dup)
91
+ end
92
+
93
+ # Serializes the internal data in the xcconfig format.
94
+ #
95
+ # @example
96
+ #
97
+ # config = Config.new('PODS_ROOT' => '"$(SRCROOT)/Pods"', 'OTHER_LDFLAGS' => '-lxml2')
98
+ # config.to_s # => "PODS_ROOT = \"$(SRCROOT)/Pods\"\nOTHER_LDFLAGS = -lxml2"
99
+ #
100
+ # @return [String] The serialized internal data.
23
101
  def to_s
24
- @attributes.map { |key, value| "#{key} = #{value}" }.join("\n")
102
+ to_hash.map { |key, value| "#{key} = #{value}" }.join("\n")
103
+ end
104
+
105
+ def inspect
106
+ to_hash.inspect
25
107
  end
26
108
 
109
+ # Writes the serialized representation of the internal data to the given
110
+ # path.
111
+ #
112
+ # @param [Pathname] pathname The file that the data should be written to.
27
113
  def save_as(pathname)
28
114
  pathname.open('w') { |file| file << to_s }
29
115
  end
116
+
117
+ private
118
+
119
+ def extract_hash(argument)
120
+ if argument.respond_to? :read
121
+ hash_from_file_content(argument.read)
122
+ elsif File.readable? argument.to_s
123
+ hash_from_file_content(File.read(argument))
124
+ else
125
+ argument
126
+ end
127
+ end
128
+
129
+ def hash_from_file_content(raw_string)
130
+ hash = {}
131
+ raw_string.split("\n").each do |line|
132
+ uncommented_line = strip_comment(line)
133
+ if include = extract_include(uncommented_line)
134
+ @includes.push include
135
+ else
136
+ key, value = extract_key_value(uncommented_line)
137
+ hash[key] = value if key
138
+ end
139
+ end
140
+ hash
141
+ end
142
+
143
+ def strip_comment(line)
144
+ line.partition('//').first
145
+ end
146
+
147
+ def extract_include(line)
148
+ regexp = /#include\s*"(.+)"/
149
+ match = line.match(regexp)
150
+ match[1] if match
151
+ end
152
+
153
+ def extract_key_value(line)
154
+ key, value = line.split('=', 2)
155
+ if key && value
156
+ [key.strip, value.strip]
157
+ else
158
+ []
159
+ end
160
+ end
161
+
30
162
  end
31
163
  end
@@ -189,7 +189,7 @@ module Xcodeproj
189
189
  if first_letter_in_uppercase
190
190
  lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
191
191
  else
192
- lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
192
+ lower_case_and_underscored_word[0,1].downcase + camelize(lower_case_and_underscored_word)[1..-1]
193
193
  end
194
194
  end
195
195
 
@@ -1,555 +1,37 @@
1
1
  require 'fileutils'
2
- require 'xcodeproj/inflector'
2
+ require 'pathname'
3
3
  require 'xcodeproj/xcodeproj_ext'
4
4
 
5
+ require 'xcodeproj/project/object'
6
+
5
7
  module Xcodeproj
8
+ # This class represents a Xcode project document.
9
+ #
10
+ # It can be used to manipulate existing documents or even create new ones
11
+ # from scratch.
12
+ #
13
+ # The Project API returns instances of AbstractPBXObject which wrap the objects
14
+ # described in the Xcode project document.
6
15
  class Project
7
- class PBXObject
8
- class AssociationReflection
9
- def initialize(name, options)
10
- @name, @options = name.to_s, options
11
- end
12
-
13
- attr_reader :name, :options
14
-
15
- def klass
16
- @options[:class] ||= begin
17
- name = "PBX#{@name.classify}"
18
- name = "XC#{@name.classify}" unless Project.const_defined?(name)
19
- Project.const_get(name)
20
- end
21
- end
22
-
23
- def inverse
24
- klass.reflection(@options[:inverse_of])
25
- end
26
-
27
- def inverse?
28
- !!@options[:inverse_of]
29
- end
30
-
31
- def singular_name
32
- @options[:singular_name] || @name.singularize
33
- end
34
-
35
- def singular_getter
36
- singular_name
37
- end
38
-
39
- def singular_setter
40
- "#{singular_name}="
41
- end
42
-
43
- def plural_name
44
- @name.pluralize
45
- end
46
-
47
- def plural_getter
48
- plural_name
49
- end
50
-
51
- def plural_setter
52
- "#{plural_name}="
53
- end
54
-
55
- def uuid_attribute
56
- @options[:uuid] || @name
57
- end
58
-
59
- def uuid_method_name
60
- (@options[:uuid] || @options[:uuids] || "#{singular_name}Reference").to_s.singularize
61
- end
62
-
63
- def uuid_getter
64
- uuid_method_name
65
- end
66
-
67
- def uuid_setter
68
- "#{uuid_method_name}="
69
- end
70
-
71
- def uuids_method_name
72
- uuid_method_name.pluralize
73
- end
74
-
75
- def uuids_getter
76
- uuids_method_name
77
- end
78
-
79
- def uuids_setter
80
- "#{uuids_method_name}="
81
- end
82
- end
83
-
84
- def self.reflections
85
- @reflections ||= []
86
- end
87
-
88
- def self.create_reflection(name, options)
89
- (reflections << AssociationReflection.new(name, options)).last
90
- end
91
-
92
- def self.reflection(name)
93
- reflections.find { |r| r.name.to_s == name.to_s }
94
- end
95
-
96
- def self.attribute(attribute_name, accessor_name = nil)
97
- attribute_name = attribute_name.to_s
98
- name = (accessor_name || attribute_name).to_s
99
- define_method(name) { @attributes[attribute_name] }
100
- define_method("#{name}=") { |value| @attributes[attribute_name] = value }
101
- end
102
-
103
- def self.attributes(*names)
104
- names.each { |name| attribute(name) }
105
- end
106
-
107
- def self.has_many(plural_attr_name, options = {}, &block)
108
- reflection = create_reflection(plural_attr_name, options)
109
- if reflection.inverse?
110
- define_method(reflection.name) do
111
- scoped = @project.objects.select_by_class(reflection.klass).select do |object|
112
- object.send(reflection.inverse.uuid_getter) == self.uuid
113
- end
114
- PBXObjectList.new(reflection.klass, @project, scoped) do |object|
115
- object.send(reflection.inverse.uuid_setter, self.uuid)
116
- end
117
- end
118
- else
119
- attribute(reflection.name, reflection.uuids_getter)
120
- define_method(reflection.name) do
121
- uuids = send(reflection.uuids_getter)
122
- if block
123
- # Evaluate the block, which was specified at the class level, in
124
- # the instance’s context.
125
- list_by_class(uuids, reflection.klass) do |object|
126
- instance_exec(object, &block)
127
- end
128
- else
129
- list_by_class(uuids, reflection.klass)
130
- end
131
- end
132
- define_method(reflection.plural_setter) do |objects|
133
- send(reflection.uuids_setter, objects.map(&:uuid))
134
- end
135
- end
136
- end
137
-
138
- def self.has_one(singular_attr_name, options = {})
139
- reflection = create_reflection(singular_attr_name, options)
140
- if reflection.inverse?
141
- define_method(reflection.name) do
142
- # Loop over all objects of the class and find the one that includes
143
- # this object in the specified uuid list.
144
- @project.objects.select_by_class(reflection.klass).find do |object|
145
- object.send(reflection.inverse.uuids_getter).include?(self.uuid)
146
- end
147
- end
148
- define_method(reflection.singular_setter) do |object|
149
- # Remove this object from the uuid list of the target
150
- # that this object was associated to.
151
- if previous = send(reflection.name)
152
- previous.send(reflection.inverse.uuids_getter).delete(self.uuid)
153
- end
154
- # Now assign this object to the new object
155
- object.send(reflection.inverse.uuids_getter) << self.uuid if object
156
- end
157
- else
158
- attribute(reflection.uuid_attribute, reflection.uuid_getter)
159
- define_method(reflection.name) do
160
- @project.objects[send(reflection.uuid_getter)]
161
- end
162
- define_method(reflection.singular_setter) do |object|
163
- send(reflection.uuid_setter, object.uuid)
164
- end
165
- end
166
- end
167
-
168
- def self.isa
169
- @isa ||= name.split('::').last
170
- end
171
-
172
- attr_reader :uuid, :attributes
173
- attributes :isa, :name
174
-
175
- def initialize(project, uuid, attributes)
176
- @project, @attributes = project, attributes
177
- unless uuid
178
- # Add new objects to the main hash with a unique UUID
179
- begin; uuid = generate_uuid; end while @project.objects_hash.has_key?(uuid)
180
- @project.objects_hash[uuid] = @attributes
181
- end
182
- @uuid = uuid
183
- self.isa ||= self.class.isa
184
- end
185
-
186
- def ==(other)
187
- other.is_a?(PBXObject) && self.uuid == other.uuid
188
- end
189
-
190
- def inspect
191
- "#<#{isa} UUID: `#{uuid}', name: `#{name}'>"
192
- end
193
-
194
- private
195
-
196
- def generate_uuid
197
- Xcodeproj.generate_uuid
198
- end
199
-
200
- def list_by_class(uuids, klass, scoped = nil, &block)
201
- unless scoped
202
- scoped = uuids.map { |uuid| @project.objects[uuid] }.select { |o| o.is_a?(klass) }
203
- end
204
- if block
205
- PBXObjectList.new(klass, @project, scoped, &block)
206
- else
207
- PBXObjectList.new(klass, @project, scoped) do |object|
208
- # Add the uuid of a newly created object to the uuids list
209
- uuids << object.uuid
210
- end
211
- end
212
- end
213
- end
214
-
215
- # Missing constants that begin with either `PBX' or `XC' are assumed to be
216
- # valid classes in a Xcode project. A new PBXObject subclass is created
217
- # for the constant and returned.
218
- def self.const_missing(name)
219
- if name.to_s =~ /^(PBX|XC)/
220
- klass = Class.new(PBXObject)
221
- const_set(name, klass)
222
- klass
223
- else
224
- super
225
- end
226
- end
227
-
228
- class PBXFileReference < PBXObject
229
- attributes :path, :sourceTree, :explicitFileType, :lastKnownFileType, :includeInIndex
230
- has_many :buildFiles, :inverse_of => :file
231
- has_one :group, :inverse_of => :children
232
-
233
- def self.new_static_library(project, productName)
234
- new(project, nil, {
235
- "path" => "lib#{productName}.a",
236
- "includeInIndex" => "0", # no idea what this is
237
- "sourceTree" => "BUILT_PRODUCTS_DIR",
238
- })
239
- end
240
-
241
- def initialize(project, uuid, attributes)
242
- is_new = uuid.nil?
243
- super
244
- self.path = path if path # sets default name
245
- self.sourceTree ||= 'SOURCE_ROOT'
246
- if is_new
247
- @project.main_group.children << self
248
- end
249
- set_default_file_type!
250
- end
251
-
252
- alias_method :_path=, :path=
253
- def path=(path)
254
- self._path = path
255
- self.name ||= pathname.basename.to_s
256
- path
257
- end
258
-
259
- def pathname
260
- Pathname.new(path)
261
- end
262
-
263
- def set_default_file_type!
264
- return if explicitFileType || lastKnownFileType
265
- case path
266
- when /\.a$/
267
- self.explicitFileType = 'archive.ar'
268
- when /\.framework$/
269
- self.lastKnownFileType = 'wrapper.framework'
270
- when /\.xcconfig$/
271
- self.lastKnownFileType = 'text.xcconfig'
272
- end
273
- end
274
- end
275
-
276
- class PBXGroup < PBXObject
277
- attributes :sourceTree
278
-
279
- has_many :children, :class => PBXFileReference do |object|
280
- if object.is_a?(Xcodeproj::Project::PBXFileReference)
281
- # Associating the file to this group through the inverse
282
- # association will also remove it from the group it was in.
283
- object.group = self
284
- else
285
- # TODO What objects can actually be in a group and don't they
286
- # all need the above treatment.
287
- childReferences << object.uuid
288
- end
289
- end
290
-
291
- def initialize(*)
292
- super
293
- self.sourceTree ||= '<group>'
294
- self.childReferences ||= []
295
- end
296
-
297
- def files
298
- list_by_class(childReferences, Xcodeproj::Project::PBXFileReference) do |file|
299
- file.group = self
300
- end
301
- end
302
-
303
- def source_files
304
- files = self.files.reject { |file| file.buildFiles.empty? }
305
- list_by_class(childReferences, Xcodeproj::Project::PBXFileReference, files) do |file|
306
- file.group = self
307
- end
308
- end
309
-
310
- def groups
311
- list_by_class(childReferences, Xcodeproj::Project::PBXGroup)
312
- end
313
-
314
- def <<(child)
315
- children << child
316
- end
317
- end
318
-
319
- class PBXBuildFile < PBXObject
320
- attributes :settings
321
- has_one :file, :uuid => :fileRef
322
- end
323
-
324
- class PBXBuildPhase < PBXObject
325
- # TODO rename this to buildFiles and add a files :through => :buildFiles shortcut
326
- has_many :files, :class => PBXBuildFile
327
-
328
- attributes :buildActionMask, :runOnlyForDeploymentPostprocessing
329
-
330
- def initialize(*)
331
- super
332
- self.fileReferences ||= []
333
- # These are always the same, no idea what they are.
334
- self.buildActionMask ||= "2147483647"
335
- self.runOnlyForDeploymentPostprocessing ||= "0"
16
+ module Object
17
+ class PBXProject < AbstractPBXObject
18
+ has_many :targets, :class => PBXNativeTarget
19
+ has_one :products_group, :uuid => :product_ref_group, :class => PBXGroup
20
+ has_one :build_configuration_list, :class => XCConfigurationList
336
21
  end
337
22
  end
338
23
 
339
- class PBXCopyFilesBuildPhase < PBXBuildPhase
340
- attributes :dstPath, :dstSubfolderSpec
341
-
342
- def initialize(*)
343
- super
344
- self.dstSubfolderSpec ||= "16"
345
- end
346
- end
347
-
348
- class PBXSourcesBuildPhase < PBXBuildPhase; end
349
- class PBXFrameworksBuildPhase < PBXBuildPhase; end
350
- class PBXShellScriptBuildPhase < PBXBuildPhase
351
- attribute :shellScript
352
- end
353
-
354
- class PBXNativeTarget < PBXObject
355
- STATIC_LIBRARY = 'com.apple.product-type.library.static'
356
-
357
- attributes :productName, :productType
358
-
359
- has_many :buildPhases
360
- has_many :dependencies # TODO :class => ?
361
- has_many :buildRules # TODO :class => ?
362
- has_one :buildConfigurationList
363
- has_one :product, :uuid => :productReference
364
-
365
- def self.new_static_library(project, productName)
366
- # TODO should probably switch the uuid and attributes argument
367
- target = new(project, nil, 'productType' => STATIC_LIBRARY, 'productName' => productName)
368
- target.product = project.files.new_static_library(productName)
369
- products = project.groups.find { |g| g.name == 'Products' }
370
- products ||= project.groups.new({ 'name' => 'Products'})
371
- products.children << target.product
372
- target.buildPhases.add(PBXSourcesBuildPhase)
373
-
374
- buildPhase = target.buildPhases.add(PBXFrameworksBuildPhase)
375
- frameworks = project.groups.find { |g| g.name == 'Frameworks' }
376
- frameworks ||= project.groups.new({ 'name' => 'Frameworks'})
377
- frameworks.files.each do |framework|
378
- buildPhase.files << framework.buildFiles.new
379
- end
380
-
381
- target.buildPhases.add(PBXCopyFilesBuildPhase, 'dstPath' => '$(PRODUCT_NAME)')
382
- target
383
- end
384
-
385
- # You need to specify a product. For a static library you can use
386
- # PBXFileReference.new_static_library.
387
- def initialize(project, *)
388
- super
389
- self.name ||= productName
390
- self.buildRuleReferences ||= []
391
- self.dependencyReferences ||= []
392
- self.buildPhaseReferences ||= []
393
-
394
- unless buildConfigurationList
395
- self.buildConfigurationList = project.objects.add(XCConfigurationList)
396
- # TODO or should this happen in buildConfigurationList?
397
- buildConfigurationList.buildConfigurations.new('name' => 'Debug')
398
- buildConfigurationList.buildConfigurations.new('name' => 'Release')
399
- end
400
- end
401
-
402
- alias_method :_product=, :product=
403
- def product=(product)
404
- self._product = product
405
- product.group = @project.products
406
- end
407
-
408
- def buildConfigurations
409
- buildConfigurationList.buildConfigurations
410
- end
411
-
412
- def source_build_phases
413
- buildPhases.select_by_class(PBXSourcesBuildPhase)
414
- end
415
-
416
- def copy_files_build_phases
417
- buildPhases.select_by_class(PBXCopyFilesBuildPhase)
418
- end
419
-
420
- def frameworks_build_phases
421
- buildPhases.select_by_class(PBXFrameworksBuildPhase)
422
- end
423
-
424
- # Finds an existing file reference or creates a new one.
425
- def add_source_file(path, copy_header_phase = nil, compiler_flags = nil)
426
- file = @project.files.find { |file| file.path == path.to_s } || @project.files.new('path' => path.to_s)
427
- buildFile = file.buildFiles.new
428
- if path.extname == '.h'
429
- buildFile.settings = { 'ATTRIBUTES' => ["Public"] }
430
- # Working around a bug in Xcode 4.2 betas, remove this once the Xcode bug is fixed:
431
- # https://github.com/alloy/cocoapods/issues/13
432
- #phase = copy_header_phase || headers_build_phases.first
433
- phase = copy_header_phase || copy_files_build_phases.first
434
- phase.files << buildFile
435
- else
436
- buildFile.settings = { 'COMPILER_FLAGS' => compiler_flags } if compiler_flags
437
- source_build_phases.first.files << buildFile
438
- end
439
- file
440
- end
441
- end
442
-
443
- class XCBuildConfiguration < PBXObject
444
- attribute :buildSettings
445
- has_one :baseConfiguration, :uuid => :baseConfigurationReference
446
-
447
- def initialize(*)
448
- super
449
- # TODO These are from an iOS static library, need to check if it works for any product type
450
- self.buildSettings = {
451
- 'DSTROOT' => '/tmp/xcodeproj.dst',
452
- 'GCC_PRECOMPILE_PREFIX_HEADER' => 'YES',
453
- 'GCC_VERSION' => 'com.apple.compilers.llvm.clang.1_0',
454
- 'PRODUCT_NAME' => '$(TARGET_NAME)',
455
- 'SKIP_INSTALL' => 'YES',
456
- }.merge(buildSettings || {})
457
- end
458
- end
459
-
460
- class XCConfigurationList < PBXObject
461
- has_many :buildConfigurations
462
-
463
- def initialize(*)
464
- super
465
- self.buildConfigurationReferences ||= []
466
- end
467
- end
468
-
469
- class PBXProject < PBXObject
470
- has_many :targets, :class => PBXNativeTarget
471
- has_one :products, :singular_name => :products, :uuid => :productRefGroup, :class => PBXGroup
472
- end
473
-
474
- class PBXObjectList
475
- include Enumerable
476
-
477
- def initialize(represented_class, project, scoped, &new_object_callback)
478
- @represented_class = represented_class
479
- @project = project
480
- @scoped_hash = scoped.is_a?(Array) ? scoped.inject({}) { |h, o| h[o.uuid] = o.attributes; h } : scoped
481
- @callback = new_object_callback
482
- end
483
-
484
- def empty?
485
- @scoped_hash.empty?
486
- end
487
-
488
- def [](uuid)
489
- if hash = @scoped_hash[uuid]
490
- Project.const_get(hash['isa']).new(@project, uuid, hash)
491
- end
492
- end
493
-
494
- def add(klass, hash = {})
495
- object = klass.new(@project, nil, hash)
496
- @callback.call(object) if @callback
497
- object
498
- end
499
-
500
- def new(hash = {})
501
- add(@represented_class, hash)
502
- end
503
-
504
- def <<(object)
505
- @callback.call(object) if @callback
506
- end
507
-
508
- def each
509
- @scoped_hash.keys.each do |uuid|
510
- yield self[uuid]
511
- end
512
- end
513
-
514
- def ==(other)
515
- self.to_a == other.to_a
516
- end
517
-
518
- def first
519
- to_a.first
520
- end
521
-
522
- def last
523
- to_a.last
524
- end
525
-
526
- def inspect
527
- "<PBXObjectList: #{map(&:inspect)}>"
528
- end
529
-
530
- # Only makes sense on lists that contain mixed classes.
531
- def select_by_class(klass)
532
- scoped = Hash[*@scoped_hash.select { |_, attr| attr['isa'] == klass.isa }.flatten]
533
- PBXObjectList.new(klass, @project, scoped) do |object|
534
- # Objects added to the subselection should still use the same
535
- # callback as this list.
536
- self << object
537
- end
538
- end
539
-
540
- def method_missing(name, *args, &block)
541
- if @represented_class.respond_to?(name)
542
- object = @represented_class.send(name, @project, *args)
543
- # The callbacks are only for PBXObject instances instantiated
544
- # from the class method that we forwarded the message to.
545
- @callback.call(object) if object.is_a?(PBXObject)
546
- object
547
- else
548
- super
549
- end
550
- end
551
- end
24
+ include Object
552
25
 
26
+ # Opens a Xcode project document if a path to one is given, otherwise a new
27
+ # Project is created.
28
+ #
29
+ # @param [Pathname, String] xcodeproj The path to the Xcode project
30
+ # document (xcodeproj).
31
+ #
32
+ # @return [Project] A new Project instance or one with
33
+ # the data of an existing Xcode
34
+ # document.
553
35
  def initialize(xcodeproj = nil)
554
36
  if xcodeproj
555
37
  file = File.join(xcodeproj, 'project.pbxproj')
@@ -561,86 +43,180 @@ module Xcodeproj
561
43
  'objectVersion' => '46',
562
44
  'objects' => {}
563
45
  }
564
- self.root_object = objects.add(Xcodeproj::Project::PBXProject, {
46
+ main_group = groups.new
47
+ self.root_object = objects.add(PBXProject, {
565
48
  'attributes' => { 'LastUpgradeCheck' => '0420' },
566
49
  'compatibilityVersion' => 'Xcode 3.2',
567
50
  'developmentRegion' => 'English',
568
51
  'hasScannedForEncodings' => '0',
569
52
  'knownRegions' => ['en'],
570
- 'mainGroup' => groups.new.uuid,
53
+ 'mainGroup' => main_group.uuid,
54
+ 'productRefGroup' => main_group.groups.new('name' => 'Products').uuid,
571
55
  'projectDirPath' => '',
572
56
  'projectRoot' => '',
573
57
  'targets' => []
574
58
  })
59
+
60
+ config_list = objects.add(XCConfigurationList)
61
+ config_list.default_configuration_name = 'Release'
62
+ config_list.default_configuration_is_visible = '0'
63
+ config_list.build_configurations.new('name' => 'Debug')
64
+ config_list.build_configurations.new('name' => 'Release')
65
+ self.root_object.build_configuration_list = config_list
66
+
67
+ # TODO make this work
68
+ #self.root_object.product_reference = groups.new('name' => 'Products').uuid
575
69
  end
576
70
  end
577
71
 
72
+ # @return [Hash] The internal data.
578
73
  def to_hash
579
74
  @plist
580
75
  end
581
76
 
77
+ def ==(other)
78
+ other.respond_to?(:to_hash) && @plist == other.to_hash
79
+ end
80
+
81
+ # This gives access to the objects part of the internal data hash. It is,
82
+ # however, **not** recommended to use this to add a hash for an object, for
83
+ # that see `add_object_hash`.
84
+ #
85
+ # @return [Hash] The `objects` part of the internal data.
582
86
  def objects_hash
583
87
  @plist['objects']
584
88
  end
585
89
 
586
- def objects
587
- @objects ||= PBXObjectList.new(PBXObject, self, objects_hash)
90
+ # This is the preferred way to add an object attributes hash to the objects
91
+ # hash, as it validates the data before inserting it.
92
+ #
93
+ # @param [String] uuid The UUID of the object.
94
+ # @param [Hash] attributes The attributes of the object.
95
+ #
96
+ # @raise [ArgumentError] Raised if the value of the `isa` key is equal
97
+ # to `AbstractPBXObject`.
98
+ #
99
+ # @todo Ideally we would do more validation here, but I don't think we know
100
+ # of all classes that can exist yet.
101
+ def add_object_hash(uuid, attributes)
102
+ if attributes['isa'] !~ /^(PBX|XC)/
103
+ raise ArgumentError, "Attempted to insert a `#{attributes['isa']}' instance into the objects hash, which is not allowed."
104
+ end
105
+ objects_hash[uuid] = attributes
588
106
  end
589
107
 
108
+ # @return [PBXProject] The root object of the project.
590
109
  def root_object
591
110
  objects[@plist['rootObject']]
592
111
  end
593
112
 
113
+ # @param [PBXProject] object The object to assign as the root object.
594
114
  def root_object=(object)
595
115
  @plist['rootObject'] = object.uuid
596
116
  end
597
117
 
118
+ # @return [PBXObjectList<AbstractPBXObject>] A list of all the objects in the
119
+ # project.
120
+ def objects
121
+ PBXObjectList.new(AbstractPBXObject, self) do |list|
122
+ list.let(:uuid_scope) { objects_hash.keys }
123
+ end
124
+ end
125
+
126
+ # @return [PBXObjectList<PBXGroup>] A list of all the groups in the
127
+ # project.
598
128
  def groups
599
- objects.select_by_class(PBXGroup)
129
+ objects.list_by_class(PBXGroup)
600
130
  end
601
-
131
+
132
+ # Tries to find a group with the given name.
133
+ #
134
+ # @param [String] name The name of the group to find.
135
+ # @return [PBXGroup, nil] The PBXgroup, if found.
136
+ def group(name)
137
+ groups.object_named(name)
138
+ end
139
+
140
+ # @return [PBXGroup] The main top-level group.
602
141
  def main_group
603
142
  objects[root_object.attributes['mainGroup']]
604
143
  end
605
144
 
145
+ # @return [PBXObjectList<PBXFileReference>] A list of all the files in the
146
+ # project.
606
147
  def files
607
- objects.select_by_class(PBXFileReference)
608
- end
609
-
148
+ objects.list_by_class(PBXFileReference)
149
+ end
150
+
151
+ # Adds a file reference for a system framework to the project.
152
+ #
153
+ # The file reference can then be added to the buildFiles of a
154
+ # PBXFrameworksBuildPhase.
155
+ #
156
+ # @example
157
+ #
158
+ # framework = project.add_system_framework('QuartzCore')
159
+ #
160
+ # target = project.targets.first
161
+ # build_phase = target.frameworks_build_phases.first
162
+ # build_phase.files << framework.buildFiles.new
163
+ #
164
+ # @todo Make it possible to do: `build_phase << framework`
165
+ #
166
+ # @param [String] name The name of a framework in the SDK System
167
+ # directory.
168
+ # @return [PBXFileReference] The file reference object.
610
169
  def add_system_framework(name)
611
- files.new({
170
+ group = groups.where('name' => 'Frameworks') || groups.new('name' => 'Frameworks')
171
+ group.files.new({
612
172
  'name' => "#{name}.framework",
613
173
  'path' => "System/Library/Frameworks/#{name}.framework",
614
174
  'sourceTree' => 'SDKROOT'
615
175
  })
616
176
  end
617
-
618
- def add_shell_script_build_phase(name, script_path)
619
- objects.add(Xcodeproj::Project::PBXShellScriptBuildPhase, {
620
- 'name' => name,
621
- 'files' => [],
622
- 'inputPaths' => [],
623
- 'outputPaths' => [],
624
- 'shellPath' => '/bin/sh',
625
- 'shellScript' => script_path
626
- })
177
+
178
+ # @return [PBXObjectList<XCBuildConfiguration] A list of project wide
179
+ # build configurations.
180
+ def build_configurations
181
+ root_object.build_configuration_list.build_configurations
627
182
  end
628
183
 
629
- def build_files
630
- objects.select_by_class(PBXBuildFile)
184
+ # @param [String] name The name of a project wide build configuration.
185
+ #
186
+ # @return [Hash] The build settings of the project wide build
187
+ # configuration with the given name.
188
+ def build_settings(name)
189
+ root_object.build_configuration_list.build_settings(name)
631
190
  end
632
191
 
192
+ # @todo There are probably other target types too. E.g. an aggregate.
193
+ #
194
+ # @return [PBXObjectList<PBXNativeTarget>] A list of all the targets in
195
+ # the project.
633
196
  def targets
634
197
  # Better to check the project object for targets to ensure they are
635
198
  # actually there so the project will work
636
199
  root_object.targets
637
200
  end
638
201
 
202
+ # @return [PBXGroup] The group which holds the product file references.
203
+ def products_group
204
+ root_object.products_group
205
+ end
206
+
207
+ # @return [PBXObjectList<PBXFileReference>] A list of the product file
208
+ # references.
639
209
  def products
640
- root_object.products
210
+ products_group.children
641
211
  end
642
212
 
213
+ # @private
643
214
  IGNORE_GROUPS = ['Frameworks', 'Products', 'Supporting Files']
215
+
216
+ # @todo I think this is here because of easier testing in CocoaPods. Move
217
+ # this extension to the CocoaPods specs.
218
+ #
219
+ # @return [Hash] A list of all the groups and their source files.
644
220
  def source_files
645
221
  source_files = {}
646
222
  groups.each do |group|
@@ -650,6 +226,18 @@ module Xcodeproj
650
226
  source_files
651
227
  end
652
228
 
229
+ # Serializes the internal data as a property list and stores it on disk at
230
+ # the given path.
231
+ #
232
+ # @example
233
+ #
234
+ # project.save_as("path/to/Project.xcodeproj") # => true
235
+ #
236
+ # @param [String, Pathname] projpath The path where the data should be
237
+ # stored.
238
+ #
239
+ # @return [true, false] Returns whether or not saving was
240
+ # successful.
653
241
  def save_as(projpath)
654
242
  projpath = projpath.to_s
655
243
  FileUtils.mkdir_p(projpath)