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 +7 -0
- data/.gitignore +18 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/zabel +30 -0
- data/lib/zabel.rb +908 -0
- data/lib/zabel/version.rb +3 -0
- data/zabel.gemspec +41 -0
- metadata +103 -0
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
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
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
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
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
|