xcodeproj 0.28.2 → 1.0.0

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/xcodeproj/command/config_dump.rb +2 -1
  3. data/lib/xcodeproj/config/other_linker_flags_parser.rb +5 -2
  4. data/lib/xcodeproj/config.rb +14 -5
  5. data/lib/xcodeproj/constants.rb +62 -27
  6. data/lib/xcodeproj/differ.rb +7 -7
  7. data/lib/xcodeproj/gem_version.rb +1 -1
  8. data/lib/xcodeproj/plist/ffi/core_foundation.rb +441 -0
  9. data/lib/xcodeproj/plist/ffi/dev_tools_core.rb +188 -0
  10. data/lib/xcodeproj/plist/ffi.rb +119 -0
  11. data/lib/xcodeproj/plist/plist_gem.rb +27 -0
  12. data/lib/xcodeproj/plist.rb +89 -0
  13. data/lib/xcodeproj/project/object/build_configuration.rb +15 -0
  14. data/lib/xcodeproj/project/object/build_rule.rb +28 -0
  15. data/lib/xcodeproj/project/object/group.rb +28 -0
  16. data/lib/xcodeproj/project/object/helpers/file_references_factory.rb +1 -1
  17. data/lib/xcodeproj/project/object/native_target.rb +26 -7
  18. data/lib/xcodeproj/project/object.rb +16 -3
  19. data/lib/xcodeproj/project/object_attributes.rb +10 -0
  20. data/lib/xcodeproj/project/object_dictionary.rb +2 -0
  21. data/lib/xcodeproj/project/object_list.rb +16 -4
  22. data/lib/xcodeproj/project/project_helper.rb +1 -1
  23. data/lib/xcodeproj/project/uuid_generator.rb +1 -0
  24. data/lib/xcodeproj/project.rb +36 -12
  25. data/lib/xcodeproj/scheme/build_action.rb +5 -2
  26. data/lib/xcodeproj/scheme/environment_variables.rb +170 -0
  27. data/lib/xcodeproj/scheme/launch_action.rb +16 -0
  28. data/lib/xcodeproj/scheme/test_action.rb +18 -0
  29. data/lib/xcodeproj/scheme.rb +5 -5
  30. data/lib/xcodeproj/workspace/file_reference.rb +2 -3
  31. data/lib/xcodeproj/workspace/group_reference.rb +64 -0
  32. data/lib/xcodeproj/workspace.rb +127 -34
  33. data/lib/xcodeproj.rb +1 -1
  34. metadata +29 -17
  35. data/lib/xcodeproj/plist_helper.rb +0 -758
@@ -0,0 +1,119 @@
1
+ module Xcodeproj
2
+ module Plist
3
+ # Provides support for loading and serializing property list files via
4
+ # Fiddle and CoreFoundation / Xcode.
5
+ #
6
+ module FFI
7
+ autoload :CoreFoundation, 'xcodeproj/plist/ffi/core_foundation'
8
+ autoload :DevToolsCore, 'xcodeproj/plist/ffi/dev_tools_core'
9
+
10
+ class << self
11
+ # Attempts to load the `fiddle` and Xcode based plist serializer.
12
+ #
13
+ # @return [String,Nil] The loading error message, or `nil` if loading
14
+ # was successful.
15
+ #
16
+ def attempt_to_load!
17
+ return @attempt_to_load if defined?(@attempt_to_load)
18
+ @attempt_to_load = begin
19
+ require 'fiddle'
20
+ nil
21
+ rescue LoadError
22
+ 'Xcodeproj relies on a library called `fiddle` to read and write ' \
23
+ 'Xcode project files. Ensure your Ruby installation includes ' \
24
+ '`fiddle` and try again.'
25
+ end
26
+ end
27
+
28
+ # Serializes a hash as an XML property list file.
29
+ #
30
+ # @param [#to_hash] hash
31
+ # The hash to store.
32
+ #
33
+ # @param [#to_s] path
34
+ # The path of the file.
35
+ #
36
+ def write_to_path(hash, path)
37
+ raise ThreadError, 'Can only write plists from the main thread.' unless Thread.current == Thread.main
38
+
39
+ if DevToolsCore.load_xcode_frameworks && path.end_with?('pbxproj')
40
+ ruby_hash_write_xcode(hash, path)
41
+ else
42
+ CoreFoundation.RubyHashPropertyListWrite(hash, path)
43
+ fix_encoding(path)
44
+ end
45
+ end
46
+
47
+ # @return [Hash] Returns the native objects loaded from a property list
48
+ # file.
49
+ #
50
+ # @param [#to_s] path
51
+ # The path of the file.
52
+ #
53
+ def read_from_path(path)
54
+ CoreFoundation.RubyHashPropertyListRead(path)
55
+ end
56
+
57
+ private
58
+
59
+ # Simple workaround to escape characters which are outside of ASCII
60
+ # character-encoding. Relies on the fact that there are no XML characters
61
+ # which would need to be escaped.
62
+ #
63
+ # @note This is necessary because Xcode (4.6 currently) uses the MacRoman
64
+ # encoding unless the `// !$*UTF8*$!` magic comment is present. It
65
+ # is not possible to serialize a plist using the NeXTSTEP format
66
+ # without access to the private classes of Xcode and that comment
67
+ # is not compatible with the XML format. For the complete
68
+ # discussion see CocoaPods/CocoaPods#926.
69
+ #
70
+ #
71
+ # @note Sadly this hack is not sufficient for supporting Emoji.
72
+ #
73
+ # @param [String, Pathname] The path of the file which needs to be fixed.
74
+ #
75
+ # @return [void]
76
+ #
77
+ def fix_encoding(filename)
78
+ output = ''
79
+ input = File.open(filename, 'rb', &:read)
80
+ input.unpack('U*').each do |codepoint|
81
+ if codepoint > 127 # ASCII is 7-bit, so 0-127 are valid characters
82
+ output << "&##{codepoint};"
83
+ else
84
+ output << codepoint.chr
85
+ end
86
+ end
87
+ File.open(filename, 'wb') { |file| file.write(output) }
88
+ end
89
+
90
+ # Serializes a hash as an ASCII plist, using Xcode.
91
+ #
92
+ # @param [Hash] hash
93
+ # The hash to store.
94
+ #
95
+ # @param [String] path
96
+ # The path of the file.
97
+ #
98
+ def ruby_hash_write_xcode(hash, path)
99
+ path = File.expand_path(path)
100
+ success = true
101
+
102
+ begin
103
+ plist = DevToolsCore::CFDictionary.new(CoreFoundation.RubyHashToCFDictionary(hash))
104
+ data = DevToolsCore::NSData.new(plist.plistDescriptionUTF8Data)
105
+ success &= data.writeToFileAtomically(path)
106
+
107
+ project = DevToolsCore::PBXProject.new(path)
108
+ success &= project.writeToFileSystemProjectFile
109
+ project.close
110
+ rescue Fiddle::DLError
111
+ success = false
112
+ end
113
+
114
+ CoreFoundation.RubyHashPropertyListWrite(hash, path) unless success
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,27 @@
1
+ module Xcodeproj
2
+ module Plist
3
+ # @visibility private
4
+ module PlistGem
5
+ def self.attempt_to_load!
6
+ return @attempt_to_load if defined?(@attempt_to_load)
7
+ @attempt_to_load = begin
8
+ require 'plist/parser'
9
+ require 'plist/generator'
10
+ nil
11
+ rescue LoadError
12
+ 'Xcodeproj relies on a library called `plist` to read and write ' \
13
+ 'Xcode project files. Ensure you have the `plist` gem installed ' \
14
+ 'and try again.'
15
+ end
16
+ end
17
+
18
+ def self.write_to_path(hash, path)
19
+ ::Plist::Emit.save_plist(hash, path)
20
+ end
21
+
22
+ def self.read_from_path(path)
23
+ ::Plist.parse_xml(path)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,89 @@
1
+ module Xcodeproj
2
+ # Provides support for loading and serializing property list files.
3
+ #
4
+ module Plist
5
+ autoload :FFI, 'xcodeproj/plist/ffi'
6
+ autoload :PlistGem, 'xcodeproj/plist/plist_gem'
7
+
8
+ # @return [Hash] Returns the native objects loaded from a property list
9
+ # file.
10
+ #
11
+ # @param [#to_s] path
12
+ # The path of the file.
13
+ #
14
+ def self.read_from_path(path)
15
+ path = path.to_s
16
+ unless File.exist?(path)
17
+ raise Informative, "The plist file at path `#{path}` doesn't exist."
18
+ end
19
+ if file_in_conflict?(path)
20
+ raise Informative, "The file `#{path}` is in a merge conflict."
21
+ end
22
+ implementation.read_from_path(path)
23
+ end
24
+
25
+ # Serializes a hash as an XML property list file.
26
+ #
27
+ # @param [#to_hash] hash
28
+ # The hash to store.
29
+ #
30
+ # @param [#to_s] path
31
+ # The path of the file.
32
+ #
33
+ def self.write_to_path(hash, path)
34
+ if hash.respond_to?(:to_hash)
35
+ hash = hash.to_hash
36
+ else
37
+ raise TypeError, "The given `#{hash.inspect}` must respond " \
38
+ "to #to_hash'."
39
+ end
40
+
41
+ unless path.is_a?(String) || path.is_a?(Pathname)
42
+ raise TypeError, "The given `#{path}` must be a string or 'pathname'."
43
+ end
44
+ path = path.to_s
45
+ raise IOError, 'Empty path.' if path.empty?
46
+ implementation.write_to_path(hash, path)
47
+ end
48
+
49
+ # The known modules that can serialize plists.
50
+ #
51
+ KNOWN_IMPLEMENTATIONS = [:FFI, :PlistGem]
52
+
53
+ class << self
54
+ # @return The module used to implement plist serialization.
55
+ #
56
+ attr_accessor :implementation
57
+ def implementation
58
+ @implementation ||= autoload_implementation
59
+ end
60
+ end
61
+
62
+ # Attempts to autoload a known plist implementation.
63
+ #
64
+ # @return a successfully loaded plist serialization implementation.
65
+ #
66
+ def self.autoload_implementation
67
+ failures = KNOWN_IMPLEMENTATIONS.map do |impl|
68
+ begin
69
+ impl = Plist.const_get(impl)
70
+ failure = impl.attempt_to_load!
71
+ return impl if failure.nil?
72
+ failure
73
+ rescue NameError, LoadError => e
74
+ e.message
75
+ end
76
+ end.compact
77
+ raise Informative, "Unable to load a plist implementation:\n\n#{failures.join("\n\n")}"
78
+ end
79
+
80
+ # @return [Bool] Checks whether there are merge conflicts in the file.
81
+ #
82
+ # @param [#to_s] path
83
+ # The path of the file.
84
+ #
85
+ def self.file_in_conflict?(path)
86
+ File.read(path).match(/^(<|=|>){7}/)
87
+ end
88
+ end
89
+ end
@@ -47,6 +47,21 @@ module Xcodeproj
47
47
  self.build_settings = sorted_build_settings
48
48
  end
49
49
 
50
+ # @return [Boolean] Whether this configuration is configured for
51
+ # debugging.
52
+ #
53
+ def debug?
54
+ gcc_preprocessor_definitions = build_settings['GCC_PREPROCESSOR_DEFINITIONS']
55
+ gcc_preprocessor_definitions && gcc_preprocessor_definitions.include?('DEBUG=1')
56
+ end
57
+
58
+ # @return [Symbol] The symbolic type of this configuration, either
59
+ # `:debug` or `:release`.
60
+ #
61
+ def type
62
+ debug? ? :debug : :release
63
+ end
64
+
50
65
  #---------------------------------------------------------------------#
51
66
 
52
67
  private
@@ -46,12 +46,40 @@ module Xcodeproj
46
46
  #
47
47
  attribute :output_files, Array
48
48
 
49
+ # @return [ObjectList<String>] the compiler flags used when creating the
50
+ # respective output files.
51
+ #
52
+ attribute :output_files_compiler_flags, Array
53
+
49
54
  # @return [String] the content of the script to use for the build rule.
50
55
  #
51
56
  # @note This attribute is present if the #{#compiler_spec} is
52
57
  # `com.apple.compilers.proxy.script`
53
58
  #
54
59
  attribute :script, String
60
+
61
+ # @!group Helpers
62
+
63
+ # Adds an output file with the specified compiler flags.
64
+ #
65
+ # @param [PBXFileReference] file the file to add.
66
+ #
67
+ # @param [String] compiler_flags the compiler flags for the file.
68
+ #
69
+ # @return [Void]
70
+ #
71
+ def add_output_file(file, compiler_flags = '')
72
+ (self.output_files ||= []) << file
73
+ (self.output_files_compiler_flags ||= []) << compiler_flags
74
+ end
75
+
76
+ # @return [Array<[PBXFileReference, String]>]
77
+ # An array containing tuples of output files and their compiler
78
+ # flags.
79
+ #
80
+ def output_files_and_flags
81
+ (output_files || []).zip(output_files_compiler_flags || [])
82
+ end
55
83
  end
56
84
  end
57
85
  end
@@ -264,6 +264,34 @@ module Xcodeproj
264
264
  group
265
265
  end
266
266
 
267
+ # Creates a new variant group and adds it to the group
268
+ #
269
+ # @note @see new_group
270
+ #
271
+ # @param [#to_s] name
272
+ # the name of the new group.
273
+ #
274
+ # @param [#to_s] path
275
+ # The, preferably absolute, path of the variant group.
276
+ # Pass the path of the folder containing all the .lproj bundles,
277
+ # that contain files for the variant group.
278
+ # Do not pass the path of a specific bundle (such as en.lproj)
279
+ #
280
+ # @param [Symbol] source_tree
281
+ # The source tree key to use to configure the path (@see
282
+ # GroupableHelper::SOURCE_TREES_BY_KEY).
283
+ #
284
+ # @return [PBXVariantGroup] the new variant group.
285
+ #
286
+ def new_variant_group(name, path = nil, source_tree = :group)
287
+ group = project.new(PBXVariantGroup)
288
+ children << group
289
+ group.name = name
290
+ group.set_source_tree(source_tree)
291
+ group.set_path(path)
292
+ group
293
+ end
294
+
267
295
  # Traverses the children groups and finds the group with the given
268
296
  # path, if exists.
269
297
  #
@@ -139,7 +139,7 @@ module Xcodeproj
139
139
  new_file_reference(ref, child_path, :group)
140
140
  elsif File.basename(child_path) == '.xccurrentversion'
141
141
  full_path = path + File.basename(child_path)
142
- xccurrentversion = Xcodeproj.read_plist(full_path)
142
+ xccurrentversion = Plist.read_from_path(full_path)
143
143
  current_version_name = xccurrentversion['_XCCurrentVersionName']
144
144
  end
145
145
  end
@@ -108,15 +108,34 @@ module Xcodeproj
108
108
  sdk.scan(/[0-9.]+/).first
109
109
  end
110
110
 
111
+ # @visibility private
112
+ #
113
+ # @return [Hash<Symbol, String>]
114
+ # The name of the setting for the deployment target by platform
115
+ # name.
116
+ #
117
+ DEPLOYMENT_TARGET_SETTING_BY_PLATFORM_NAME = {
118
+ :ios => 'IPHONEOS_DEPLOYMENT_TARGET',
119
+ :osx => 'MACOSX_DEPLOYMENT_TARGET',
120
+ :tvos => 'TVOS_DEPLOYMENT_TARGET',
121
+ :watchos => 'WATCHOS_DEPLOYMENT_TARGET',
122
+ }.freeze
123
+
111
124
  # @return [String] the deployment target of the target according to its
112
125
  # platform.
113
126
  #
114
127
  def deployment_target
115
- case platform_name
116
- when :ios then common_resolved_build_setting('IPHONEOS_DEPLOYMENT_TARGET')
117
- when :osx then common_resolved_build_setting('MACOSX_DEPLOYMENT_TARGET')
118
- when :tvos then common_resolved_build_setting('TVOS_DEPLOYMENT_TARGET')
119
- when :watchos then common_resolved_build_setting('WATCHOS_DEPLOYMENT_TARGET')
128
+ return unless setting = DEPLOYMENT_TARGET_SETTING_BY_PLATFORM_NAME[platform_name]
129
+ common_resolved_build_setting(setting)
130
+ end
131
+
132
+ # @param [String] deployment_target the deployment target to set for
133
+ # the target according to its platform.
134
+ #
135
+ def deployment_target=(deployment_target)
136
+ return unless setting = DEPLOYMENT_TARGET_SETTING_BY_PLATFORM_NAME[platform_name]
137
+ build_configurations.each do |config|
138
+ config.build_settings[setting] = deployment_target
120
139
  end
121
140
  end
122
141
 
@@ -515,8 +534,8 @@ module Xcodeproj
515
534
  unless phase_class < AbstractBuildPhase
516
535
  raise ArgumentError, "#{phase_class} must be a subclass of #{AbstractBuildPhase.class}"
517
536
  end
518
- @phases[phase_class] ||= build_phases.find { |bp| bp.class == phase_class } \
519
- || project.new(phase_class).tap { |bp| build_phases << bp }
537
+ @phases[phase_class] ||= build_phases.find { |bp| bp.class == phase_class } ||
538
+ project.new(phase_class).tap { |bp| build_phases << bp }
520
539
  end
521
540
 
522
541
  public
@@ -59,11 +59,12 @@ module Xcodeproj
59
59
  # @visibility private
60
60
  #
61
61
  def initialize(project, uuid)
62
- @project, @uuid = project, uuid
62
+ @project = project
63
+ @uuid = uuid
63
64
  @isa = self.class.isa
64
65
  @referrers = []
65
- unless @isa.match(/^(PBX|XC)/)
66
- raise '[Xcodeproj] Attempt to initialize an abstract class.'
66
+ unless @isa =~ /^(PBX|XC)/
67
+ raise "[Xcodeproj] Attempt to initialize an abstract class (#{self.class})."
67
68
  end
68
69
  end
69
70
 
@@ -98,6 +99,7 @@ module Xcodeproj
98
99
  # @return [void]
99
100
  #
100
101
  def remove_from_project
102
+ mark_project_as_dirty!
101
103
  project.objects_by_uuid.delete(uuid)
102
104
 
103
105
  referrers.dup.each do |referrer|
@@ -214,6 +216,7 @@ module Xcodeproj
214
216
  def remove_referrer(referrer)
215
217
  @referrers.delete(referrer)
216
218
  if @referrers.count == 0
219
+ mark_project_as_dirty!
217
220
  @project.objects_by_uuid.delete(uuid)
218
221
  end
219
222
  end
@@ -241,6 +244,16 @@ module Xcodeproj
241
244
  end
242
245
  end
243
246
 
247
+ # Marks the project that this object belongs to as having been modified.
248
+ #
249
+ # @return [void]
250
+ #
251
+ # @visibility private
252
+ #
253
+ def mark_project_as_dirty!
254
+ project.mark_dirty!
255
+ end
256
+
244
257
  #---------------------------------------------------------------------#
245
258
 
246
259
  public
@@ -316,6 +316,14 @@ module Xcodeproj
316
316
  define_method("#{attrb.name}=") do |value|
317
317
  @simple_attributes_hash ||= {}
318
318
  attrb.validate_value(value)
319
+
320
+ existing = @simple_attributes_hash[attrb.plist_name]
321
+ if existing.is_a?(Hash) && value.is_a?(Hash)
322
+ return value if existing.keys == value.keys && existing == value
323
+ elsif existing == value
324
+ return value if existing == value
325
+ end
326
+ mark_project_as_dirty!
319
327
  @simple_attributes_hash[attrb.plist_name] = value
320
328
  end
321
329
  end
@@ -352,6 +360,8 @@ module Xcodeproj
352
360
  attrb.validate_value(value)
353
361
 
354
362
  previous_value = send(attrb.name)
363
+ return value if previous_value == value
364
+ mark_project_as_dirty!
355
365
  previous_value.remove_referrer(self) if previous_value
356
366
  instance_variable_set("@#{attrb.name}", value)
357
367
  value.add_referrer(self) if value
@@ -187,6 +187,7 @@ module Xcodeproj
187
187
  # @return [void]
188
188
  #
189
189
  def perform_additions_operations(object, key)
190
+ owner.mark_project_as_dirty!
190
191
  object.add_referrer(owner)
191
192
  attribute.validate_value_for_key(object, key)
192
193
  end
@@ -197,6 +198,7 @@ module Xcodeproj
197
198
  # @return [void]
198
199
  #
199
200
  def perform_deletion_operations(objects)
201
+ owner.mark_project_as_dirty!
200
202
  objects.remove_referrer(owner)
201
203
  end
202
204
  end
@@ -140,10 +140,10 @@ module Xcodeproj
140
140
  super
141
141
  end
142
142
 
143
- # Moves the object at the given given index to the given position.
143
+ # Moves the given object to the given index.
144
144
  #
145
- # @param [Fixnum] from
146
- # The current index of the object.
145
+ # @param [AbstractObject, ObjectDictionary] object
146
+ # The object to move.
147
147
  #
148
148
  # @param [Fixnum] to
149
149
  # The new index for the object.
@@ -151,6 +151,7 @@ module Xcodeproj
151
151
  # @return [void]
152
152
  #
153
153
  def move(object, new_index)
154
+ return if index(object) == new_index
154
155
  if obj = delete(object)
155
156
  insert(new_index, obj)
156
157
  else
@@ -158,7 +159,7 @@ module Xcodeproj
158
159
  end
159
160
  end
160
161
 
161
- # Moves the object at the given given index to the given position.
162
+ # Moves the object at the given index to the given position.
162
163
  #
163
164
  # @param [Fixnum] from
164
165
  # The current index of the object.
@@ -169,6 +170,7 @@ module Xcodeproj
169
170
  # @return [void]
170
171
  #
171
172
  def move_from(current_index, new_index)
173
+ return if current_index == new_index
172
174
  if obj = delete_at(current_index)
173
175
  insert(new_index, obj)
174
176
  else
@@ -176,6 +178,14 @@ module Xcodeproj
176
178
  end
177
179
  end
178
180
 
181
+ def sort!
182
+ return super if owner.project.dirty?
183
+ previous = to_a
184
+ super
185
+ owner.mark_project_as_dirty! unless previous == to_a
186
+ self
187
+ end
188
+
179
189
  private
180
190
 
181
191
  # @!group Notification Methods
@@ -190,6 +200,7 @@ module Xcodeproj
190
200
  def perform_additions_operations(objects)
191
201
  objects = [objects] unless objects.is_a?(Array)
192
202
  objects.each do |obj|
203
+ owner.mark_project_as_dirty!
193
204
  obj.add_referrer(owner)
194
205
  attribute.validate_value(obj) unless obj.is_a?(ObjectDictionary)
195
206
  end
@@ -203,6 +214,7 @@ module Xcodeproj
203
214
  def perform_deletion_operations(objects)
204
215
  objects = [objects] unless objects.is_a?(Array)
205
216
  objects.each do |obj|
217
+ owner.mark_project_as_dirty!
206
218
  obj.remove_referrer(owner) unless obj.is_a?(ObjectDictionary)
207
219
  end
208
220
  end
@@ -189,7 +189,7 @@ module Xcodeproj
189
189
 
190
190
  new_config = project.new(XCBuildConfiguration)
191
191
  new_config.name = configuration.name
192
- new_config.build_settings = common_build_settings(:release, platform, deployment_target, target_product_type, language)
192
+ new_config.build_settings = common_build_settings(configuration.type, platform, deployment_target, target_product_type, language)
193
193
  cl.build_configurations << new_config
194
194
  end
195
195
 
@@ -66,6 +66,7 @@ module Xcodeproj
66
66
  end
67
67
 
68
68
  def switch_uuids(objects)
69
+ @project.mark_dirty!
69
70
  objects.each do |object|
70
71
  next unless path = @paths_by_object[object]
71
72
  uuid = uuid_for_path(path)
@@ -4,7 +4,7 @@ require 'securerandom'
4
4
  require 'xcodeproj/project/object'
5
5
  require 'xcodeproj/project/project_helper'
6
6
  require 'xcodeproj/project/uuid_generator'
7
- require 'xcodeproj/plist_helper'
7
+ require 'xcodeproj/plist'
8
8
 
9
9
  module Xcodeproj
10
10
  # This class represents a Xcode project document.
@@ -55,19 +55,26 @@ module Xcodeproj
55
55
  # @example Creating a project
56
56
  # Project.new("path/to/Project.xcodeproj")
57
57
  #
58
+ # @note When initializing the project, Xcodeproj mimics the Xcode behaviour
59
+ # including the setup of a debug and release configuration. If you want a
60
+ # clean project without any configurations, you should override the
61
+ # `initialize_from_scratch` method to not add these configurations and
62
+ # manually set the object version.
63
+ #
58
64
  def initialize(path, skip_initialization = false, object_version = Constants::DEFAULT_OBJECT_VERSION)
59
65
  @path = Pathname.new(path).expand_path
60
66
  @objects_by_uuid = {}
61
67
  @generated_uuids = []
62
68
  @available_uuids = []
63
- unless skip_initialization
64
- initialize_from_scratch
65
- @object_version = object_version.to_s
66
- end
69
+ @dirty = true
67
70
  unless skip_initialization.is_a?(TrueClass) || skip_initialization.is_a?(FalseClass)
68
71
  raise ArgumentError, '[Xcodeproj] Initialization parameter expected to ' \
69
72
  "be a boolean #{skip_initialization}"
70
73
  end
74
+ unless skip_initialization
75
+ initialize_from_scratch
76
+ @object_version = object_version.to_s
77
+ end
71
78
  end
72
79
 
73
80
  # Opens the project at the given path.
@@ -190,12 +197,13 @@ module Xcodeproj
190
197
  #
191
198
  def initialize_from_file
192
199
  pbxproj_path = path + 'project.pbxproj'
193
- plist = Xcodeproj.read_plist(pbxproj_path.to_s)
200
+ plist = Plist.read_from_path(pbxproj_path.to_s)
194
201
  root_object.remove_referrer(self) if root_object
195
- @root_object = new_from_plist(plist['rootObject'], plist['objects'], self)
196
- @archive_version = plist['archiveVersion']
197
- @object_version = plist['objectVersion']
198
- @classes = plist['classes']
202
+ @root_object = new_from_plist(plist['rootObject'], plist['objects'], self)
203
+ @archive_version = plist['archiveVersion']
204
+ @object_version = plist['objectVersion']
205
+ @classes = plist['classes']
206
+ @dirty = false
199
207
 
200
208
  unless root_object
201
209
  raise "[Xcodeproj] Unable to find a root object in #{pbxproj_path}."
@@ -318,9 +326,25 @@ module Xcodeproj
318
326
  #
319
327
  def save(save_path = nil)
320
328
  save_path ||= path
329
+ @dirty = false if save_path == path
321
330
  FileUtils.mkdir_p(save_path)
322
331
  file = File.join(save_path, 'project.pbxproj')
323
- Xcodeproj.write_plist(to_hash, file)
332
+ Plist.write_to_path(to_hash, file)
333
+ end
334
+
335
+ # Marks the project as dirty, that is, modified from what is on disk.
336
+ #
337
+ # @return [void]
338
+ #
339
+ def mark_dirty!
340
+ @dirty = true
341
+ end
342
+
343
+ # @return [Boolean] Whether this project has been modified since read from
344
+ # disk or saved.
345
+ #
346
+ def dirty?
347
+ @dirty == true
324
348
  end
325
349
 
326
350
  # Replaces all the UUIDs in the project with deterministic MD5 checksums.
@@ -736,7 +760,7 @@ module Xcodeproj
736
760
  end
737
761
 
738
762
  xcschememanagement_path = schemes_dir + 'xcschememanagement.plist'
739
- Xcodeproj.write_plist(xcschememanagement, xcschememanagement_path)
763
+ Plist.write_to_path(xcschememanagement, xcschememanagement_path)
740
764
  end
741
765
 
742
766
  #-------------------------------------------------------------------------#
@@ -76,9 +76,12 @@ module Xcodeproj
76
76
  def initialize(target_or_node = nil)
77
77
  create_xml_element_with_fallback(target_or_node, 'BuildActionEntry') do
78
78
  # Check target type to configure the default entry attributes accordingly
79
- is_test_target, is_app_target = [false, false]
79
+ is_test_target = false
80
+ is_app_target = false
80
81
  if target_or_node && target_or_node.is_a?(::Xcodeproj::Project::Object::PBXNativeTarget)
81
- test_types = [Constants::PRODUCT_TYPE_UTI[:octest_bundle], Constants::PRODUCT_TYPE_UTI[:unit_test_bundle]]
82
+ test_types = [Constants::PRODUCT_TYPE_UTI[:octest_bundle],
83
+ Constants::PRODUCT_TYPE_UTI[:unit_test_bundle],
84
+ Constants::PRODUCT_TYPE_UTI[:ui_test_bundle]]
82
85
  app_types = [Constants::PRODUCT_TYPE_UTI[:application]]
83
86
  is_test_target = test_types.include?(target_or_node.product_type)
84
87
  is_app_target = app_types.include?(target_or_node.product_type)