zabel 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 40a25da43fd7d7baad52891f28711a40becb1fecc3994cc55012947a3533b7b4
4
+ data.tar.gz: d6e31ca241e61654e1ef27142be0c670c13d8f9391c3a81118b9cf73605ed87d
5
+ SHA512:
6
+ metadata.gz: 4d3c4baa573dc08a5c5515deaf48741f2d1b8c6df5865ba621484d7481af25bc91323bb6e9cac0343712eb973eda9f0d0b97399428cee0f790bd7a83bb937d7e
7
+ data.tar.gz: 3e406539303eda08035c1a0179d09c427827f12d7f08f3c41bd576edb1cfbf25159294df450dae9d0fb318fa190e26c0d88b488494464d953111e9eaf3e06242
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Pods
10
+ build
11
+ build-*
12
+ cache
13
+ logs
14
+ *.xcodeproj
15
+ *.xcworkspace
16
+ *.lock
17
+ *.log
18
+ *.gem
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at TODO: Write your email address. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 dengweijun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # Zabel
2
+
3
+ Zabel, is a build cacher for Xcode, using Xcodeproj and MD5, to detect and cache products for targets. Zabel is not Bazel.
4
+
5
+ ## Feature
6
+
7
+ - only support Cocoapods targets now
8
+ - support bundle / a / framework
9
+ - support C / C++ / Objective-C / Objective-C++ / Swift
10
+ - support Cocoapods option use_frameworks! :linkage => :dynamic and not
11
+ - support Cocoapods option use_frameworks! :linkage => :static and not
12
+ - support Cocoapods option use_modular_headers and not
13
+ - support Cocoapods option generate_multiple_pod_projects and not
14
+ - support prefix header and precompile prefix header
15
+ - support XCFrameworks
16
+ - support modulemap
17
+ - support development pods
18
+ - support different build path
19
+ - support dependent files and implicit dependent targets
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'zabel'
27
+ ```
28
+
29
+ And then execute:
30
+
31
+ $ bundle
32
+
33
+ Or install it yourself as:
34
+
35
+ $ gem install zabel
36
+
37
+ ## Usage
38
+
39
+ Simply add zabel before your xcodebuild command.
40
+
41
+ ```
42
+ pod install/update
43
+ zabel xcodebuild -workspace app.xcworkspace -scheme app -configuration Release -sdk iphonesimulator
44
+ ```
45
+
46
+ ## Advanced usage
47
+
48
+ You can controll your cache keys, which can be more or less.
49
+
50
+ ```
51
+ zabel pre -configuration Release -sdk iphonesimulator
52
+ xcodebuild -workspace app.xcworkspace -scheme app -configuration Release -sdk iphonesimulator
53
+ zabel post -configuration Release -sdk iphonesimulator
54
+ ```
55
+
56
+ ## Development
57
+
58
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
59
+
60
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
61
+
62
+ ## Test
63
+
64
+ ```bash
65
+ # test all cases
66
+ ruby test/all.rb
67
+ # test one case
68
+ ruby test/case/development_pods/test.rb
69
+ # test one todo case
70
+ ruby test/todo/modulemap_file/test.rb
71
+ ```
72
+
73
+ ## TODO
74
+
75
+ - support more projects and targets, not only Pods
76
+ - support and test more clang arguments
77
+ - support intermediate cache such as .o and .gcno
78
+ - try to support local development
79
+ - try to support remote cache server
80
+
81
+ ## Contributing
82
+
83
+ Bug reports and pull requests are welcome on GitHub at https://github.com/WeijunDeng/Zabel. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
84
+
85
+ ## License
86
+
87
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
88
+
89
+ ## Code of Conduct
90
+
91
+ Everyone interacting in the Zabel project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/WeijunDeng/Zabel/blob/master/CODE_OF_CONDUCT.md).
92
+
93
+ ## FAQ
94
+
95
+ Q: Must I set -configuration ?
96
+
97
+ A: Yes, for now.
98
+
99
+ Q: How to define a cache hit ?
100
+
101
+ A: A target cache will be hit only when it matches all arguments, settings, sources and dependencies.
102
+
103
+ Q: What will happen if a cache is hit ?
104
+
105
+ A: Firstly, PBXHeadersBuildPhase and PBXSourcesBuildPhase and PBXResourcesBuildPhase of a target will be deleted to disable build. Secondly, scripts to extract cache product will be added.
106
+
107
+ Q: What about scripts ?
108
+
109
+ A: All original PBXCopyFilesBuildPhase or PBXFrameworksBuildPhase or PBXShellScriptBuildPhase will not be deleted or changed. At most time, they did not take much time. However, they are difficult to be cached.
110
+
111
+ Q: What about dependencies ?
112
+
113
+ A: Simple dependent files (headers) and implicit dependent targets will be detected. If dependent files of a target change, this target will be recompiled. If dependent targets of a target miss cache, this target and dependent targets will be recompiled.
114
+
115
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "zabel"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/zabel ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'zabel'
4
+
5
+ if ARGV[0] == Zabel::STAGE_CLEAN
6
+ Zabel::zabel_clean
7
+ elsif ARGV[0] == Zabel::STAGE_EXTRACT
8
+ Zabel::zabel_extract
9
+ elsif ARGV[0] == Zabel::STAGE_PRINTENV
10
+ Zabel::zabel_printenv
11
+ elsif ARGV[0] == Zabel::STAGE_PRE
12
+ Zabel::zabel_pre(ARGV[1..-1])
13
+ elsif ARGV[0] == Zabel::STAGE_POST
14
+ Zabel::zabel_post(ARGV[1..-1])
15
+ elsif ARGV[0] and ARGV[0].include? "xcodebuild"
16
+ total_start_time = Time.now
17
+
18
+ Zabel::zabel_pre(ARGV)
19
+
20
+ build_start_time = Time.now
21
+ raise unless system(*ARGV)
22
+ puts "[ZABEL]<INFO> duration = #{(Time.now - build_start_time).to_i} s in stage build"
23
+
24
+ Zabel::zabel_post(ARGV)
25
+
26
+ puts "[ZABEL]<INFO> duration = #{(Time.now - total_start_time).to_i} s in stage all"
27
+ else
28
+ puts Zabel::VERSION
29
+ puts "https://github.com/WeijunDeng/Zabel"
30
+ end
data/lib/zabel.rb ADDED
@@ -0,0 +1,908 @@
1
+ require "zabel/version"
2
+
3
+ require 'xcodeproj'
4
+ require 'digest'
5
+ require 'set'
6
+ require 'open3'
7
+ require "find"
8
+ require 'yaml'
9
+ require 'pathname'
10
+
11
+ module Zabel
12
+ class Error < StandardError; end
13
+
14
+ BUILD_KEY_SYMROOT = "SYMROOT"
15
+ BUILD_KEY_TARGET_BUILD_DIR = "TARGET_BUILD_DIR"
16
+ BUILD_KEY_OBJROOT = "OBJROOT"
17
+ BUILD_KEY_TARGET_TEMP_DIR = "TARGET_TEMP_DIR"
18
+ BUILD_KEY_PODS_XCFRAMEWORKS_BUILD_DIR = "PODS_XCFRAMEWORKS_BUILD_DIR"
19
+ BUILD_KEY_MODULEMAP_FILE = "MODULEMAP_FILE"
20
+ BUILD_KEY_SRCROOT = "SRCROOT"
21
+ BUILD_KEY_WRAPPER_NAME = "WRAPPER_NAME"
22
+
23
+ STATUS_HIT = "hit"
24
+ STATUS_MISS = "miss"
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
+ def self.zabel_get_cache_root
38
+ cache_root = ENV["ZABEL_CACHE_ROOT"]
39
+ if cache_root and cache_root.size > 0
40
+ return cache_root
41
+ end
42
+
43
+ return Dir.home + "/zabel"
44
+ end
45
+
46
+ def self.zabel_get_cache_count
47
+ cache_count = ENV["ZABEL_CACHE_COUNT"]
48
+ if cache_count and cache_count.to_i.to_s == cache_count
49
+ return cache_count.to_i
50
+ end
51
+ return 10000
52
+ end
53
+
54
+ def self.zabel_should_not_detect_module_map_dependency
55
+ # By default, zabel detects module map dependency.
56
+ # However, there are bugs of xcodebuild or swift-frontend, which emits unnecessary and incorrect modulemap dependencies.
57
+ # To test by run "ruby test/todo/modulemap_file/test.rb"
58
+ # To avoid by set "export ZABEL_NOT_DETECT_MODULE_MAP_DEPENDENCY=YES"
59
+ zabel_should_not_detect_module_map_dependency = ENV["ZABEL_NOT_DETECT_MODULE_MAP_DEPENDENCY"]
60
+ if zabel_should_not_detect_module_map_dependency == "YES"
61
+ return true
62
+ end
63
+ return false
64
+ end
65
+
66
+ def self.zabel_get_min_source_file_count
67
+ # By default, zable caches targets which count of source files is greater than or equal 1.
68
+ # You can set this value to 0 or more than 1 to achieve higher speed.
69
+ min_source_file_count = ENV["ZABEL_MIN_SOURCE_FILE_COUNT"]
70
+ if min_source_file_count and min_source_file_count.to_i.to_s == min_source_file_count
71
+ return min_source_file_count.to_i
72
+ end
73
+ return 1
74
+ end
75
+
76
+ def self.zabel_should_extract_once
77
+ # By default, to achieve better compatibility, zabel extracts target cache ondemand,
78
+ # which means it depends on original dependencies of targets and it is in parallel.
79
+ # However, extracting once in a shell script build phase rather than multiple shell script build phases,
80
+ # is a little bit faster in some cases.
81
+ # You can enable this by set "export ZABEL_EXTRACT_ONCE=YES"
82
+ should_extract_once = ENV["ZABEL_EXTRACT_ONCE"]
83
+ if should_extract_once == "YES"
84
+ return true
85
+ end
86
+ return false
87
+ end
88
+
89
+ def self.zabel_get_projects
90
+ # TODO: to support more project, not only Pods
91
+ pods_project = Xcodeproj::Project.open("Pods/Pods.xcodeproj")
92
+ wrapper_project_paths = zabel_get_wrapper_project_paths(pods_project)
93
+ wrapper_projects = []
94
+ wrapper_project_paths.each do | path |
95
+ next if path.end_with? "Pods/Pods.xcodeproj"
96
+ project = Xcodeproj::Project.open(path)
97
+ wrapper_projects.push project
98
+ end
99
+ return (wrapper_projects + [pods_project])
100
+ end
101
+
102
+ def self.zabel_get_wrapper_project_paths(project)
103
+ wrapper_projects = project.files.select{|file|file.last_known_file_type=="wrapper.pb-project"}
104
+ wrapper_project_paths = []
105
+ wrapper_projects.each do | wrapper_project_file |
106
+ wrapper_project_file_path = wrapper_project_file.real_path.to_s
107
+ wrapper_project_paths.push wrapper_project_file_path
108
+ end
109
+ return wrapper_project_paths.uniq
110
+ end
111
+
112
+ def self.zabel_can_cache_target(target)
113
+ if target.name.start_with? "Pods-"
114
+ return false
115
+ end
116
+ if target.class == Xcodeproj::Project::Object::PBXNativeTarget
117
+ # see https://github.com/CocoaPods/Xcodeproj/blob/master/lib/xcodeproj/constants.rb#L145
118
+ if target.product_type == "com.apple.product-type.bundle" or
119
+ target.product_type == "com.apple.product-type.library.static" or
120
+ target.product_type == "com.apple.product-type.framework"
121
+ return true
122
+ end
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 "[ZABEL]<ERROR> #{target.name} #{file} should exist in dependency file #{dependency_file}"
141
+ return []
142
+ end
143
+
144
+ if file.start_with? intermediate_dir + "/" or
145
+ file.start_with? product_dir + "/" or
146
+ file.start_with? xcframeworks_build_dir + "/"
147
+ next
148
+ end
149
+
150
+ dependency_files.push file
151
+ end
152
+ end
153
+ return dependency_files.uniq
154
+ end
155
+
156
+ def self.zabel_get_target_source_files(target)
157
+ files = []
158
+ target.source_build_phase.files.each do | file |
159
+ file_path = file.file_ref.real_path.to_s
160
+ files.push file_path
161
+ end
162
+ target.headers_build_phase.files.each do | file |
163
+ file_path = file.file_ref.real_path.to_s
164
+ files.push file_path
165
+ end
166
+ target.resources_build_phase.files.each do | file |
167
+ file_path = file.file_ref.real_path.to_s
168
+ files.push file_path
169
+ end
170
+ expand_files = []
171
+ files.uniq.each do | file |
172
+ next unless File.exist? file
173
+ if File.file? file
174
+ expand_files.push file
175
+ else
176
+ Find.find(file).each do | file_in_dir |
177
+ if File.file? file_in_dir
178
+ expand_files.push file_in_dir
179
+ end
180
+ end
181
+ end
182
+ end
183
+ return expand_files.uniq
184
+ end
185
+
186
+ def self.zabel_get_content_without_pwd(content)
187
+ content = content.gsub("#{Dir.pwd}/", "").gsub(/#{Dir.pwd}(\W|$)/, '\1')
188
+ return content
189
+ end
190
+
191
+ $zabel_file_md5_hash = {}
192
+
193
+ def self.zabel_get_file_md5(file)
194
+ if $zabel_file_md5_hash.has_key? file
195
+ return $zabel_file_md5_hash[file]
196
+ end
197
+ md5 = Digest::MD5.hexdigest(File.read(file))
198
+ $zabel_file_md5_hash[file] = md5
199
+ return md5
200
+ end
201
+
202
+ def self.zabel_keep
203
+ file_list = Dir.glob("#{zabel_get_cache_root}/*")
204
+ file_time_hash = {}
205
+ file_list.each do | file |
206
+ file_time_hash[file] = File.mtime(file)
207
+ end
208
+ file_list = file_list.sort_by {|file| - file_time_hash[file].to_f}
209
+ puts "[ZABEL]<INFO> keep cache " + file_list.size.to_s + " " + Open3.capture3("du -sh #{zabel_get_cache_root}")[0].to_s
210
+
211
+ if file_list.size > 1
212
+ puts "[ZABEL]<INFO> keep oldest " + file_time_hash[file_list.last].to_s + " " + file_list.last
213
+ puts "[ZABEL]<INFO> keep newest " + file_time_hash[file_list.first].to_s + " " + file_list.first
214
+ end
215
+
216
+ if file_list.size > zabel_get_cache_count
217
+ file_list_remove = file_list[zabel_get_cache_count..(file_list.size-1)]
218
+ file_list_remove.each do | file |
219
+ raise unless system "rm -rf \"#{file}\""
220
+ end
221
+ end
222
+ end
223
+
224
+ def self.zabel_clean_backup_project(project)
225
+ command = "rm -rf \"#{project.path}/project.zabel_backup_pbxproj\""
226
+ raise unless system command
227
+ end
228
+
229
+
230
+ def self.zabel_backup_project(project)
231
+ command = "cp \"#{project.path}/project.pbxproj\" \"#{project.path}/project.zabel_backup_pbxproj\""
232
+ raise unless system command
233
+ end
234
+
235
+ def self.zabel_restore_project(project)
236
+ if File.exist? "#{project.path}/project.zabel_backup_pbxproj"
237
+ command = "mv \"#{project.path}/project.zabel_backup_pbxproj\" \"#{project.path}/project.pbxproj\""
238
+ raise unless system command
239
+ end
240
+ end
241
+
242
+ $zabel_podfile_spec_checksums = nil
243
+
244
+ def self.zabel_get_target_md5_content(project, target, configuration_name, argv, source_files)
245
+
246
+ unless $zabel_podfile_spec_checksums
247
+ if File.exist? "Podfile.lock"
248
+ podfile_lock = YAML.load(File.read("Podfile.lock"))
249
+ $zabel_podfile_spec_checksums = podfile_lock["SPEC CHECKSUMS"]
250
+ end
251
+ end
252
+
253
+ project_configuration = project.build_configurations.detect { | config | config.name == configuration_name}
254
+ project_configuration_content = project_configuration.pretty_print.to_yaml
255
+ project_xcconfig = ""
256
+ if project_configuration.base_configuration_reference
257
+ config_file_path = project_configuration.base_configuration_reference.real_path
258
+ if File.exist? config_file_path
259
+ project_xcconfig = File.read(config_file_path).lines.reject{|line|line.include? "_SEARCH_PATHS"}.sort.join("")
260
+ end
261
+ end
262
+
263
+ target_configuration = target.build_configurations.detect { | config | config.name == configuration_name}
264
+ target_configuration_content = target_configuration.pretty_print.to_yaml
265
+ target_xcconfig = ""
266
+ if target_configuration.base_configuration_reference
267
+ config_file_path = target_configuration.base_configuration_reference.real_path
268
+ if File.exist? config_file_path
269
+ target_xcconfig = File.read(config_file_path).lines.reject{|line|line.include? "_SEARCH_PATHS"}.sort.join("")
270
+ end
271
+ end
272
+
273
+ first_configuration = []
274
+ build_phases = []
275
+ build_phases.push target.source_build_phase if target.methods.include? :source_build_phase
276
+ build_phases.push target.resources_build_phase if target.methods.include? :resources_build_phase
277
+ build_phases.each do | build_phase |
278
+ target.source_build_phase.files_references.each do | files_reference |
279
+ files_reference.build_files.each do |build_file|
280
+ if build_file.settings and build_file.settings.class == Hash
281
+ first_configuration.push File.basename(build_file.file_ref.real_path.to_s) + "\n" + build_file.settings.to_yaml
282
+ end
283
+ end
284
+ end
285
+ end
286
+ first_configuration_content = first_configuration.sort.uniq.join("\n")
287
+
288
+ key_argv = []
289
+
290
+ # TODO: to add more and test more
291
+ # However, you can control your cache keys manually by using pre and post.
292
+ temp_path_list = ["-derivedDataPath", "-archivePath", "-exportPath", "-packageCachePath"]
293
+ argv.each_with_index do | arg, index |
294
+ next if temp_path_list.include? arg
295
+ next if index > 0 and temp_path_list.include? argv[index-1]
296
+ next if arg.start_with? "DSTROOT="
297
+ next if arg.start_with? "OBJROOT="
298
+ next if arg.start_with? "SYMROOT="
299
+ key_argv.push arg
300
+ end
301
+
302
+ source_md5_list = []
303
+ # zabel built-in verison, which will be changed for incompatibility in the future
304
+ source_md5_list.push "Version : #{Zabel::VERSION}"
305
+ source_md5_list.push "ARGV : #{key_argv.to_s}"
306
+
307
+ has_found_checksum = false
308
+ split_parts = target.name.split("-")
309
+ split_parts.each_with_index do | part, index |
310
+ spec_name = split_parts[0..index].join("-")
311
+ # TODO: to get a explicit spec name from a target.
312
+ # Now all potential spec names are push into md5 for safety.
313
+ if $zabel_podfile_spec_checksums.has_key? spec_name
314
+ source_md5_list.push "SPEC CHECKSUM : #{spec_name} #{$zabel_podfile_spec_checksums[spec_name]}"
315
+ has_found_checksum = true
316
+ end
317
+ end
318
+ unless has_found_checksum
319
+ puts "[ZABEL]<ERROR> #{target.name} SPEC CHECKSUM should be found"
320
+ end
321
+
322
+ source_md5_list.push "Project : #{File.basename(project.path)}"
323
+ source_md5_list.push "Project configuration : "
324
+ source_md5_list.push project_configuration_content.strip
325
+ source_md5_list.push "Project xcconfig : "
326
+ source_md5_list.push project_xcconfig.strip
327
+ source_md5_list.push "Target : #{target.name}"
328
+ source_md5_list.push "Target type : #{target.product_type}"
329
+ source_md5_list.push "Target configuration : "
330
+ source_md5_list.push target_configuration_content.strip
331
+ source_md5_list.push "Target xcconfig : "
332
+ source_md5_list.push target_xcconfig.strip
333
+ source_md5_list.push "Files settings : "
334
+ source_md5_list.push first_configuration_content.strip
335
+
336
+ source_md5_list.push "Files MD5 : "
337
+ source_files.uniq.sort.each do | file |
338
+ source_md5_list.push zabel_get_content_without_pwd(file) + " : " + zabel_get_file_md5(file)
339
+ end
340
+
341
+ source_md5_content = source_md5_list.join("\n")
342
+ return source_md5_content
343
+ end
344
+
345
+ def self.zabel_clean_temp_files
346
+ command = "rm -rf Pods/*.xcodeproj/project.zabel_backup_pbxproj"
347
+ puts command
348
+ raise unless system command
349
+
350
+ command = "rm -rf Pods/*.xcodeproj/*.#{FILE_NAME_TARGET_CONTEXT}"
351
+ puts command
352
+ raise unless system command
353
+
354
+ command = "rm -rf Pods/zabel.xcodeproj"
355
+ puts command
356
+ raise unless system command
357
+ end
358
+
359
+ def self.zabel_add_cache(target, target_context, message)
360
+ target_md5 = target_context[:target_md5]
361
+
362
+ product_dir = target_context[BUILD_KEY_TARGET_BUILD_DIR]
363
+ intermediate_dir = target_context[BUILD_KEY_TARGET_TEMP_DIR]
364
+ wrapper_name = target_context[BUILD_KEY_WRAPPER_NAME]
365
+
366
+ target_cache_dir = zabel_get_cache_root + "/" + target.name + "-" + target_md5 + "-" + (Time.now.to_f * 1000).to_i.to_s
367
+
368
+ Dir.glob("#{product_dir}/**/*.modulemap").each do | modulemap |
369
+ modulemap_content = File.read(modulemap)
370
+ if modulemap_content.include? File.dirname(modulemap) + "/"
371
+ modulemap_content = modulemap_content.gsub(File.dirname(modulemap) + "/", "")
372
+ File.write(modulemap, modulemap_content)
373
+ end
374
+ end
375
+
376
+ if target.product_type == "com.apple.product-type.library.static"
377
+ find_result = Open3.capture3("find #{product_dir}/*.a -maxdepth 0")
378
+ unless find_result[2] == 0 and find_result[0].lines.size == 1
379
+ puts "[ZABEL]<ERROR> #{target.name} #{product_dir}/*.a should exist"
380
+ return false
381
+ end
382
+ elsif target.product_type == "com.apple.product-type.bundle" or target.product_type == "com.apple.product-type.framework"
383
+ unless wrapper_name and wrapper_name.size > 0 and File.exist? "#{product_dir}/#{wrapper_name}"
384
+ puts "[ZABEL]<ERROR> #{target.name} #{product_dir}/#{wrapper_name} should exist"
385
+ return false
386
+ end
387
+ end
388
+
389
+ zip_start_time = Time.now
390
+
391
+ command = "cd \"#{File.dirname(product_dir)}\" && tar -cf #{target.name}.#{FILE_NAME_PRODUCT} #{File.basename(product_dir)}"
392
+ if target.product_type == "com.apple.product-type.library.static"
393
+ command = "cd \"#{File.dirname(product_dir)}\" && tar --exclude=*.bundle -cf #{target.name}.#{FILE_NAME_PRODUCT} #{File.basename(product_dir)}"
394
+ elsif target.product_type == 'com.apple.product-type.bundle'
395
+ if wrapper_name and wrapper_name.size > 0
396
+ command = "cd \"#{File.dirname(product_dir)}\" && tar -cf #{target.name}.#{FILE_NAME_PRODUCT} #{File.basename(product_dir)}/#{wrapper_name}"
397
+ else
398
+ puts "[ZABEL]<ERROR> #{target.name} WRAPPER_NAME should be found"
399
+ return false
400
+ end
401
+ end
402
+
403
+ puts command
404
+ unless system command
405
+ puts "[ZABEL]<ERROR> #{command} should succeed"
406
+ return false
407
+ end
408
+
409
+ if File.exist? target_cache_dir
410
+ puts "[ZABEL]<ERROR> #{target_cache_dir} should not exist"
411
+ raise unless system "rm -rf \"#{target_cache_dir}\""
412
+ return false
413
+ end
414
+
415
+ command = "mkdir -p \"#{target_cache_dir}\""
416
+ unless system command
417
+ puts command
418
+ puts "[ZABEL]<ERROR> #{command} should succeed"
419
+ return false
420
+ end
421
+
422
+ cache_product_path = target_cache_dir + "/#{FILE_NAME_PRODUCT}"
423
+
424
+ command = "mv \"#{File.dirname(product_dir)}/#{target.name}.#{FILE_NAME_PRODUCT}\" \"#{cache_product_path}\""
425
+ puts command
426
+ unless system command
427
+ puts command
428
+ puts "[ZABEL]<ERROR> #{command} should succeed"
429
+ return false
430
+ end
431
+ unless File.exist? cache_product_path
432
+ puts "[ZABEL]<ERROR> #{cache_product_path} should exist after mv"
433
+ return false
434
+ end
435
+
436
+ target_context[:product_md5] = zabel_get_file_md5(cache_product_path)
437
+ target_context[:target_build_dir_name] = target_context[BUILD_KEY_TARGET_BUILD_DIR].gsub(target_context[BUILD_KEY_SYMROOT] + "/", "")
438
+ target_context[:target_temp_dir_name] = target_context[BUILD_KEY_TARGET_TEMP_DIR].gsub(target_context[BUILD_KEY_OBJROOT] + "/", "")
439
+ if target_context[BUILD_KEY_MODULEMAP_FILE]
440
+ target_context[BUILD_KEY_MODULEMAP_FILE] = zabel_get_content_without_pwd target_context[BUILD_KEY_MODULEMAP_FILE]
441
+ end
442
+
443
+ target_context = target_context.clone
444
+ target_context.delete(:dependency_files)
445
+ target_context.delete(:target_status)
446
+ target_context.delete(:potential_hit_target_cache_dirs)
447
+ target_context.delete(:target_md5_content)
448
+ [BUILD_KEY_SYMROOT, BUILD_KEY_TARGET_BUILD_DIR, BUILD_KEY_OBJROOT, BUILD_KEY_TARGET_TEMP_DIR, BUILD_KEY_PODS_XCFRAMEWORKS_BUILD_DIR, BUILD_KEY_SRCROOT].each do | key |
449
+ target_context.delete(key)
450
+ end
451
+
452
+ File.write(target_cache_dir + "/" + FILE_NAME_CONTEXT, target_context.to_yaml)
453
+ File.write(target_cache_dir + "/" + FILE_NAME_MESSAGE, message)
454
+
455
+ return true
456
+ end
457
+
458
+ def self.zabel_post(argv)
459
+
460
+ unless argv.index("-configuration")
461
+ raise "[ZABEL]<ERROR> -configuration should be set"
462
+ end
463
+ configuration_name = argv[argv.index("-configuration") + 1]
464
+
465
+ start_time = Time.now
466
+
467
+ add_count = 0
468
+
469
+ projects = zabel_get_projects
470
+
471
+ post_targets_context = {}
472
+
473
+ projects.each do | project |
474
+ project.native_targets.each do | target |
475
+ if zabel_can_cache_target(target)
476
+
477
+ target_context_file = "#{project.path}/#{target.name}.#{FILE_NAME_TARGET_CONTEXT}"
478
+ unless File.exist? target_context_file
479
+ next
480
+ end
481
+
482
+ target_context = YAML.load(File.read(target_context_file))
483
+
484
+ if target_context[:target_status] == STATUS_MISS
485
+ source_files = zabel_get_target_source_files(target)
486
+
487
+ product_dir = target_context[BUILD_KEY_TARGET_BUILD_DIR]
488
+ intermediate_dir = target_context[BUILD_KEY_TARGET_TEMP_DIR]
489
+ xcframeworks_build_dir = target_context[BUILD_KEY_PODS_XCFRAMEWORKS_BUILD_DIR]
490
+
491
+ dependency_files = zabel_get_dependency_files(target, intermediate_dir, product_dir, xcframeworks_build_dir)
492
+ if source_files.size > 0 and dependency_files.size == 0 and target.product_type != "com.apple.product-type.bundle"
493
+ puts "[ZABEL]<ERROR> #{target.name} should have dependent files"
494
+ next
495
+ end
496
+ target_context[:dependency_files] = dependency_files - source_files
497
+ target_md5_content = zabel_get_target_md5_content(project, target, configuration_name, argv, source_files)
498
+ target_context[:target_md5_content] = target_md5_content
499
+ target_md5 = Digest::MD5.hexdigest(target_md5_content)
500
+ unless target_context[:target_md5] == target_md5
501
+ puts "[ZABEL]<ERROR> #{target.name} md5 should not be changed after build"
502
+ next
503
+ end
504
+ if target_context[BUILD_KEY_SRCROOT] and target_context[BUILD_KEY_SRCROOT].size > 0 and
505
+ target_context[BUILD_KEY_MODULEMAP_FILE] and target_context[BUILD_KEY_MODULEMAP_FILE].size > 0
506
+ if File.exist? Dir.pwd + "/" + zabel_get_content_without_pwd("#{target_context[BUILD_KEY_SRCROOT]}/#{target_context[BUILD_KEY_MODULEMAP_FILE]}")
507
+ target_context[BUILD_KEY_MODULEMAP_FILE] = zabel_get_content_without_pwd("#{target_context[BUILD_KEY_SRCROOT]}/#{target_context[BUILD_KEY_MODULEMAP_FILE]}")
508
+ else
509
+ puts "[ZABEL]<ERROR> #{target.name} #{target_context[BUILD_KEY_MODULEMAP_FILE]} should be supported"
510
+ next
511
+ end
512
+ end
513
+ elsif target_context[:target_status] == STATUS_HIT
514
+ if target_context[BUILD_KEY_MODULEMAP_FILE] and target_context[BUILD_KEY_MODULEMAP_FILE].size > 0
515
+ if not File.exist? Dir.pwd + "/" + target_context[BUILD_KEY_MODULEMAP_FILE]
516
+ puts "[ZABEL]<ERROR> #{target.name} #{target_context[BUILD_KEY_MODULEMAP_FILE]} should be supported"
517
+ next
518
+ end
519
+ end
520
+ else
521
+ puts "[ZABEL]<ERROR> #{target.name} should be hit or miss"
522
+ next
523
+ end
524
+
525
+ post_targets_context[target] = target_context
526
+ end
527
+ end
528
+ end
529
+
530
+ projects.each do | project |
531
+ project.native_targets.each do | target |
532
+ if post_targets_context.has_key? target
533
+ target_context = post_targets_context[target]
534
+ next unless target_context[:target_status] == STATUS_MISS
535
+
536
+ dependency_targets_set = Set.new
537
+ implicit_dependencies = []
538
+
539
+ post_targets_context.each do | other_target, other_target_context |
540
+ next if other_target == target
541
+
542
+ next if target.product_type == "com.apple.product-type.bundle"
543
+ next if other_target.product_type == "com.apple.product-type.bundle"
544
+
545
+ target_context[:dependency_files].each do | dependency |
546
+
547
+ if other_target_context[BUILD_KEY_TARGET_BUILD_DIR] and other_target_context[BUILD_KEY_TARGET_BUILD_DIR].size > 0 and
548
+ dependency.start_with? other_target_context[BUILD_KEY_TARGET_BUILD_DIR] + "/"
549
+ dependency_targets_set.add other_target
550
+ implicit_dependencies.push dependency
551
+ elsif other_target_context[BUILD_KEY_TARGET_TEMP_DIR] and other_target_context[BUILD_KEY_TARGET_TEMP_DIR].size > 0 and
552
+ dependency.start_with? other_target_context[BUILD_KEY_TARGET_TEMP_DIR] + "/"
553
+ dependency_targets_set.add other_target
554
+ implicit_dependencies.push dependency
555
+ elsif other_target_context[:target_build_dir_name] and other_target_context[:target_build_dir_name].size > 0 and
556
+ dependency.start_with? target_context[BUILD_KEY_SYMROOT] + "/" + other_target_context[:target_build_dir_name] + "/"
557
+ dependency_targets_set.add other_target
558
+ implicit_dependencies.push dependency
559
+ elsif other_target_context[:target_temp_dir_name] and other_target_context[:target_temp_dir_name].size > 0 and
560
+ dependency.start_with? target_context[BUILD_KEY_OBJROOT] + "/" + other_target_context[:target_temp_dir_name] + "/"
561
+ dependency_targets_set.add other_target
562
+ implicit_dependencies.push dependency
563
+ end
564
+
565
+ unless zabel_should_not_detect_module_map_dependency
566
+ if other_target_context[BUILD_KEY_MODULEMAP_FILE] and other_target_context[BUILD_KEY_MODULEMAP_FILE].size > 0 and
567
+ dependency == Dir.pwd + "/" + other_target_context[BUILD_KEY_MODULEMAP_FILE]
568
+ dependency_targets_set.add other_target
569
+ end
570
+ end
571
+ end
572
+
573
+ target_context[:dependency_files] = target_context[:dependency_files] - implicit_dependencies
574
+
575
+ end
576
+
577
+ target_context[:dependency_files] = target_context[:dependency_files] - implicit_dependencies
578
+ dependency_files_md5 = []
579
+ target_context[:dependency_files].each do | file |
580
+ dependency_files_md5.push [zabel_get_content_without_pwd(file), zabel_get_file_md5(file)]
581
+ end
582
+ target_context[:dependency_files_md5] = dependency_files_md5.sort.uniq
583
+
584
+ dependency_targets_md5 = dependency_targets_set.to_a.map { | target | [target.name, post_targets_context[target][:target_md5]]}
585
+ target_context[:dependency_targets_md5] = dependency_targets_md5
586
+
587
+ message = target_context[:target_md5_content]
588
+
589
+ if zabel_add_cache(target, target_context, message)
590
+ add_count = add_count + 1
591
+ end
592
+ end
593
+ end
594
+ end
595
+
596
+ projects.each do | project |
597
+ zabel_restore_project(project)
598
+ end
599
+
600
+ zabel_keep
601
+
602
+ puts "[ZABEL]<INFO> total add #{add_count}"
603
+
604
+ puts "[ZABEL]<INFO> duration = #{(Time.now - start_time).to_i} s in stage post"
605
+
606
+ end
607
+
608
+ def self.zabel_get_potential_hit_target_cache_dirs(target, target_md5)
609
+ dependency_start_time = Time.now
610
+ target_cache_dirs = Dir.glob(zabel_get_cache_root + "/" + target.name + "-" + target_md5 + "-*")
611
+ file_time_hash = {}
612
+ target_cache_dirs.each do | file |
613
+ file_time_hash[file] = File.mtime(file)
614
+ end
615
+ target_cache_dirs = target_cache_dirs.sort_by {|file| - file_time_hash[file].to_f}
616
+ potential_hit_target_cache_dirs = []
617
+ target_cache_dirs.each do | target_cache_dir |
618
+ next unless File.exist? target_cache_dir + "/" + FILE_NAME_PRODUCT
619
+ next unless File.exist? target_cache_dir + "/" + FILE_NAME_CONTEXT
620
+ target_context = YAML.load(File.read(target_cache_dir + "/" + FILE_NAME_CONTEXT))
621
+ dependency_miss = false
622
+ target_context[:dependency_files_md5].each do | item |
623
+ dependency_file = item[0]
624
+ dependency_md5 = item[1]
625
+
626
+ unless File.exist? dependency_file
627
+ puts "[ZABEL]<WARNING> #{target.name} #{dependency_file} file should exist to be hit"
628
+ dependency_miss = true
629
+ break
630
+ end
631
+ unless zabel_get_file_md5(dependency_file) == dependency_md5
632
+ puts "[ZABEL]<WARNING> #{target.name} #{dependency_file} md5 should match to be hit"
633
+ dependency_miss = true
634
+ break
635
+ end
636
+ end
637
+ if not dependency_miss
638
+ if not target_context[:target_md5] == target_md5
639
+ command = "rm -rf \"#{target_cache_dir}\""
640
+ raise unless system command
641
+ puts "[ZABEL]<ERROR> #{target.name} #{target_cache_dir} target md5 should match to be verified"
642
+ dependency_miss = false
643
+ next
644
+ end
645
+ if not target_context[:product_md5] == zabel_get_file_md5(target_cache_dir + "/" + FILE_NAME_PRODUCT)
646
+ command = "rm -rf \"#{target_cache_dir}\""
647
+ raise unless system command
648
+ puts "[ZABEL]<ERROR> #{target.name} #{target_cache_dir} product md5 should match to be verified"
649
+ dependency_miss = false
650
+ next
651
+ end
652
+
653
+ potential_hit_target_cache_dirs.push target_cache_dir
654
+ if target_context[:dependency_targets_md5].size == 0
655
+ break
656
+ end
657
+ if potential_hit_target_cache_dirs.size > 10
658
+ break
659
+ end
660
+ end
661
+ end
662
+ return potential_hit_target_cache_dirs
663
+ end
664
+
665
+ # see https://github.com/CocoaPods/Xcodeproj/blob/master/lib/xcodeproj/project/object/native_target.rb#L239
666
+ # and this is faster, without searching deeply.
667
+ def self.zabel_fast_add_dependency(project, target_target, target, subproject_reference)
668
+ container_proxy = project.new(Xcodeproj::Project::PBXContainerItemProxy)
669
+ container_proxy.container_portal = subproject_reference.uuid
670
+ container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:native_target]
671
+ container_proxy.remote_global_id_string = target.uuid
672
+ container_proxy.remote_info = target.name
673
+
674
+ dependency = project.new(Xcodeproj::Project::PBXTargetDependency)
675
+ dependency.name = target.name
676
+ dependency.target_proxy = container_proxy
677
+
678
+ target_target.dependencies << dependency
679
+ end
680
+
681
+ def self.zabel_disable_build_and_inject_extract(project, target, inject_project, inject_target, inject_scripts, target_context)
682
+ target_cache_dir = target_context[:hit_target_cache_dir]
683
+
684
+ # touch to update mtime
685
+ raise unless system "touch \"#{target_cache_dir}\""
686
+
687
+ # delete build phases to disable build command
688
+ target.build_phases.delete_if { | build_phase |
689
+ build_phase.class == Xcodeproj::Project::Object::PBXHeadersBuildPhase or
690
+ build_phase.class == Xcodeproj::Project::Object::PBXSourcesBuildPhase or
691
+ build_phase.class == Xcodeproj::Project::Object::PBXResourcesBuildPhase
692
+ }
693
+
694
+ extract_script = "zabel #{STAGE_EXTRACT} \"#{target_cache_dir}\" \"#{target_context[:target_build_dir_name]}\" \"#{target_context[:target_temp_dir_name]}\""
695
+
696
+ if zabel_should_extract_once
697
+ subproject_reference = nil
698
+ project.main_group.files.each do | file |
699
+ if file.class == Xcodeproj::Project::Object::PBXFileReference and File.basename(file.path) == File.basename(inject_project.path)
700
+ subproject_reference = file
701
+ break
702
+ end
703
+ end
704
+
705
+ unless subproject_reference
706
+ subproject_reference = project.main_group.new_reference(inject_project.path, :group)
707
+ end
708
+
709
+ zabel_fast_add_dependency(project, target, inject_target, subproject_reference)
710
+
711
+ inject_scripts.push extract_script
712
+ else
713
+ inject_phase = target.new_shell_script_build_phase("zabel_extract_#{target.name}")
714
+ inject_phase.shell_script = extract_script
715
+ inject_phase.show_env_vars_in_log = '0'
716
+ end
717
+ end
718
+
719
+ def self.zabel_inject_printenv(project, target)
720
+ inject_phase = target.new_shell_script_build_phase("zabel_printenv_#{target.name}")
721
+ inject_phase.shell_script = "zabel #{STAGE_PRINTENV} #{target.name} \"#{project.path}\""
722
+ inject_phase.show_env_vars_in_log = '0'
723
+ end
724
+
725
+ def self.zabel_pre(argv)
726
+
727
+ unless argv.index("-configuration")
728
+ raise "[ZABEL]<ERROR> -configuration should be set"
729
+ end
730
+ configuration_name = argv[argv.index("-configuration") + 1]
731
+
732
+ start_time = Time.now
733
+
734
+ if ENV["ZABEL_CLEAR_ALL"] == "YES"
735
+ command = "rm -rf \"#{zabel_get_cache_root}\""
736
+ puts command
737
+ raise unless system command
738
+ end
739
+
740
+ zabel_clean_temp_files
741
+
742
+ if zabel_should_extract_once
743
+ inject_project = Xcodeproj::Project.new("Pods/zabel.xcodeproj")
744
+ inject_target = inject_project.new_aggregate_target("zabel")
745
+ inject_phase = inject_target.new_shell_script_build_phase("zabel_extract")
746
+ inject_phase.show_env_vars_in_log = '0'
747
+ inject_project.save
748
+ inject_scripts = []
749
+ end
750
+
751
+ projects = zabel_get_projects
752
+
753
+ pre_targets_context = {}
754
+
755
+ hit_count = 0
756
+ miss_count = 0
757
+ hit_target_md5_cache_set = Set.new
758
+ iteration_count = 0
759
+
760
+ projects.each do | project |
761
+ project.native_targets.each do | target |
762
+ if zabel_can_cache_target(target)
763
+ source_files = zabel_get_target_source_files(target)
764
+ next unless source_files.size >= zabel_get_min_source_file_count
765
+ target_md5_content = zabel_get_target_md5_content(project, target, configuration_name, argv, source_files)
766
+ target_md5 = Digest::MD5.hexdigest(target_md5_content)
767
+ potential_hit_target_cache_dirs = zabel_get_potential_hit_target_cache_dirs(target, target_md5)
768
+
769
+ target_context = {}
770
+ target_context[:target_md5] = target_md5
771
+ target_context[:potential_hit_target_cache_dirs] = potential_hit_target_cache_dirs
772
+ if potential_hit_target_cache_dirs.size == 0
773
+ puts "[ZABEL]<INFO> miss #{target.name} #{target_md5} in iteration #{iteration_count}"
774
+ target_context[:target_status] = STATUS_MISS
775
+ miss_count = miss_count + 1
776
+ end
777
+ pre_targets_context[target] = target_context
778
+ end
779
+ end
780
+ end
781
+
782
+ while true
783
+ iteration_count = iteration_count + 1
784
+ confirm_count = hit_count + miss_count
785
+ projects.each do | project |
786
+ project.native_targets.each do | target |
787
+ next unless pre_targets_context.has_key? target
788
+ target_context = pre_targets_context[target]
789
+ next if target_context[:target_status] == STATUS_MISS
790
+ next if target_context[:target_status] == STATUS_HIT
791
+ potential_hit_target_cache_dirs = target_context[:potential_hit_target_cache_dirs]
792
+ next if potential_hit_target_cache_dirs.size == 0
793
+
794
+ hit_target_cache_dir = nil
795
+ potential_hit_target_cache_dirs.each do | target_cache_dir |
796
+ next unless File.exist? target_cache_dir + "/" + FILE_NAME_CONTEXT
797
+ hit_target_context = YAML.load(File.read(target_cache_dir + "/" + FILE_NAME_CONTEXT))
798
+ hit_target_cache_dir = target_cache_dir
799
+ hit_target_context[:dependency_targets_md5].each do | item |
800
+ dependency_target = item[0]
801
+ dependency_target_md5 = item[1]
802
+
803
+ # cycle dependency targets will be miss every time.
804
+ # TODO: to detect cycle dependency so that cache will not be added,
805
+ # or to hit cache together with some kind of algorithms.
806
+ unless hit_target_md5_cache_set.include? "#{dependency_target}-#{dependency_target_md5}"
807
+ hit_target_cache_dir = nil
808
+ break
809
+ end
810
+ end
811
+ if hit_target_cache_dir
812
+ target_context = target_context.merge!(hit_target_context)
813
+ break
814
+ end
815
+ end
816
+ if hit_target_cache_dir
817
+ puts "[ZABEL]<INFO> hit #{target.name} #{target_context[:target_md5]} in iteration #{iteration_count} potential #{potential_hit_target_cache_dirs.size}"
818
+ target_context[:target_status] = STATUS_HIT
819
+ target_context[:hit_target_cache_dir] = hit_target_cache_dir
820
+ hit_count = hit_count + 1
821
+ hit_target_md5_cache_set.add "#{target.name}-#{target_context[:target_md5]}"
822
+ end
823
+ end
824
+ end
825
+ if hit_count + miss_count == confirm_count
826
+ break
827
+ end
828
+ end
829
+
830
+ projects.each do | project |
831
+ should_save = false
832
+ project.native_targets.each do | target |
833
+ next unless pre_targets_context.has_key? target
834
+ target_context = pre_targets_context[target]
835
+
836
+ if target_context[:target_status] == STATUS_HIT
837
+ zabel_disable_build_and_inject_extract(project, target, inject_project, inject_target, inject_scripts, target_context)
838
+ else
839
+ unless target_context[:target_status] == STATUS_MISS
840
+ target_context[:target_status] = STATUS_MISS
841
+ puts "[ZABEL]<INFO> miss #{target.name} #{target_context[:target_md5]} in iteration #{iteration_count}"
842
+ miss_count = miss_count + 1
843
+ end
844
+ zabel_inject_printenv(project, target)
845
+ end
846
+ File.write("#{project.path}/#{target.name}.#{FILE_NAME_TARGET_CONTEXT}", target_context.to_yaml)
847
+
848
+ should_save = true
849
+ end
850
+
851
+ if should_save
852
+ zabel_backup_project(project)
853
+ project.save
854
+ else
855
+ zabel_clean_backup_project(project)
856
+ end
857
+ end
858
+
859
+ if zabel_should_extract_once and inject_scripts.size > 0
860
+ inject_scripts = (["startTime_s=`date +%s`"] + inject_scripts + ["echo \"[ZABEL]<INFO> duration = $[ `date +%s` - $startTime_s ] s in stage #{STAGE_EXTRACT}\""]).flatten
861
+ inject_phase.shell_script = inject_scripts.join("\n")
862
+ inject_project.save
863
+ end
864
+
865
+ puts "[ZABEL]<INFO> total #{hit_count + miss_count} hit #{hit_count} miss #{miss_count} iteration #{iteration_count}"
866
+
867
+ puts "[ZABEL]<INFO> duration = #{(Time.now - start_time).to_i} s in stage pre"
868
+ end
869
+
870
+ def self.zabel_extract
871
+ target_cache_dir = ARGV[1]
872
+ product_path = ARGV[2]
873
+ intermediate_path = ARGV[3]
874
+
875
+ cache_product_path = target_cache_dir + "/#{FILE_NAME_PRODUCT}"
876
+
877
+ start_time = Time.now
878
+ command = "mkdir -p \"#{ENV[BUILD_KEY_SYMROOT]}/#{product_path}\" && cd \"#{ENV[BUILD_KEY_SYMROOT]}/#{product_path}/..\" && tar -xf \"#{cache_product_path}\""
879
+ puts command
880
+ raise unless system command
881
+
882
+ end
883
+
884
+ def self.zabel_printenv
885
+ target_name = ARGV[1]
886
+ project_path = ARGV[2]
887
+
888
+ target_context = YAML.load(File.read("#{project_path}/#{target_name}.#{FILE_NAME_TARGET_CONTEXT}"))
889
+
890
+ # see https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html
891
+ [BUILD_KEY_SYMROOT, BUILD_KEY_TARGET_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_WRAPPER_NAME].sort.each do | key |
892
+ if ENV[key]
893
+ target_context[key] = ENV[key]
894
+ end
895
+ end
896
+ File.write("#{project_path}/#{target_name}.#{FILE_NAME_TARGET_CONTEXT}", target_context.to_yaml)
897
+ end
898
+
899
+ def self.zabel_clean
900
+ if File.exist? "Pods/zabel.xcodeproj"
901
+ command = "rm -rf Pods/*.xcodeproj"
902
+ puts command
903
+ raise unless system command
904
+ end
905
+ zabel_clean_temp_files
906
+ end
907
+
908
+ end