xcodeproj 0.28.2 → 1.0.0.beta.1

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,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
@@ -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
  #-------------------------------------------------------------------------#
@@ -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
66
  unless @isa.match(/^(PBX|XC)/)
66
- raise '[Xcodeproj] Attempt to initialize an abstract class.'
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -76,7 +76,8 @@ 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
82
  test_types = [Constants::PRODUCT_TYPE_UTI[:octest_bundle], Constants::PRODUCT_TYPE_UTI[:unit_test_bundle]]
82
83
  app_types = [Constants::PRODUCT_TYPE_UTI[:application]]
@@ -0,0 +1,170 @@
1
+ require 'xcodeproj/scheme/xml_element_wrapper'
2
+
3
+ module Xcodeproj
4
+ class XCScheme
5
+ VARIABLES_NODE = 'EnvironmentVariables'
6
+ VARIABLE_NODE = 'EnvironmentVariable'
7
+
8
+ # This class wraps the EnvironmentVariables node of a .xcscheme XML file. This
9
+ # is just a container of EnvironmentVariable objects. It can either appear on a
10
+ # LaunchAction or TestAction scheme group.
11
+ #
12
+ class EnvironmentVariables < XMLElementWrapper
13
+ # @param [nil,REXML::Element,Array<EnvironmentVariable>,Array<Hash{Symbol => String,Bool}>] node_or_variables
14
+ # The 'EnvironmentVariables' XML node, or list of environment variables, that this object represents.
15
+ # - If nil, an empty 'EnvironmentVariables' XML node will be created
16
+ # - If an REXML::Element, it must be named 'EnvironmentVariables'
17
+ # - If an Array of objects or Hashes, they'll each be passed to {#assign_variable}
18
+ #
19
+ def initialize(node_or_variables = nil)
20
+ create_xml_element_with_fallback(node_or_variables, VARIABLES_NODE) do
21
+ @all_variables = []
22
+ node_or_variables.each { |var| assign_variable(var) } unless node_or_variables.nil?
23
+ end
24
+ end
25
+
26
+ # @return [Array<EnvironmentVariable>]
27
+ # The key value pairs currently set in @xml_element
28
+ #
29
+ def all_variables
30
+ @all_variables ||= @xml_element.get_elements(VARIABLE_NODE).map { |variable| EnvironmentVariable.new(variable) }
31
+ end
32
+
33
+ # Adds a given variable to the set of environment variables, or replaces it if that key already exists
34
+ #
35
+ # @param [EnvironmentVariable,Hash{Symbol => String,Bool}] variable
36
+ # The variable to add or update, backed by an EnvironmentVariable node.
37
+ # - If an EnvironmentVariable, the previous reference will still be valid
38
+ # - If a Hash, must conform to {EnvironmentVariable#initialize} requirements
39
+ # @return [Array<EnvironmentVariable>]
40
+ # The new set of environment variables after addition
41
+ #
42
+ def assign_variable(variable)
43
+ env_var = variable.is_a?(EnvironmentVariable) ? variable : EnvironmentVariable.new(variable)
44
+ all_variables.each { |existing_var| remove_variable(existing_var) if existing_var.key == env_var.key }
45
+ @xml_element.add_element(env_var.xml_element)
46
+ @all_variables << env_var
47
+ end
48
+
49
+ # Removes a specified variable (by string or object) from the set of environment variables
50
+ #
51
+ # @param [EnvironmentVariable,String] variable
52
+ # The variable to remove
53
+ # @return [Array<EnvironmentVariable>]
54
+ # The new set of environment variables after removal
55
+ #
56
+ def remove_variable(variable)
57
+ env_var = variable.is_a?(EnvironmentVariable) ? variable : all_variables.find { |var| var.key == variable }
58
+ raise "Unexpected parameter type: #{env_var.class}" unless env_var.is_a?(EnvironmentVariable)
59
+ @xml_element.delete_element(env_var.xml_element)
60
+ @all_variables -= [env_var]
61
+ end
62
+
63
+ # @param [String] key
64
+ # The key to lookup
65
+ # @return [EnvironmentVariable] variable
66
+ # Returns the matching environment variable for a specified key
67
+ #
68
+ def [](key)
69
+ all_variables.find { |var| var.key == key }
70
+ end
71
+
72
+ # Assigns a value for a specified key
73
+ #
74
+ # @param [String] key
75
+ # The key to update in the environment variables
76
+ # @param [String] value
77
+ # The value to lookup
78
+ # @return [EnvironmentVariable] variable
79
+ # The newly updated environment variable
80
+ #
81
+ def []=(key, value)
82
+ assign_variable(:key => key, :value => value)
83
+ self[key]
84
+ end
85
+
86
+ # @return [Array<Hash{Symbol => String,Bool}>]
87
+ # The current environment variables represented as an array
88
+ #
89
+ def to_a
90
+ all_variables.map(&:to_h)
91
+ end
92
+ end
93
+
94
+ # This class wraps the EnvironmentVariable node of a .xcscheme XML file.
95
+ # Environment variables are accessible via the NSDictionary returned from
96
+ # [[NSProcessInfo processInfo] environment] in your app code.
97
+ #
98
+ class EnvironmentVariable < XMLElementWrapper
99
+ # @param [nil,REXML::Element,Hash{Symbol => String,Bool}] node_or_variable
100
+ # - If nil, it will create a default XML node to use
101
+ # - If a REXML::Element, should be a <EnvironmentVariable> XML node to wrap
102
+ # - If a Hash, must contain keys :key and :value (Strings) and optionally :enabled (Boolean)
103
+ #
104
+ def initialize(node_or_variable)
105
+ create_xml_element_with_fallback(node_or_variable, VARIABLE_NODE) do
106
+ raise "Must pass a Hash with 'key' and 'value'!" unless node_or_variable.is_a?(Hash) &&
107
+ node_or_variable.key?(:key) && node_or_variable.key?(:value)
108
+
109
+ @xml_element.attributes['key'] = node_or_variable[:key]
110
+ @xml_element.attributes['value'] = node_or_variable[:value]
111
+
112
+ if node_or_variable.key?(:enabled)
113
+ @xml_element.attributes['isEnabled'] = bool_to_string(node_or_variable[:enabled])
114
+ else
115
+ @xml_element.attributes['isEnabled'] = bool_to_string(true)
116
+ end
117
+ end
118
+ end
119
+
120
+ # Returns the EnvironmentValue's key
121
+ # @return [String]
122
+ #
123
+ def key
124
+ @xml_element.attributes['key']
125
+ end
126
+
127
+ # Sets the EnvironmentValue's key
128
+ # @param [String] key
129
+ #
130
+ def key=(key)
131
+ @xml_element.attributes['key'] = key
132
+ end
133
+
134
+ # Returns the EnvironmentValue's value
135
+ # @return [String]
136
+ #
137
+ def value
138
+ @xml_element.attributes['value']
139
+ end
140
+
141
+ # Sets the EnvironmentValue's value
142
+ # @param [String] value
143
+ #
144
+ def value=(value)
145
+ @xml_element.attributes['value'] = value
146
+ end
147
+
148
+ # Returns the EnvironmentValue's enabled state
149
+ # @return [Bool]
150
+ #
151
+ def enabled
152
+ string_to_bool(@xml_element.attributes['isEnabled'])
153
+ end
154
+
155
+ # Sets the EnvironmentValue's enabled state
156
+ # @param [Bool] enabled
157
+ #
158
+ def enabled=(enabled)
159
+ @xml_element.attributes['isEnabled'] = bool_to_string(enabled)
160
+ end
161
+
162
+ # @return [Hash{:key => String, :value => String, :enabled => Bool}]
163
+ # The environment variable XML node with attributes converted to a representative Hash
164
+ #
165
+ def to_h
166
+ { :key => key, :value => value, :enabled => enabled }
167
+ end
168
+ end
169
+ end
170
+ end