xcodeproj 0.28.2 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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