zerg_xcode 0.1 → 0.2

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