zerg_xcode 0.1 → 0.2

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.
data/CHANGELOG CHANGED
@@ -1 +1,3 @@
1
+ v0.2. Implemented import.
2
+
1
3
  v0.1. Initial release. Knows help, irb, ls, retarget.
data/Manifest CHANGED
@@ -6,8 +6,12 @@ lib/zerg_xcode/file_format/lexer.rb
6
6
  lib/zerg_xcode/file_format/parser.rb
7
7
  lib/zerg_xcode/file_format/paths.rb
8
8
  lib/zerg_xcode/objects/pbx_build_file.rb
9
+ lib/zerg_xcode/objects/pbx_container_item_proxy.rb
10
+ lib/zerg_xcode/objects/pbx_group.rb
9
11
  lib/zerg_xcode/objects/pbx_native_target.rb
10
12
  lib/zerg_xcode/objects/pbx_project.rb
13
+ lib/zerg_xcode/objects/pbx_target_dependency.rb
14
+ lib/zerg_xcode/objects/xc_configuration_list.rb
11
15
  lib/zerg_xcode/objects/xcode_object.rb
12
16
  lib/zerg_xcode/plugins/core/core.rb
13
17
  lib/zerg_xcode/plugins/help.rb
@@ -28,8 +32,12 @@ test/file_format/lexer_test.rb
28
32
  test/file_format/parser_test.rb
29
33
  test/file_format/path_test.rb
30
34
  test/objects/pbx_build_file_test.rb
35
+ test/objects/pbx_container_item_proxy_test.rb
36
+ test/objects/pbx_group_test.rb
31
37
  test/objects/pbx_native_target_test.rb
32
38
  test/objects/pbx_project_test.rb
39
+ test/objects/pbx_target_dependency_test.rb
40
+ test/objects/xc_configuration_list_test.rb
33
41
  test/objects/xcode_object_test.rb
34
42
  test/plugins/core/core_test.rb
35
43
  test/plugins/import_test.rb
@@ -38,6 +46,7 @@ test/plugins/ls_test.rb
38
46
  test/plugins/retarget_test.rb
39
47
  test/plugins/test_helper.rb
40
48
  test/shortcuts_test.rb
49
+ testdata/FlatTestApp/FlatTestApp.xcodeproj/project.pbxproj
41
50
  testdata/project.pbxproj
42
51
  testdata/project.pbxproj.compat
43
52
  testdata/TestApp/TestApp.xcodeproj/project.pbxproj
data/RUBYFORGE CHANGED
@@ -1,6 +1,6 @@
1
1
  Quickstart for Rubyforge:
2
2
 
3
- 1) Checkout the zergxcode code
3
+ 1) Get the code
4
4
  git clone git@github.com:costan/zerg_xcode.git
5
5
 
6
6
  2) Install the rubyforge gem
@@ -30,7 +30,7 @@ Releasing a new gemspec to Github
30
30
  rake package
31
31
 
32
32
  2) Copy the spec
33
- cp pkg/zerg_xcode-0.1/zerg_xcode.gemspec .
33
+ cp pkg/zerg_xcode-*/zerg_xcode.gemspec .
34
34
 
35
35
  3) Commit the spec
36
36
  git add zerg_xcode.gemspec
@@ -0,0 +1,10 @@
1
+ # Proxy for another object.
2
+ #
3
+ # Unsure these are useful, since each object has an unique ID. They are probably
4
+ # implementation artifacts.
5
+ class ZergXcode::Objects::PBXContainerItemProxy < ZergXcode::XcodeObject
6
+ # The proxied object.
7
+ def target
8
+ self['remoteGlobalIDString']
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ # A group of files.
2
+ class ZergXcode::Objects::PBXGroup < ZergXcode::XcodeObject
3
+ end
@@ -3,12 +3,23 @@ class ZergXcode::Objects::PBXProject < ZergXcode::XcodeObject
3
3
  # Used to implement save!
4
4
  attr_accessor :source_filename
5
5
 
6
+ # :nodoc: override to copy the new metadata
7
+ def copy_metadata(source)
8
+ super
9
+ self.source_filename = source.source_filename
10
+ end
11
+
6
12
  # Saves a project that was loaded by ZergXcode.load
7
13
  def save!
8
14
  raise 'Project not loaded by ZergXcode.load' unless @source_filename
9
15
  ZergXcode.dump self, @source_filename
10
16
  end
11
17
 
18
+ # The root path of the project.
19
+ def root_path
20
+ ZergXcode::Paths.project_root_at source_filename
21
+ end
22
+
12
23
  # All the files referenced by the project.
13
24
  def all_files
14
25
  files = []
@@ -0,0 +1,7 @@
1
+ # Expresses a target's dependency on another target.
2
+ class ZergXcode::Objects::PBXTargetDependency < ZergXcode::XcodeObject
3
+ # The target that this target depends on.
4
+ def target
5
+ self['target']
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ # The configurations associated with an object.
2
+ class ZergXcode::Objects::XCConfigurationList < ZergXcode::XcodeObject
3
+ end
@@ -71,6 +71,16 @@ class ZergXcode::XcodeObject
71
71
  visit_hash(@attrs, &accept)
72
72
  self
73
73
  end
74
+
75
+ # Convenience method mapping over visit and exploring each object once.
76
+ def visit_once(&accept)
77
+ visited = Set.new([self])
78
+ self.visit do |object, parent, key, value|
79
+ visited << object
80
+ next_value = yield object, parent, key, value
81
+ visited.include?(value) ? false : next_value
82
+ end
83
+ end
74
84
 
75
85
  def visit_hash(hash, &accept)
76
86
  hash.each_key do |key|
@@ -137,8 +147,12 @@ class ZergXcode::XcodeObject
137
147
 
138
148
  def shallow_copy
139
149
  new_object = self.class.new @attrs.dup
140
- new_object.version, new_object.archive_id = version, archive_id
150
+ new_object.copy_metadata self
141
151
  return new_object
142
- end
152
+ end
153
+
154
+ def copy_metadata(source)
155
+ self.archive_id, self.version = source.archive_id, source.version
156
+ end
143
157
  end
144
158
 
@@ -1,4 +1,9 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+
1
4
  class ZergXcode::Plugins::Import
5
+ include ZergXcode::Objects
6
+
2
7
  def help
3
8
  {:short => 'imports the objects from a project into another project',
4
9
  :long => <<"END" }
@@ -14,6 +19,306 @@ END
14
19
  source = ZergXcode.load args.shift
15
20
  target = ZergXcode.load(args.shift || '.')
16
21
 
17
- print "Import is not yet implemented."
22
+ file_ops = import_project! source, target
23
+ target.save!
24
+ execute_file_ops! file_ops
25
+ end
26
+
27
+ # Executes the given file operations.
28
+ def execute_file_ops!(file_ops)
29
+ file_ops.each do |op|
30
+ case op[:op]
31
+ when :delete
32
+ FileUtils.rm_r op[:path] if File.exist? op[:path]
33
+ when :copy
34
+ target_dir = File.dirname op[:to]
35
+ FileUtils.mkdir_p target_dir unless File.exist? target_dir
36
+ if File.exist? op[:from]
37
+ FileUtils.cp_r op[:from], op[:to]
38
+ else
39
+ print "Source does not have file #{op[:from]}\n"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # Imports the objects of the source project into the target project.
46
+ #
47
+ # Attempts to preserve reference integrity in the target project, as follows.
48
+ # If the source objects have counterparts in the target, their contents is
49
+ # merged into the target project's objects.
50
+ #
51
+ # Returns an array of file operations that need to be performed to migrate the
52
+ # files associated with the two projects.
53
+ #
54
+ # The target project is modified in place.
55
+ def import_project!(source, target)
56
+ old_target_paths = target.all_files.map { |file| file[:path] }
57
+
58
+ # duplicate the source, because the duplicate's object graph will be warped
59
+ scrap_source = ZergXcode::XcodeObject.from source
60
+
61
+ mappings = cross_reference scrap_source, target
62
+ bins = bin_mappings mappings, scrap_source
63
+
64
+ # special case for merging targets
65
+ map! scrap_source, mappings
66
+ merge! scrap_source['targets'], target['targets']
67
+
68
+ # merge the object graphs
69
+ bins[:merge].each do |object|
70
+ map! object, mappings
71
+ merge! object, mappings[object]
72
+ end
73
+ bins[:overwrite].each do |object|
74
+ map! object, mappings
75
+ overwrite! object, mappings[object]
76
+ end
77
+
78
+ # make sure all the mappings point in the right place
79
+ target.visit_once do |object, parent, key, value|
80
+ if mappings[value]
81
+ next mappings[value]
82
+ else
83
+ next true
84
+ end
85
+ end
86
+
87
+ new_target_paths = target.all_files.map { |file| file[:path] }
88
+ source_paths = source.all_files.map { |file| file[:path] }
89
+ return compute_deletes(target.root_path, old_target_paths,
90
+ new_target_paths) +
91
+ compute_copies(source.root_path, source_paths, target.root_path)
92
+ end
93
+
94
+ # Computes the file delete operations in a merge.
95
+ #
96
+ # Deletes all the files that aren't in the target project anymore.
97
+ def compute_deletes(root, old_paths, new_paths)
98
+ new_path_set = Set.new(new_paths)
99
+ old_paths.select { |path| path[0, 2] == './' }.
100
+ reject { |path| new_path_set.include? path }.
101
+ map { |path| { :op => :delete, :path => clean_join(root, path) } }
102
+ end
103
+
104
+ # Computes the file copy operations in a merge.
105
+ #
106
+ # Copies all the files from the source project assuming they received the same
107
+ # path in the target project. The assumption is correct if source was imported
108
+ # into target.
109
+ def compute_copies(source_root, source_paths, target_root)
110
+ source_paths.select { |path| path[0, 2] == './' }.map do |path|
111
+ { :op => :copy, :from => clean_join(source_root, path),
112
+ :to => clean_join(target_root, path) }
113
+ end
114
+ end
115
+
116
+ def clean_join(root, path)
117
+ Pathname.new(File.join(root, path)).cleanpath.to_s
118
+ end
119
+
120
+ # Bins merge mappings for a project into mappings to be merged and mappings to
121
+ # be overwritten.
122
+ def bin_mappings(mappings, source)
123
+ merge_set = Set.new
124
+ overwrite_set = Set.new
125
+
126
+ # the project's top-level attributes are always merged
127
+ source.attrs.each do |attr|
128
+ merge_set << source[attr] if mappings[source[attr]]
129
+ end
130
+
131
+ mappings.each do |source_object, target_object|
132
+ next if source_object == target_object
133
+ next if merge_set.include? source_object
134
+
135
+ if source_object.kind_of?(PBXGroup) && source_object['path'].nil?
136
+ merge_set << source_object
137
+ elsif source_object.kind_of? XCConfigurationList
138
+ merge_set << source_object
139
+ else
140
+ overwrite_set << source_object
141
+ end
142
+ end
143
+
144
+ overwrite_set.delete source
145
+ { :merge => merge_set, :overwrite => overwrite_set }
146
+ end
147
+
148
+ # Modifies an object's attributes according to the given mappings.
149
+ # This explores the object graph, but does not go into sub-objects.
150
+ def map!(object, mappings)
151
+ object.visit_once do |object, parent, key, value|
152
+ if mappings[value]
153
+ parent[key] = mappings[value]
154
+ next false
155
+ end
156
+ next true
157
+ end
158
+ end
159
+
160
+ # Merges the contents of a source object into the target object.
161
+ #
162
+ # Warning: the target will share internal objects with the source. This is
163
+ # intended to be used in a bigger-level opration, where the source will be
164
+ # thrown away afterwards.
165
+ def merge!(source, target)
166
+ if source.class != target.class
167
+ raise "Attempting to merge-combine different kinds of objects - " +
168
+ "#{source.class} != #{target.class}"
169
+ end
170
+
171
+ case source
172
+ when ZergXcode::XcodeObject
173
+ merge! source._attr_hash, target._attr_hash
174
+ when Hash
175
+ source.each_key do |key|
176
+ if !target.has_key?(key)
177
+ target[key] = source[key]
178
+ elsif source[key].kind_of? ZergXcode::XcodeObject
179
+ target[key] = source[key]
180
+ elsif source[key].kind_of?(String)
181
+ target[key] = source[key]
182
+ elsif !source[key].kind_of?(Enumerable)
183
+ target[key] = source[key]
184
+ else
185
+ merge! source[key], target[key]
186
+ end
187
+ end
188
+ when Enumerable
189
+ target_set = Set.new(target.to_a)
190
+ source.each do |value|
191
+ next if target_set.include? value
192
+ target << value
193
+ end
194
+ end
195
+ end
196
+
197
+ # Overwrites the contents of the target object with the source object.
198
+ #
199
+ # Warning: the target will share internal objects with the source. This is
200
+ # intended to be used in a bigger-level opration, where the source will be
201
+ # thrown away afterwards.
202
+ def overwrite!(source, target)
203
+ if source.class != target.class
204
+ raise "Attempting to overwrite-combine different kinds of objects - " +
205
+ "#{source.class} != #{target.class}"
206
+ end
207
+
208
+ case source
209
+ when ZergXcode::XcodeObject
210
+ overwrite! source._attr_hash, target._attr_hash
211
+ when Hash
212
+ target.clear
213
+ target.merge! source
214
+ when Enumerable
215
+ target.clear
216
+ source.each { |value| target << value }
217
+ end
218
+ end
219
+
220
+ # Cross-references the objects in two object graphs that are to be merged.
221
+ # Returns a Hash associating objects in both the source and the target object
222
+ # graphs with the corresponding objects in the
223
+ def cross_reference(source, target, mappings = {})
224
+ if source.class != target.class
225
+ raise "Attempting to cross-reference different kinds of objects - " +
226
+ "#{source.class} != #{target.class}"
227
+ end
228
+
229
+ case target
230
+ when ZergXcode::XcodeObject
231
+ cross_op = :cross_reference_objects
232
+ when Hash
233
+ cross_op = :cross_reference_hashes
234
+ when Enumerable
235
+ cross_op = :cross_reference_enumerables
236
+ else
237
+ return mappings
238
+ end
239
+
240
+ self.send cross_op, source, target, mappings
241
+ end
242
+
243
+ def cross_reference_objects(source, target, mappings)
244
+ return mappings if mappings[source] || mappings[target]
245
+ return mappings if source.xref_key != target.xref_key
246
+
247
+ mappings[target] = target
248
+ mappings[source] = target
249
+ cross_reference source._attr_hash, target._attr_hash, mappings
250
+ end
251
+ private :cross_reference_objects
252
+
253
+ def cross_reference_hashes(source, target, mappings)
254
+ source.each_key do |key|
255
+ p [source.keys, key] if source[key].nil?
256
+ cross_reference source[key], target[key], mappings if target[key]
257
+ end
258
+ mappings
259
+ end
260
+ private :cross_reference_hashes
261
+
262
+ def cross_reference_enumerables(source, target, mappings)
263
+ source_keys = {}
264
+ source.each do |value|
265
+ next unless value.kind_of? ZergXcode::XcodeObject
266
+ source_keys[value.xref_key] = value
267
+ end
268
+ target.each do |value|
269
+ next unless value.kind_of? ZergXcode::XcodeObject
270
+ next unless source_value = source_keys[value.xref_key]
271
+ cross_reference source_value, value, mappings
272
+ end
273
+ mappings
274
+ end
275
+ private :cross_reference_enumerables
276
+ end
277
+
278
+ class ZergXcode::XcodeObject
279
+ def xref_key
280
+ # If the object doesn't have a merge name, use its (unique) object_id.
281
+ [isa, xref_name || object_id]
282
+ end
283
+
284
+ def xref_name
285
+ # Do not use this to override xref_name for specific objects. Only use
286
+ # it for object families.
287
+ case isa.to_s
288
+ when /BuildPhase$/
289
+ isa.to_s
290
+ else
291
+ self['name'] || self['explicitPath'] || self['path']
292
+ end
293
+ end
294
+ end
295
+
296
+ class ZergXcode::Objects::PBXBuildFile
297
+ def xref_name
298
+ self['fileRef'].xref_name
299
+ end
300
+ end
301
+
302
+ class ZergXcode::Objects::PBXProject
303
+ def xref_name
304
+ isa.to_s
305
+ end
306
+ end
307
+
308
+ class ZergXcode::Objects::XCConfigurationList
309
+ def xref_name
310
+ isa.to_s
311
+ end
312
+ end
313
+
314
+ class ZergXcode::Objects::PBXContainerItemProxy
315
+ def xref_name
316
+ self['remoteInfo']
317
+ end
318
+ end
319
+
320
+ class ZergXcode::Objects::PBXTargetDependency
321
+ def xref_name
322
+ target.xref_name
18
323
  end
19
324
  end
data/lib/zerg_xcode.rb CHANGED
@@ -10,8 +10,12 @@ require 'zerg_xcode/file_format/paths.rb'
10
10
 
11
11
  require 'zerg_xcode/objects/xcode_object.rb'
12
12
  require 'zerg_xcode/objects/pbx_build_file.rb'
13
+ require 'zerg_xcode/objects/pbx_container_item_proxy.rb'
14
+ require 'zerg_xcode/objects/pbx_group.rb'
13
15
  require 'zerg_xcode/objects/pbx_native_target.rb'
14
16
  require 'zerg_xcode/objects/pbx_project.rb'
17
+ require 'zerg_xcode/objects/pbx_target_dependency.rb'
18
+ require 'zerg_xcode/objects/xc_configuration_list.rb'
15
19
  require 'zerg_xcode/plugins/core/core.rb'
16
20
 
17
21
  require 'zerg_xcode/shortcuts.rb'
@@ -0,0 +1,14 @@
1
+ require 'test/unit'
2
+
3
+ require 'zerg_xcode'
4
+
5
+ class PBXContainerItemProxyTest < Test::Unit::TestCase
6
+ PBXContainerItemProxy = ZergXcode::Objects::PBXContainerItemProxy
7
+
8
+ def test_target
9
+ proj = ZergXcode.load 'testdata/ZergSupport.xcodeproj/project.pbxproj'
10
+ proxy = proj['targets'][2]['dependencies'].first['targetProxy']
11
+ assert_equal PBXContainerItemProxy, proxy.class
12
+ assert_equal proj['targets'][1], proxy.target
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require 'test/unit'
2
+
3
+ require 'zerg_xcode'
4
+
5
+ class PBXGroupTest < Test::Unit::TestCase
6
+ PBXGroup = ZergXcode::Objects::PBXGroup
7
+
8
+ def test_instantiation
9
+ proj = ZergXcode.load 'testdata/project.pbxproj'
10
+ assert_equal PBXGroup, proj['mainGroup'].class
11
+ end
12
+ end
@@ -59,4 +59,16 @@ class PBXProjectTest < Test::Unit::TestCase
59
59
  and_return(nil)
60
60
  project.save!
61
61
  end
62
+
63
+ def test_root_path
64
+ project = ZergXcode.load('testdata/ZergSupport.xcodeproj')
65
+ assert_equal 'testdata', project.root_path
66
+ end
67
+
68
+ def test_copy_metadata
69
+ project = ZergXcode.load('testdata/ZergSupport.xcodeproj')
70
+ clone = ZergXcode::XcodeObject.from project
71
+
72
+ assert_equal project.source_filename, clone.source_filename
73
+ end
62
74
  end
@@ -0,0 +1,14 @@
1
+ require 'test/unit'
2
+
3
+ require 'zerg_xcode'
4
+
5
+ class PBXTargetDependencyTest < Test::Unit::TestCase
6
+ PBXTargetDependency = ZergXcode::Objects::PBXTargetDependency
7
+
8
+ def test_target
9
+ proj = ZergXcode.load 'testdata/ZergSupport.xcodeproj/project.pbxproj'
10
+ dependency = proj['targets'][2]['dependencies'].first
11
+ assert_equal PBXTargetDependency, dependency.class
12
+ assert_equal proj['targets'][1], dependency.target
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require 'test/unit'
2
+
3
+ require 'zerg_xcode'
4
+
5
+ class XCConfigurationListTest < Test::Unit::TestCase
6
+ XCConfigurationList = ZergXcode::Objects::XCConfigurationList
7
+
8
+ def test_instantiation
9
+ proj = ZergXcode.load 'testdata/project.pbxproj'
10
+ assert_equal XCConfigurationList, proj['buildConfigurationList'].class
11
+ end
12
+ end
@@ -46,6 +46,32 @@ class ObjectTest < Test::Unit::TestCase
46
46
  assert_equal golden_visited.sort, visited.sort
47
47
  end
48
48
 
49
+ def test_visit_once
50
+ golden_visited = [
51
+ [49, 'sub1', @sub1],
52
+ [39, 'array', @sub1[:array]],
53
+ [39, '0', 'a'],
54
+ [39, '1', 'b'],
55
+ [39, '2', 'c'],
56
+ [39, 'string', 's'],
57
+ [39, 'root', @root],
58
+ [49, 'sub2', @sub2],
59
+ [42, 'hash', @sub2[:hash]],
60
+ [42, 'k', 'v'],
61
+ [42, 'k2', 'v2'],
62
+ [42, 'sub1', @sub1],
63
+ ]
64
+ @sub1['root'] = @root
65
+
66
+ visited = []
67
+ @root.visit_once do |object, parent, key, value|
68
+ assert_equal value, parent[key], 'Parent/Key/Value check'
69
+ visited << [object.archive_id, key.to_s, value]
70
+ next true
71
+ end
72
+ assert_equal golden_visited.sort, visited.sort
73
+ end
74
+
49
75
  def test_mutating_visit
50
76
  golden_visited = [
51
77
  [49, 'sub1', @sub1],