cocoapods-xccache 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1047 @@
1
+ require 'xcodeproj'
2
+ require 'digest'
3
+ require 'set'
4
+ require 'open3'
5
+ require "find"
6
+ require 'yaml'
7
+ require 'pathname'
8
+
9
+ module Zabel
10
+ class Error < StandardError; end
11
+
12
+ BUILD_KEY_SYMROOT = "SYMROOT"
13
+ BUILD_KEY_CONFIGURATION_BUILD_DIR = "CONFIGURATION_BUILD_DIR"
14
+ BUILD_KEY_TARGET_BUILD_DIR = "TARGET_BUILD_DIR"
15
+ BUILD_KEY_OBJROOT = "OBJROOT"
16
+ BUILD_KEY_TARGET_TEMP_DIR = "TARGET_TEMP_DIR"
17
+ BUILD_KEY_PODS_XCFRAMEWORKS_BUILD_DIR = "PODS_XCFRAMEWORKS_BUILD_DIR"
18
+ BUILD_KEY_MODULEMAP_FILE = "MODULEMAP_FILE"
19
+ BUILD_KEY_SRCROOT = "SRCROOT"
20
+ BUILD_KEY_FULL_PRODUCT_NAME = "FULL_PRODUCT_NAME"
21
+
22
+ STATUS_HIT = "hit"
23
+ STATUS_MISS = "miss"
24
+ STATUS_MISS_AND_READY = "miss_ready"
25
+
26
+ STAGE_CLEAN = "clean"
27
+ STAGE_EXTRACT = "extract"
28
+ STAGE_PRINTENV = "printenv"
29
+ STAGE_PRE = "pre"
30
+ STAGE_POST = "post"
31
+
32
+ FILE_NAME_MESSAGE = "message.txt"
33
+ FILE_NAME_CONTEXT = "context.yml"
34
+ FILE_NAME_PRODUCT = "product.tar"
35
+ FILE_NAME_TARGET_CONTEXT = "zabel_target_context.yml"
36
+
37
+ #FILE_NAME_CACHE = "pod_cache.txt"
38
+ #PRE_SHELL_NAME = "xccache_pre"
39
+ MAIN_PROJECT_SHELL_NAME = "zable_xccache"
40
+
41
+ def self.zabel_get_cache_root
42
+ cache_root = ENV["ZABEL_CACHE_ROOT"]
43
+ if cache_root and cache_root.size > 0
44
+ return cache_root
45
+ end
46
+
47
+ return Dir.home + "/xccache"
48
+ end
49
+
50
+ def self.zabel_get_cache_count
51
+ cache_count = ENV["ZABEL_CACHE_COUNT"]
52
+ if cache_count and cache_count.to_i.to_s == cache_count
53
+ return cache_count.to_i
54
+ end
55
+ return 10000
56
+ end
57
+
58
+ def self.zabel_should_not_detect_module_map_dependency
59
+ # By default, zabel detects module map dependency.
60
+ # However, there are bugs of xcodebuild or swift-frontend, which emits unnecessary and incorrect modulemap dependencies.
61
+ # To test by run "ruby test/one.rb test/todo/modulemap_file/Podfile"
62
+ # To avoid by set "export ZABEL_NOT_DETECT_MODULE_MAP_DEPENDENCY=YES"
63
+ zabel_should_not_detect_module_map_dependency = ENV["ZABEL_NOT_DETECT_MODULE_MAP_DEPENDENCY"]
64
+ if zabel_should_not_detect_module_map_dependency == "YES"
65
+ return true
66
+ end
67
+ return false
68
+ end
69
+
70
+ def self.zabel_get_min_source_file_count
71
+ # By default, zable caches targets which count of source files is greater than or equal 1.
72
+ # You can set this value to 0 or more than 1 to achieve higher speed.
73
+ min_source_file_count = ENV["ZABEL_MIN_SOURCE_FILE_COUNT"]
74
+ if min_source_file_count and min_source_file_count.to_i.to_s == min_source_file_count
75
+ return min_source_file_count.to_i
76
+ end
77
+ return 1
78
+ end
79
+
80
+ def self.zabel_get_projects
81
+ # TODO: to support more project, not only Pods
82
+ pods_project = Xcodeproj::Project.open("Pods/Pods.xcodeproj")
83
+ wrapper_project_paths = zabel_get_wrapper_project_paths(pods_project)
84
+ wrapper_projects = []
85
+ wrapper_project_paths.each do | path |
86
+ next if path.end_with? "Pods/Pods.xcodeproj"
87
+ project = Xcodeproj::Project.open(path)
88
+ wrapper_projects.push project
89
+ end
90
+ return (wrapper_projects + [pods_project])
91
+ end
92
+
93
+ def self.zabel_get_wrapper_project_paths(project)
94
+ wrapper_projects = project.files.select{|file| file.last_known_file_type=="wrapper.pb-project"}
95
+ wrapper_project_paths = []
96
+ wrapper_projects.each do | wrapper_project_file |
97
+ wrapper_project_file_path = wrapper_project_file.real_path.to_s
98
+ wrapper_project_paths.push wrapper_project_file_path
99
+ end
100
+ return wrapper_project_paths.uniq
101
+ end
102
+
103
+ # 是否缓存
104
+ # $cache_hash = {}
105
+ # $has_load_cache_file = false
106
+
107
+ def self.zabel_can_cache_target(target)
108
+ if target.name.start_with? "Pods-"
109
+ puts "[XCCACHE/I] skip #{target.name}"
110
+ return false
111
+ end
112
+ if target.class == Xcodeproj::Project::Object::PBXNativeTarget
113
+ # see https://github.com/CocoaPods/Xcodeproj/blob/master/lib/xcodeproj/constants.rb#L145
114
+ if target.product_type == "com.apple.product-type.bundle" or
115
+ target.product_type == "com.apple.product-type.library.static" or
116
+ target.product_type == "com.apple.product-type.framework"
117
+ return true
118
+ else
119
+ puts "[XCCACHE/I] skip #{target.name} #{target.class} #{target.product_type}"
120
+ end
121
+ else
122
+ puts "[XCCACHE/I] skip #{target.name} #{target.class}"
123
+ end
124
+ return false
125
+ end
126
+
127
+ def self.zabel_get_dependency_files(target, intermediate_dir, product_dir, xcframeworks_build_dir)
128
+ dependency_files = []
129
+ Dir.glob("#{intermediate_dir}/**/*.d").each do | dependency_file |
130
+ content = File.read(dependency_file)
131
+ # see https://github.com/ccache/ccache/blob/master/src/Depfile.cpp#L141
132
+ # and this is a simple regex parser enough to get all files, as far as I know.
133
+ files = content.scan(/(?:\S(?:\\ )*)+/).flatten.uniq
134
+ files = files - ["dependencies:", "\\", ":"]
135
+
136
+ files.each do | file |
137
+ file = file.gsub("\\ ", " ")
138
+
139
+ unless File.exist? file
140
+ puts "[XCCACHE/E] #{target.name} #{file} should exist in dependency file #{dependency_file} in #{intermediate_dir}/**/*.d"
141
+ return []
142
+ end
143
+
144
+ if file.start_with? intermediate_dir + "/" or
145
+ file.start_with? product_dir + "/"
146
+ next
147
+ end
148
+
149
+ if xcframeworks_build_dir and xcframeworks_build_dir.size > 0 and file.start_with? xcframeworks_build_dir + "/"
150
+ next
151
+ end
152
+
153
+ dependency_files.push file
154
+ end
155
+ end
156
+ return dependency_files.uniq
157
+ end
158
+
159
+ def self.zabel_get_target_source_files(target)
160
+ files = []
161
+ target.source_build_phase.files.each do | file |
162
+ file_path = file.file_ref.real_path.to_s
163
+ files.push file_path
164
+ end
165
+ target.headers_build_phase.files.each do | file |
166
+ file_path = file.file_ref.real_path.to_s
167
+ files.push file_path
168
+ end
169
+ target.resources_build_phase.files.each do | file |
170
+ file_path = file.file_ref.real_path.to_s
171
+ files.push file_path
172
+ end
173
+
174
+ expand_files = []
175
+ files.uniq.each do | file |
176
+ next unless File.exist? file
177
+ if File.file? file
178
+ expand_files.push file
179
+ else
180
+ Find.find(file).each do | file_in_dir |
181
+ if File.file? file_in_dir
182
+ expand_files.push file_in_dir
183
+ end
184
+ end
185
+ end
186
+ end
187
+ return expand_files.uniq
188
+ end
189
+
190
+ def self.zabel_get_content_without_pwd(content)
191
+ content = content.gsub("#{Dir.pwd}/", "").gsub(/#{Dir.pwd}(\W|$)/, '\1')
192
+ return content
193
+ end
194
+
195
+ $zabel_file_md5_hash = {}
196
+
197
+ def self.zabel_get_file_md5(file)
198
+ if $zabel_file_md5_hash.has_key? file
199
+ return $zabel_file_md5_hash[file]
200
+ end
201
+ md5 = Digest::MD5.hexdigest(File.read(file))
202
+ $zabel_file_md5_hash[file] = md5
203
+ return md5
204
+ end
205
+
206
+ def self.zabel_keep
207
+ file_list = Dir.glob("#{zabel_get_cache_root}/*")
208
+ file_time_hash = {}
209
+ file_list.each do | file |
210
+ file_time_hash[file] = File.mtime(file)
211
+ end
212
+ file_list = file_list.sort_by {|file| - file_time_hash[file].to_f}
213
+ puts "[XCCACHE/I] keep cache " + file_list.size.to_s + " " + Open3.capture3("du -sh #{zabel_get_cache_root}")[0].to_s
214
+
215
+ if file_list.size > 1
216
+ puts "[XCCACHE/I] keep oldest " + file_time_hash[file_list.last].to_s + " " + file_list.last
217
+ puts "[XCCACHE/I] keep newest " + file_time_hash[file_list.first].to_s + " " + file_list.first
218
+ end
219
+
220
+ if file_list.size > zabel_get_cache_count
221
+ file_list_remove = file_list[zabel_get_cache_count..(file_list.size-1)]
222
+ file_list_remove.each do | file |
223
+ raise unless system "rm -rf \"#{file}\""
224
+ end
225
+ end
226
+ end
227
+
228
+ def self.zabel_clean_backup_project(project)
229
+ command = "rm -rf \"#{project.path}/project.zabel_backup_pbxproj\""
230
+ raise unless system command
231
+ end
232
+
233
+
234
+ def self.zabel_backup_project(project)
235
+ command = "cp \"#{project.path}/project.pbxproj\" \"#{project.path}/project.zabel_backup_pbxproj\""
236
+ raise unless system command
237
+ end
238
+
239
+ def self.zabel_restore_project(project)
240
+ if File.exist? "#{project.path}/project.zabel_backup_pbxproj"
241
+ command = "mv \"#{project.path}/project.zabel_backup_pbxproj\" \"#{project.path}/project.pbxproj\""
242
+ raise unless system command
243
+ end
244
+ end
245
+
246
+ $zabel_podfile_spec_checksums = nil
247
+
248
+ def self.zabel_get_target_md5_content(project, target, configuration_name, argv, source_files)
249
+
250
+ unless $zabel_podfile_spec_checksums
251
+ if File.exist? "Podfile.lock"
252
+ podfile_lock = YAML.load(File.read("Podfile.lock"))
253
+ $zabel_podfile_spec_checksums = podfile_lock["SPEC CHECKSUMS"]
254
+ end
255
+ end
256
+
257
+ project_configuration = project.build_configurations.detect { | config |
258
+ config.name == configuration_name
259
+ }
260
+ project_configuration_content = project_configuration.pretty_print.to_yaml
261
+ project_xcconfig = ""
262
+ if project_configuration.base_configuration_reference
263
+ config_file_path = project_configuration.base_configuration_reference.real_path.to_s
264
+ if File.exist? config_file_path
265
+ project_xcconfig = File.read(config_file_path).lines.reject{|line|line.include? "_SEARCH_PATHS"}.sort.join("")
266
+ end
267
+ end
268
+
269
+ target_configuration = target.build_configurations.detect { | config | config.name == configuration_name}
270
+ target_configuration_content = target_configuration.pretty_print.to_yaml
271
+ target_xcconfig = ""
272
+ if target_configuration.base_configuration_reference
273
+ config_file_path = target_configuration.base_configuration_reference.real_path.to_s
274
+ if File.exist? config_file_path
275
+ target_xcconfig = File.read(config_file_path).lines.reject{|line|line.include? "_SEARCH_PATHS"}.sort.join("")
276
+ end
277
+ end
278
+
279
+ first_configuration = []
280
+ build_phases = []
281
+ build_phases.push target.source_build_phase if target.methods.include? :source_build_phase
282
+ build_phases.push target.resources_build_phase if target.methods.include? :resources_build_phase
283
+ build_phases.each do | build_phase |
284
+ build_phase.files_references.each do | files_reference |
285
+ if files_reference.class == Xcodeproj::Project::Object::PBXVariantGroup
286
+ files_reference.files.each do |file_ref_in_group|
287
+ file_ref_in_group.build_files.each do |build_file|
288
+ if build_file.settings and build_file.settings.class == Hash
289
+ first_configuration.push File.basename(build_file.file_ref.real_path.to_s) + "\n" + build_file.settings.to_yaml
290
+ end
291
+ end
292
+ end
293
+ else
294
+ files_reference.build_files.each do |build_file|
295
+ if build_file.settings and build_file.settings.class == Hash
296
+ first_configuration.push File.basename(build_file.file_ref.real_path.to_s) + "\n" + build_file.settings.to_yaml
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+ first_configuration_content = first_configuration.sort.uniq.join("\n")
303
+
304
+ key_argv = []
305
+
306
+ # TODO: to add more and test more
307
+ # However, you can control your cache keys manually by using pre and post.
308
+ temp_path_list = ["-derivedDataPath", "-archivePath", "--derived_data_path", "--archive_path", "--build_path"]
309
+ argv.each_with_index do | arg, index |
310
+ next if temp_path_list.include? arg
311
+ next if index > 0 and temp_path_list.include? argv[index-1]
312
+ next if arg.start_with? "DSTROOT="
313
+ next if arg.start_with? "OBJROOT="
314
+ next if arg.start_with? "SYMROOT="
315
+ key_argv.push arg
316
+ end
317
+
318
+ source_md5_list = []
319
+ # zabel built-in verison, which will be changed for incompatibility in the future
320
+ source_md5_list.push "Zabel cache version : #{CocoapodsXccache::CACHE_VERSION}"
321
+ source_md5_list.push "ARGV : #{key_argv.to_s}"
322
+
323
+ # TODO: to get a explicit spec name from a target.
324
+ target_possible_spec_names = []
325
+ target_possible_spec_names.push target_configuration.build_settings["PRODUCT_NAME"] if target_configuration.build_settings["PRODUCT_NAME"]
326
+ target_possible_spec_names.push target_configuration.build_settings["IBSC_MODULE"] if target_configuration.build_settings["IBSC_MODULE"]
327
+ target_possible_spec_names.push File.basename(target_configuration.build_settings["CONFIGURATION_BUILD_DIR"]) if target_configuration.build_settings["CONFIGURATION_BUILD_DIR"]
328
+ if target_xcconfig.lines.detect { | line | line.start_with? "CONFIGURATION_BUILD_DIR = "}
329
+ target_possible_spec_names.push File.basename(target_xcconfig.lines.detect { | line | line.start_with? "CONFIGURATION_BUILD_DIR = "}.strip)
330
+ end
331
+ if target_xcconfig.lines.detect { | line | line.start_with? "PODS_TARGET_SRCROOT = "}
332
+ target_possible_spec_names.push File.basename(target_xcconfig.lines.detect { | line | line.start_with? "PODS_TARGET_SRCROOT = "}.strip)
333
+ end
334
+
335
+ target_match_spec_names = []
336
+ target_possible_spec_names.uniq.sort.each do | spec_name |
337
+ if spec_name.size > 0 and $zabel_podfile_spec_checksums.has_key? spec_name
338
+ source_md5_list.push "SPEC CHECKSUM : #{spec_name} #{$zabel_podfile_spec_checksums[spec_name]}"
339
+ target_match_spec_names.push spec_name
340
+ end
341
+ end
342
+
343
+ unless target_match_spec_names.size == 1
344
+ puts "[XCCACHE/E] #{target.name} #{target_possible_spec_names.to_s} #{target_match_spec_names.to_s} SPEC CHECKSUM should be found"
345
+ puts target_configuration.build_settings.to_s
346
+ puts target_xcconfig
347
+ end
348
+
349
+ source_md5_list.push "Project : #{File.basename(project.path)}"
350
+ source_md5_list.push "Project configuration : "
351
+ source_md5_list.push project_configuration_content.strip
352
+ source_md5_list.push "Project xcconfig : "
353
+ source_md5_list.push project_xcconfig.strip
354
+ source_md5_list.push "Target : #{target.name}"
355
+ source_md5_list.push "Target type : #{target.product_type}"
356
+ source_md5_list.push "Target configuration : "
357
+ source_md5_list.push target_configuration_content.strip
358
+ source_md5_list.push "Target xcconfig : "
359
+ source_md5_list.push target_xcconfig.strip
360
+ source_md5_list.push "Files settings : "
361
+ source_md5_list.push first_configuration_content.strip
362
+
363
+ source_md5_list.push "Files MD5 : "
364
+ source_files.uniq.sort.each do | file |
365
+ source_md5_list.push zabel_get_content_without_pwd(file) + " : " + zabel_get_file_md5(file)
366
+ end
367
+
368
+ source_md5_content = source_md5_list.join("\n")
369
+ return source_md5_content
370
+ end
371
+
372
+ def self.zabel_clean_temp_files
373
+ command = "rm -rf Pods/*.xcodeproj/project.zabel_backup_pbxproj"
374
+ puts command
375
+ raise unless system command
376
+
377
+ command = "rm -rf Pods/*.xcodeproj/*.#{FILE_NAME_TARGET_CONTEXT}"
378
+ puts command
379
+ raise unless system command
380
+ end
381
+
382
+ def self.zabel_add_cache(target, target_context, message)
383
+ target_md5 = target_context[:target_md5]
384
+
385
+ product_dir = target_context[BUILD_KEY_CONFIGURATION_BUILD_DIR]
386
+ intermediate_dir = target_context[BUILD_KEY_TARGET_TEMP_DIR]
387
+ full_product_name = target_context[BUILD_KEY_FULL_PRODUCT_NAME]
388
+
389
+ target_cache_dir = zabel_get_cache_root + "/" + target.name + "-" + target_md5 + "-" + (Time.now.to_f * 1000).to_i.to_s
390
+
391
+ Dir.glob("#{product_dir}/**/*.modulemap").each do | modulemap |
392
+ modulemap_content = File.read(modulemap)
393
+ if modulemap_content.include? File.dirname(modulemap) + "/"
394
+ modulemap_content = modulemap_content.gsub(File.dirname(modulemap) + "/", "")
395
+ File.write(modulemap, modulemap_content)
396
+ end
397
+ end
398
+
399
+ unless full_product_name and full_product_name.size > 0 and File.exist? "#{product_dir}/#{full_product_name}"
400
+ puts "[XCCACHE/E] #{target.name} #{product_dir}/#{full_product_name} should exist"
401
+ return false
402
+ end
403
+
404
+ zip_start_time = Time.now
405
+
406
+ command = "cd \"#{File.dirname(product_dir)}\" && tar -L -c -f #{target.name}.#{FILE_NAME_PRODUCT} #{File.basename(product_dir)}/#{full_product_name}"
407
+ if target.product_type == "com.apple.product-type.library.static"
408
+ command = "cd \"#{File.dirname(product_dir)}\" && tar --exclude=*.bundle --exclude=*.framework -L -c -f #{target.name}.#{FILE_NAME_PRODUCT} #{File.basename(product_dir)}"
409
+ end
410
+
411
+ puts command
412
+ unless system command
413
+ puts "[XCCACHE/E] #{command} should succeed"
414
+ return false
415
+ end
416
+
417
+ if File.exist? target_cache_dir
418
+ puts "[XCCACHE/E] #{target_cache_dir} should not exist"
419
+ raise unless system "rm -rf \"#{target_cache_dir}\""
420
+ return false
421
+ end
422
+
423
+ command = "mkdir -p \"#{target_cache_dir}\""
424
+ unless system command
425
+ puts command
426
+ puts "[XCCACHE/E] #{command} should succeed"
427
+ return false
428
+ end
429
+
430
+ cache_product_path = target_cache_dir + "/#{FILE_NAME_PRODUCT}"
431
+
432
+ command = "mv \"#{File.dirname(product_dir)}/#{target.name}.#{FILE_NAME_PRODUCT}\" \"#{cache_product_path}\""
433
+ puts command
434
+ unless system command
435
+ puts command
436
+ puts "[XCCACHE/E] #{command} should succeed"
437
+ return false
438
+ end
439
+ unless File.exist? cache_product_path
440
+ puts "[XCCACHE/E] #{cache_product_path} should exist after mv"
441
+ return false
442
+ end
443
+
444
+ target_context[:product_md5] = zabel_get_file_md5(cache_product_path)
445
+ target_context[:build_product_dir] = target_context[BUILD_KEY_CONFIGURATION_BUILD_DIR].gsub(target_context[BUILD_KEY_SYMROOT] + "/", "")
446
+ target_context[:build_intermediate_dir] = target_context[BUILD_KEY_TARGET_TEMP_DIR].gsub(target_context[BUILD_KEY_OBJROOT] + "/", "")
447
+ if target_context[BUILD_KEY_MODULEMAP_FILE]
448
+ target_context[BUILD_KEY_MODULEMAP_FILE] = zabel_get_content_without_pwd target_context[BUILD_KEY_MODULEMAP_FILE]
449
+ end
450
+
451
+ target_context = target_context.clone
452
+ target_context.delete(:dependency_files)
453
+ target_context.delete(:target_status)
454
+ target_context.delete(:potential_hit_target_cache_dirs)
455
+ target_context.delete(:target_md5_content)
456
+ target_context.delete(:miss_dependency_list)
457
+ [BUILD_KEY_SYMROOT, BUILD_KEY_CONFIGURATION_BUILD_DIR, BUILD_KEY_OBJROOT, BUILD_KEY_TARGET_TEMP_DIR, BUILD_KEY_PODS_XCFRAMEWORKS_BUILD_DIR, BUILD_KEY_SRCROOT].each do | key |
458
+ target_context.delete(key)
459
+ end
460
+
461
+ File.write(target_cache_dir + "/" + FILE_NAME_CONTEXT, target_context.to_yaml)
462
+ File.write(target_cache_dir + "/" + FILE_NAME_MESSAGE, message)
463
+
464
+ return true
465
+ end
466
+
467
+ def self.zabel_post(argv)
468
+
469
+ if argv.nil?
470
+ argv = {}
471
+ end
472
+
473
+ # configuration_name = nil
474
+ configuration_name = "Debug"
475
+
476
+ # if argv.include?("-configuration")
477
+ # configuration_name = argv[argv.index("-configuration") + 1]
478
+ # elsif argv.include?("--configuration")
479
+ # configuration_name = argv[argv.index("--configuration") + 1]
480
+ # end
481
+ # unless configuration_name and configuration_name.size > 0
482
+ # raise "[XCCACHE/E] -configuration or --configuration should be set"
483
+ # end
484
+
485
+ start_time = Time.now
486
+
487
+ add_count = 0
488
+
489
+ projects = zabel_get_projects
490
+
491
+ post_targets_context = {}
492
+
493
+ projects.each do | project |
494
+ project_configuration = project.build_configurations.detect { | config | config.name == configuration_name}
495
+ unless project_configuration
496
+ puts "[XCCACHE/E] #{project.path} should have config #{configuration_name}"
497
+ next
498
+ end
499
+ project.native_targets.each do | target |
500
+ target_context_file = "#{project.path}/#{target.name}.#{FILE_NAME_TARGET_CONTEXT}"
501
+ unless File.exist? target_context_file
502
+ next
503
+ end
504
+ if zabel_can_cache_target(target)
505
+ target_context = YAML.load(File.read(target_context_file))
506
+
507
+ if target_context[:target_status] == STATUS_MISS_AND_READY
508
+ environment_valid = true
509
+ [BUILD_KEY_SYMROOT, BUILD_KEY_CONFIGURATION_BUILD_DIR, BUILD_KEY_OBJROOT, BUILD_KEY_TARGET_TEMP_DIR, BUILD_KEY_SRCROOT, BUILD_KEY_FULL_PRODUCT_NAME].sort.each do | key |
510
+ unless target_context.has_key? key and target_context[key] and target_context[key].size > 0
511
+ puts "[XCCACHE/E] #{target.name} should have #{key} in #{target_context.to_s}"
512
+ environment_valid = false
513
+ break
514
+ end
515
+ end
516
+ next unless environment_valid
517
+
518
+ source_files = zabel_get_target_source_files(target)
519
+
520
+ product_dir = target_context[BUILD_KEY_CONFIGURATION_BUILD_DIR]
521
+ intermediate_dir = target_context[BUILD_KEY_TARGET_TEMP_DIR]
522
+ xcframeworks_build_dir = target_context[BUILD_KEY_PODS_XCFRAMEWORKS_BUILD_DIR]
523
+
524
+ dependency_files = zabel_get_dependency_files(target, intermediate_dir, product_dir, xcframeworks_build_dir)
525
+ if source_files.size > 0 and dependency_files.size == 0 and target.product_type != "com.apple.product-type.bundle"
526
+ puts "[XCCACHE/E] #{target.name} should have dependent files"
527
+ next
528
+ end
529
+ target_context[:dependency_files] = dependency_files - source_files
530
+ target_md5_content = zabel_get_target_md5_content(project, target, configuration_name, argv, source_files)
531
+ target_context[:target_md5_content] = target_md5_content
532
+ target_md5 = Digest::MD5.hexdigest(target_md5_content)
533
+ unless target_context[:target_md5] == target_md5
534
+ puts "[XCCACHE/E] #{target.name} md5 should not be changed after build"
535
+ next
536
+ end
537
+ if target_context[BUILD_KEY_SRCROOT] and target_context[BUILD_KEY_SRCROOT].size > 0 and
538
+ target_context[BUILD_KEY_MODULEMAP_FILE] and target_context[BUILD_KEY_MODULEMAP_FILE].size > 0
539
+ if File.exist? Dir.pwd + "/" + zabel_get_content_without_pwd("#{target_context[BUILD_KEY_SRCROOT]}/#{target_context[BUILD_KEY_MODULEMAP_FILE]}")
540
+ target_context[BUILD_KEY_MODULEMAP_FILE] = zabel_get_content_without_pwd("#{target_context[BUILD_KEY_SRCROOT]}/#{target_context[BUILD_KEY_MODULEMAP_FILE]}")
541
+ else
542
+ puts "[XCCACHE/E] #{target.name} #{target_context[BUILD_KEY_MODULEMAP_FILE]} should be supported"
543
+ next
544
+ end
545
+ end
546
+ elsif target_context[:target_status] == STATUS_HIT
547
+ if target_context[BUILD_KEY_MODULEMAP_FILE] and target_context[BUILD_KEY_MODULEMAP_FILE].size > 0
548
+ if not File.exist? Dir.pwd + "/" + target_context[BUILD_KEY_MODULEMAP_FILE]
549
+ puts "[XCCACHE/E] #{target.name} #{target_context[BUILD_KEY_MODULEMAP_FILE]} should be supported"
550
+ next
551
+ end
552
+ end
553
+ else
554
+ puts "[XCCACHE/E] #{target.name} should be hit or miss"
555
+ next
556
+ end
557
+
558
+ post_targets_context[target] = target_context
559
+ end
560
+ end
561
+ end
562
+
563
+ projects.each do | project |
564
+ project.native_targets.each do | target |
565
+ if post_targets_context.has_key? target
566
+ target_context = post_targets_context[target]
567
+ next unless target_context[:target_status] == STATUS_MISS_AND_READY
568
+
569
+ dependency_targets_set = Set.new
570
+ implicit_dependencies = []
571
+
572
+ post_targets_context.each do | other_target, other_target_context |
573
+ next if other_target == target
574
+
575
+ next if target.product_type == "com.apple.product-type.bundle"
576
+ next if other_target.product_type == "com.apple.product-type.bundle"
577
+
578
+ target_context[:dependency_files].each do | dependency |
579
+
580
+ if other_target_context[BUILD_KEY_CONFIGURATION_BUILD_DIR] and other_target_context[BUILD_KEY_CONFIGURATION_BUILD_DIR].size > 0 and
581
+ dependency.start_with? other_target_context[BUILD_KEY_CONFIGURATION_BUILD_DIR] + "/"
582
+ dependency_targets_set.add other_target
583
+ implicit_dependencies.push dependency
584
+ elsif other_target_context[BUILD_KEY_TARGET_TEMP_DIR] and other_target_context[BUILD_KEY_TARGET_TEMP_DIR].size > 0 and
585
+ dependency.start_with? other_target_context[BUILD_KEY_TARGET_TEMP_DIR] + "/"
586
+ dependency_targets_set.add other_target
587
+ implicit_dependencies.push dependency
588
+ elsif other_target_context[:build_product_dir] and other_target_context[:build_product_dir].size > 0 and
589
+ dependency.start_with? target_context[BUILD_KEY_SYMROOT] + "/" + other_target_context[:build_product_dir] + "/"
590
+ dependency_targets_set.add other_target
591
+ implicit_dependencies.push dependency
592
+ elsif other_target_context[:build_intermediate_dir] and other_target_context[:build_intermediate_dir].size > 0 and
593
+ dependency.start_with? target_context[BUILD_KEY_OBJROOT] + "/" + other_target_context[:build_intermediate_dir] + "/"
594
+ dependency_targets_set.add other_target
595
+ implicit_dependencies.push dependency
596
+ end
597
+
598
+ unless zabel_should_not_detect_module_map_dependency
599
+ if other_target_context[BUILD_KEY_MODULEMAP_FILE] and other_target_context[BUILD_KEY_MODULEMAP_FILE].size > 0 and
600
+ dependency == Dir.pwd + "/" + other_target_context[BUILD_KEY_MODULEMAP_FILE]
601
+ dependency_targets_set.add other_target
602
+ end
603
+ end
604
+ end
605
+
606
+ target_context[:dependency_files] = target_context[:dependency_files] - implicit_dependencies
607
+
608
+ end
609
+
610
+ target_context[:dependency_files] = target_context[:dependency_files] - implicit_dependencies
611
+ dependency_files_md5 = []
612
+ should_not_cache = false
613
+ target_context[:dependency_files].each do | file |
614
+ if file.start_with? target_context[BUILD_KEY_OBJROOT] + "/" or file.start_with? target_context[BUILD_KEY_SYMROOT] + "/"
615
+ puts "[XCCACHE/W] #{target.name} #{file} dependecy should not include build path"
616
+ should_not_cache = true
617
+ break
618
+ end
619
+ dependency_files_md5.push [zabel_get_content_without_pwd(file), zabel_get_file_md5(file)]
620
+ end
621
+ next if should_not_cache
622
+
623
+ target_context[:dependency_files_md5] = dependency_files_md5.sort.uniq
624
+
625
+ dependency_targets_md5 = dependency_targets_set.to_a.map { | target | [target.name, post_targets_context[target][:target_md5]]}
626
+ target_context[:dependency_targets_md5] = dependency_targets_md5
627
+
628
+ message = target_context[:target_md5_content]
629
+
630
+ if zabel_add_cache(target, target_context, message)
631
+ add_count = add_count + 1
632
+ end
633
+ end
634
+ end
635
+ end
636
+
637
+ puts "[XCCACHE/I] 恢复"
638
+ projects.each do | project |
639
+ zabel_restore_project(project)
640
+ end
641
+
642
+ zabel_keep
643
+
644
+ puts "[XCCACHE/I] total add #{add_count}"
645
+
646
+ puts "[XCCACHE/I] duration = #{(Time.now - start_time).to_i} s in stage post"
647
+
648
+ #zd_delete_main_target_shell
649
+ end
650
+
651
+ def self.zabel_get_potential_hit_target_cache_dirs(target, target_md5, miss_dependency_list)
652
+ dependency_start_time = Time.now
653
+ target_cache_dirs = Dir.glob(zabel_get_cache_root + "/" + target.name + "-" + target_md5 + "-*")
654
+ file_time_hash = {}
655
+ target_cache_dirs.each do | file |
656
+ file_time_hash[file] = File.mtime(file)
657
+ end
658
+ target_cache_dirs = target_cache_dirs.sort_by {|file| - file_time_hash[file].to_f}
659
+ potential_hit_target_cache_dirs = []
660
+ target_cache_dirs.each do | target_cache_dir |
661
+ next unless File.exist? target_cache_dir + "/" + FILE_NAME_PRODUCT
662
+ next unless File.exist? target_cache_dir + "/" + FILE_NAME_CONTEXT
663
+ target_context = YAML.load(File.read(target_cache_dir + "/" + FILE_NAME_CONTEXT))
664
+ dependency_miss = false
665
+ target_context[:dependency_files_md5].each do | item |
666
+ dependency_file = item[0]
667
+ dependency_md5 = item[1]
668
+
669
+ unless File.exist? dependency_file
670
+ miss_dependency_list.push "[XCCACHE/W] #{target.name} #{dependency_file} file should exist to be hit"
671
+ dependency_miss = true
672
+ break
673
+ end
674
+ unless zabel_get_file_md5(dependency_file) == dependency_md5
675
+ miss_dependency_list.push "[XCCACHE/W] #{target.name} #{dependency_file} md5 #{zabel_get_file_md5(dependency_file)} should match #{dependency_md5} to be hit"
676
+ dependency_miss = true
677
+ break
678
+ end
679
+ end
680
+ if not dependency_miss
681
+ if not target_context[:target_md5] == target_md5
682
+ command = "rm -rf \"#{target_cache_dir}\""
683
+ raise unless system command
684
+ puts "[XCCACHE/E] #{target.name} #{target_cache_dir} target md5 should match to be verified"
685
+ dependency_miss = false
686
+ next
687
+ end
688
+ if not target_context[:product_md5] == zabel_get_file_md5(target_cache_dir + "/" + FILE_NAME_PRODUCT)
689
+ command = "rm -rf \"#{target_cache_dir}\""
690
+ raise unless system command
691
+ puts "[XCCACHE/E] #{target.name} #{target_cache_dir} product md5 should match to be verified"
692
+ dependency_miss = false
693
+ next
694
+ end
695
+
696
+ potential_hit_target_cache_dirs.push target_cache_dir
697
+ if target_context[:dependency_targets_md5].size == 0
698
+ break
699
+ end
700
+ if potential_hit_target_cache_dirs.size > 10
701
+ break
702
+ end
703
+ end
704
+ end
705
+ return potential_hit_target_cache_dirs
706
+ end
707
+
708
+ def self.zabel_disable_build_and_inject_extract(project, target, target_context)
709
+ target_cache_dir = target_context[:hit_target_cache_dir]
710
+
711
+ # touch to update mtime
712
+ raise unless system "touch \"#{target_cache_dir}\""
713
+
714
+ # delete build phases to disable build command
715
+ target.build_phases.delete_if { | build_phase |
716
+ build_phase.class == Xcodeproj::Project::Object::PBXHeadersBuildPhase or
717
+ build_phase.class == Xcodeproj::Project::Object::PBXSourcesBuildPhase or
718
+ build_phase.class == Xcodeproj::Project::Object::PBXResourcesBuildPhase
719
+ }
720
+
721
+ # zabel_exec = "\"#{$0}\""
722
+ # if ENV["BUNDLE_BIN_PATH"] and ENV["BUNDLE_BIN_PATH"].size > 0 and ENV["BUNDLE_GEMFILE"] and ENV["BUNDLE_GEMFILE"].size > 0
723
+ # zabel_exec = "cd \"#{File.dirname(ENV["BUNDLE_GEMFILE"])}\" && \"#{ENV["BUNDLE_BIN_PATH"]}\" exe zabel"
724
+ # end
725
+ # extract_script = "#{zabel_exec} #{STAGE_EXTRACT} \"#{target_cache_dir}\" \"#{target_context[:build_product_dir]}\" \"#{target_context[:build_intermediate_dir]}\""
726
+
727
+ inject_phase = target.new_shell_script_build_phase("zabel_extract_#{target.name}")
728
+ inject_phase.shell_path = "/usr/bin/env bash -l"
729
+ inject_phase.shell_script = "export LC_ALL=en_US.UTF-8 \nexport LANG=en_US.UTF-8 \n\npod xcextract #{target_cache_dir}"
730
+ inject_phase.show_env_vars_in_log = '1'
731
+
732
+ puts "成功注入pod extract 脚本"
733
+ end
734
+
735
+ #####################################################################
736
+
737
+ def self.zabel_inject_printenv(project, target)
738
+ # zabel_exec = "\"#{$0}\""
739
+ # if ENV["BUNDLE_BIN_PATH"] and ENV["BUNDLE_BIN_PATH"].size > 0 and ENV["BUNDLE_GEMFILE"] and ENV["BUNDLE_GEMFILE"].size > 0
740
+ # zabel_exec = "cd \"#{File.dirname(ENV["BUNDLE_GEMFILE"])}\" && \"#{ENV["BUNDLE_BIN_PATH"]}\" exe zabel"
741
+ # end
742
+ # inject_phase = target.new_shell_script_build_phase("zabel_printenv_#{target.name}")
743
+ # inject_phase.shell_script = "#{zabel_exec} #{STAGE_PRINTENV} #{target.name} \"#{project.path}\""
744
+
745
+ zabel_exec = "export LC_ALL=en_US.UTF-8 \nexport LANG=en_US.UTF-8 \n\npod xcprintenv --targetname=#{target.name} --projectpath=#{project.path}"
746
+ inject_phase = target.new_shell_script_build_phase("zabel_printenv_#{target.name}")
747
+ inject_phase.shell_path = "/usr/bin/env bash -l"
748
+ inject_phase.shell_script = zabel_exec
749
+ inject_phase.show_env_vars_in_log = '1'
750
+ end
751
+
752
+ def self.zabel_pre(argv)
753
+
754
+ if argv.nil?
755
+ argv = {}
756
+ end
757
+
758
+ # configuration_name = nil
759
+ configuration_name = "Debug"
760
+
761
+ # if argv.include?("-configuration")
762
+ # configuration_name = argv[argv.index("-configuration") + 1]
763
+ # elsif argv.include?("--configuration")
764
+ # configuration_name = argv[argv.index("--configuration") + 1]
765
+ # end
766
+ # unless configuration_name and configuration_name.size > 0
767
+ # raise "[XCCACHE/E] -configuration or --configuration should be set"
768
+ # end
769
+
770
+ start_time = Time.now
771
+
772
+ if ENV["ZABEL_CLEAR_ALL"] == "YES"
773
+ command = "rm -rf \"#{zabel_get_cache_root}\""
774
+ puts command
775
+ raise unless system command
776
+ end
777
+
778
+ zabel_clean_temp_files
779
+
780
+ projects = zabel_get_projects
781
+
782
+ pre_targets_context = {}
783
+
784
+ hit_count = 0
785
+ miss_count = 0
786
+ hit_target_md5_cache_set = Set.new
787
+ iteration_count = 0
788
+
789
+ projects.each do | project |
790
+ project_configuration = project.build_configurations.detect { | config | config.name == configuration_name}
791
+ unless project_configuration
792
+ puts "[XCCACHE/E] #{project.path} should have config #{configuration_name}"
793
+ next
794
+ end
795
+ project.native_targets.each do | target |
796
+ if zabel_can_cache_target(target)
797
+ source_files = zabel_get_target_source_files(target)
798
+ unless source_files.size >= zabel_get_min_source_file_count
799
+ puts "[XCCACHE/I] skip #{target.name} #{source_files.size} < #{zabel_get_min_source_file_count}"
800
+ next
801
+ end
802
+ target_md5_content = zabel_get_target_md5_content(project, target, configuration_name, argv, source_files)
803
+ target_md5 = Digest::MD5.hexdigest(target_md5_content)
804
+ miss_dependency_list = []
805
+ potential_hit_target_cache_dirs = zabel_get_potential_hit_target_cache_dirs(target, target_md5, miss_dependency_list)
806
+
807
+ target_context = {}
808
+ target_context[:target_md5] = target_md5
809
+ target_context[:potential_hit_target_cache_dirs] = potential_hit_target_cache_dirs
810
+ target_context[:miss_dependency_list] = miss_dependency_list
811
+ if potential_hit_target_cache_dirs.size == 0
812
+ if miss_dependency_list.size > 0
813
+ puts miss_dependency_list.uniq.join("\n")
814
+ end
815
+ puts "[XCCACHE/I] miss #{target.name} #{target_md5} in iteration #{iteration_count}"
816
+ target_context[:target_status] = STATUS_MISS
817
+ miss_count = miss_count + 1
818
+ end
819
+ pre_targets_context[target] = target_context
820
+ end
821
+ end
822
+ end
823
+
824
+ while true
825
+ iteration_count = iteration_count + 1
826
+ confirm_count = hit_count + miss_count
827
+ projects.each do | project |
828
+ project.native_targets.each do | target |
829
+ next unless pre_targets_context.has_key? target
830
+ target_context = pre_targets_context[target]
831
+ next if target_context[:target_status] == STATUS_MISS
832
+ next if target_context[:target_status] == STATUS_HIT
833
+ potential_hit_target_cache_dirs = target_context[:potential_hit_target_cache_dirs]
834
+ next if potential_hit_target_cache_dirs.size == 0
835
+
836
+ hit_target_cache_dir = nil
837
+ potential_hit_target_cache_dirs.each do | target_cache_dir |
838
+ next unless File.exist? target_cache_dir + "/" + FILE_NAME_CONTEXT
839
+ hit_target_context = YAML.load(File.read(target_cache_dir + "/" + FILE_NAME_CONTEXT))
840
+ hit_target_cache_dir = target_cache_dir
841
+ hit_target_context[:dependency_targets_md5].each do | item |
842
+ dependency_target = item[0]
843
+ dependency_target_md5 = item[1]
844
+
845
+ # cycle dependency targets will be miss every time.
846
+ # TODO: to detect cycle dependency so that cache will not be added,
847
+ # or to hit cache together with some kind of algorithms.
848
+ unless hit_target_md5_cache_set.include? "#{dependency_target}-#{dependency_target_md5}"
849
+ hit_target_cache_dir = nil
850
+ break
851
+ end
852
+ end
853
+ if hit_target_cache_dir
854
+ target_context = target_context.merge!(hit_target_context)
855
+ break
856
+ end
857
+ end
858
+ if hit_target_cache_dir
859
+ puts "[XCCACHE/I] hit #{target.name} #{target_context[:target_md5]} in iteration #{iteration_count} potential #{potential_hit_target_cache_dirs.size}"
860
+ target_context[:target_status] = STATUS_HIT
861
+ target_context[:hit_target_cache_dir] = hit_target_cache_dir
862
+ hit_count = hit_count + 1
863
+ hit_target_md5_cache_set.add "#{target.name}-#{target_context[:target_md5]}"
864
+ end
865
+ end
866
+ end
867
+ if hit_count + miss_count == confirm_count
868
+ break
869
+ end
870
+ end
871
+
872
+ projects.each do | project |
873
+ should_save = false
874
+ project.native_targets.each do | target |
875
+ next unless pre_targets_context.has_key? target
876
+ target_context = pre_targets_context[target]
877
+
878
+ if target_context[:target_status] == STATUS_HIT
879
+ zabel_disable_build_and_inject_extract(project, target, target_context)
880
+ else
881
+ unless target_context[:target_status] == STATUS_MISS
882
+ target_context[:target_status] = STATUS_MISS
883
+ miss_dependency_list = target_context[:miss_dependency_list]
884
+ if miss_dependency_list.size > 0
885
+ puts miss_dependency_list.uniq.join("\n")
886
+ end
887
+ puts "[XCCACHE/I] miss #{target.name} #{target_context[:target_md5]} in iteration #{iteration_count}"
888
+ miss_count = miss_count + 1
889
+ end
890
+ zabel_inject_printenv(project, target)
891
+ end
892
+ File.write("#{project.path}/#{target.name}.#{FILE_NAME_TARGET_CONTEXT}", target_context.to_yaml)
893
+
894
+ should_save = true
895
+ end
896
+
897
+ if should_save
898
+ zabel_backup_project(project)
899
+ project.save
900
+ else
901
+ zabel_clean_backup_project(project)
902
+ end
903
+ end
904
+
905
+ #MARK: 给主工程注入执行脚本
906
+ zd_inject_main_target_shell
907
+
908
+ puts "[XCCACHE/I] total #{hit_count + miss_count} hit #{hit_count} miss #{miss_count} iteration #{iteration_count}"
909
+
910
+ puts "[XCCACHE/I] duration = #{(Time.now - start_time).to_i} s in stage pre"
911
+ end
912
+
913
+ def self.zabel_extract(target_cache_dir)
914
+ puts "[XCCACHE/D] #{Time.now.to_f.to_s}"
915
+
916
+ #target_cache_dir = ARGV[1]
917
+
918
+ cache_product_path = target_cache_dir + "/#{FILE_NAME_PRODUCT}"
919
+
920
+ start_time = Time.now
921
+
922
+ [BUILD_KEY_SYMROOT, BUILD_KEY_CONFIGURATION_BUILD_DIR, BUILD_KEY_OBJROOT, BUILD_KEY_TARGET_TEMP_DIR, BUILD_KEY_SRCROOT, BUILD_KEY_FULL_PRODUCT_NAME].sort.each do | key |
923
+ unless ENV.has_key? key and ENV[key] and ENV[key].size > 0
924
+ raise "[XCCACHE/E] #{target.name} should have #{key}"
925
+ break
926
+ end
927
+ end
928
+
929
+ if ENV[BUILD_KEY_CONFIGURATION_BUILD_DIR] != ENV[BUILD_KEY_TARGET_BUILD_DIR]
930
+ command = "mkdir -p \"#{ENV[BUILD_KEY_CONFIGURATION_BUILD_DIR]}\" && cd \"#{File.dirname(ENV[BUILD_KEY_CONFIGURATION_BUILD_DIR])}/\" && tar -xf \"#{cache_product_path}\""
931
+ puts command
932
+ raise unless system command
933
+
934
+ command = "rm -rf \"#{ENV[BUILD_KEY_TARGET_BUILD_DIR]+"/"+ENV[BUILD_KEY_FULL_PRODUCT_NAME]}\""
935
+ puts command
936
+ raise unless system command
937
+
938
+ command = "mkdir -p \"#{File.dirname(ENV[BUILD_KEY_TARGET_BUILD_DIR]+"/"+ENV[BUILD_KEY_FULL_PRODUCT_NAME])}\""
939
+ puts command
940
+ raise unless system command
941
+
942
+ command = "mv \"#{ENV[BUILD_KEY_CONFIGURATION_BUILD_DIR]+"/"+ENV[BUILD_KEY_FULL_PRODUCT_NAME]}\" \"#{ENV[BUILD_KEY_TARGET_BUILD_DIR]+"/"+ENV[BUILD_KEY_FULL_PRODUCT_NAME]}\""
943
+ puts command
944
+ raise unless system command
945
+
946
+ command = "/bin/ln -sfh \"#{ENV[BUILD_KEY_TARGET_BUILD_DIR]+"/"+ENV[BUILD_KEY_FULL_PRODUCT_NAME]}\" \"#{ENV[BUILD_KEY_CONFIGURATION_BUILD_DIR]+"/"+ENV[BUILD_KEY_FULL_PRODUCT_NAME]}\""
947
+ puts command
948
+ raise unless system command
949
+ else
950
+ command = "mkdir -p \"#{ENV[BUILD_KEY_CONFIGURATION_BUILD_DIR]}\" && cd \"#{File.dirname(ENV[BUILD_KEY_CONFIGURATION_BUILD_DIR])}/\" && tar -xf \"#{cache_product_path}\""
951
+ puts command
952
+ raise unless system command
953
+ end
954
+
955
+ puts "[XCCACHE/I] duration = #{(Time.now - start_time).to_i} s in stage extract"
956
+ puts "[XCCACHE/D] #{Time.now.to_f.to_s}"
957
+ end
958
+
959
+ def self.zabel_printenv(target_name, project_path)
960
+ # puts ARGV.to_s
961
+ # target_name = ARGV[1]
962
+ # project_path = ARGV[2]
963
+
964
+ target_context = YAML.load(File.read("#{project_path}/#{target_name}.#{FILE_NAME_TARGET_CONTEXT}"))
965
+
966
+ # see https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html
967
+ [BUILD_KEY_SYMROOT, BUILD_KEY_CONFIGURATION_BUILD_DIR, BUILD_KEY_OBJROOT, BUILD_KEY_TARGET_TEMP_DIR, BUILD_KEY_PODS_XCFRAMEWORKS_BUILD_DIR, BUILD_KEY_MODULEMAP_FILE, BUILD_KEY_SRCROOT, BUILD_KEY_FULL_PRODUCT_NAME].sort.each do | key |
968
+ if ENV[key]
969
+ target_context[key] = ENV[key]
970
+ end
971
+ end
972
+ target_context[:target_status] = STATUS_MISS_AND_READY
973
+ File.write("#{project_path}/#{target_name}.#{FILE_NAME_TARGET_CONTEXT}", target_context.to_yaml)
974
+ end
975
+
976
+ def self.zabel_clean
977
+ zabel_clean_temp_files
978
+ end
979
+
980
+
981
+ ################### 新加函数 ####################
982
+
983
+ # 获取主工程 和 主target
984
+ def self.zd_get_main_project_and_target
985
+ main_xcodeproj_path_arry = Dir.glob("*.xcodeproj")
986
+ if main_xcodeproj_path_arry.size == 0
987
+ main_xcodeproj_path_arry = Dir.glob("#{File.basename(Dir.pwd.to_s)}/*.xcodeproj")
988
+ end
989
+ if main_xcodeproj_path_arry.size == 0
990
+ puts "[XCCACHE/E 未找到 project]"
991
+ return nil
992
+ end
993
+
994
+ main_xcodeproj_path = main_xcodeproj_path_arry[0]
995
+ puts "[XCCACHE/I] 主工程路径 = #{main_xcodeproj_path}"
996
+
997
+ main_project = Xcodeproj::Project.open(main_xcodeproj_path)
998
+
999
+ main_project_name = File.basename(main_xcodeproj_path, ".*")
1000
+ main_target = nil
1001
+ main_project.native_targets.each do | target |
1002
+ if target.name == main_project_name
1003
+ main_target = target
1004
+ break
1005
+ end
1006
+ end
1007
+ puts "[XCCACHE/E 未找到 target]" unless main_project
1008
+
1009
+ return main_project, main_target
1010
+ end
1011
+
1012
+ # 在主工程中注入脚本
1013
+ def self.zd_inject_main_target_shell
1014
+ project, target = zd_get_main_project_and_target
1015
+
1016
+ main_target_shell_name = "#{MAIN_PROJECT_SHELL_NAME}_#{target.name}"
1017
+
1018
+ targetIndex = target.build_phases.index { |build_phase|
1019
+ build_phase.class == Xcodeproj::Project::Object::PBXShellScriptBuildPhase and build_phase.name == main_target_shell_name
1020
+ }
1021
+
1022
+ if targetIndex.nil?
1023
+ inject_phase = target.new_shell_script_build_phase("#{MAIN_PROJECT_SHELL_NAME}_#{target.name}")
1024
+ inject_phase.shell_path = "/usr/bin/env bash -l"
1025
+ inject_phase.shell_script = "export LC_ALL=en_US.UTF-8 \nexport LANG=en_US.UTF-8 \n\npod xccache"
1026
+ #inject_phase.run_only_for_deployment_postprocessing = "1"
1027
+ inject_phase.show_env_vars_in_log = '1'
1028
+
1029
+ puts "[XCCACHE/I] 在主target中注入 XCCache 脚本"
1030
+ else
1031
+ puts "[XCCACHE/I] 主target中已存在 XCCache 脚本"
1032
+ end
1033
+ project.save
1034
+ end
1035
+
1036
+ # 删除主工程注入的脚本
1037
+ def self.zd_delete_main_target_shell
1038
+ puts "[XCCACHE/I] 删除主工程注入的脚本"
1039
+ project, target = zd_get_main_project_and_target
1040
+ main_target_shell_name = "#{MAIN_PROJECT_SHELL_NAME}_#{target_name}"
1041
+
1042
+ target.build_phases.delete_if { | build_phase |
1043
+ build_phase.class == Xcodeproj::Project::Object::PBXShellScriptBuildPhase and build_phase.name == main_target_shell_name
1044
+ }
1045
+ project.save
1046
+ end
1047
+ end