xcodeproj 0.1.0 → 0.2.0.rc1

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,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)