pod-binary-pack 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/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +147 -0
- data/exe/pod-binary-pack +6 -0
- data/lib/core/auto_build.rb +299 -0
- data/lib/core/build_item.rb +69 -0
- data/lib/core/cli.rb +442 -0
- data/lib/core/commit.rb +19 -0
- data/lib/core/config.rb +56 -0
- data/lib/core/cost.rb +68 -0
- data/lib/core/git_helper.rb +222 -0
- data/lib/core/github_fork.rb +425 -0
- data/lib/core/github_helper.rb +334 -0
- data/lib/core/hook_manager.rb +32 -0
- data/lib/core/hosting/gitee_client.rb +114 -0
- data/lib/core/hosting/gitee_provider.rb +312 -0
- data/lib/core/hosting/gitlab_provider.rb +342 -0
- data/lib/core/hosting/provider.rb +96 -0
- data/lib/core/hosting.rb +43 -0
- data/lib/core/logger.rb +66 -0
- data/lib/core/podspec.rb +126 -0
- data/lib/core/push.rb +80 -0
- data/lib/core/repo.rb +284 -0
- data/lib/core/repo_cocoapods.rb +64 -0
- data/lib/core/repo_module.rb +281 -0
- data/lib/core/repo_module_binary.rb +439 -0
- data/lib/core/repo_module_source.rb +506 -0
- data/lib/core/repo_xcode.rb +41 -0
- data/lib/core/repo_xcode_package.rb +713 -0
- data/lib/core/repo_xcode_shell.rb +170 -0
- data/lib/core/util/file_helper.rb +100 -0
- data/lib/core/util/string.rb +35 -0
- data/lib/pod/binary_pack/version.rb +7 -0
- data/lib/pod/binary_pack.rb +9 -0
- metadata +144 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'shellwords'
|
|
3
|
+
require_relative 'push'
|
|
4
|
+
|
|
5
|
+
module Build
|
|
6
|
+
class GitHelper
|
|
7
|
+
|
|
8
|
+
# @return [Boolean] 返回是否有变更
|
|
9
|
+
def GitHelper.status(path)
|
|
10
|
+
current = Dir.pwd
|
|
11
|
+
Dir.chdir(path)
|
|
12
|
+
Build.log.debug("获取代码状态(#{File.basename(path)}): git status")
|
|
13
|
+
res = %x(git status)
|
|
14
|
+
Dir.chdir(current)
|
|
15
|
+
if res.include? 'nothing to commit, working tree clean'
|
|
16
|
+
Build.log.debug("当前无变更")
|
|
17
|
+
return false
|
|
18
|
+
end
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def GitHelper.add_stag(path)
|
|
23
|
+
command = "git add ."
|
|
24
|
+
GitHelper.execute(path, command, true)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# 仅暂存指定文件(避免把构建产物/临时文件一并提交)
|
|
28
|
+
def GitHelper.add_files(path, files)
|
|
29
|
+
list = Array(files).map { |f| Shellwords.escape(f) }.join(' ')
|
|
30
|
+
GitHelper.execute(path, "git add -- #{list}", true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 指定文件相对HEAD是否有改动(含修改/新增/删除)
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def GitHelper.file_changed?(path, file)
|
|
36
|
+
res = GitHelper.capture(path, "git status --porcelain -- #{Shellwords.escape(file)}")
|
|
37
|
+
!res.strip.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def GitHelper.commit(path, commit_msg = nil)
|
|
41
|
+
commit_msg = commit_msg ? commit_msg : "none message"
|
|
42
|
+
# commit_msg含上游committer/commit信息(任意字符),必须转义避免引号断裂命令或注入
|
|
43
|
+
command = "git commit -m #{Shellwords.escape(commit_msg)}"
|
|
44
|
+
GitHelper.execute(path, command, true)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def GitHelper.pull(path)
|
|
48
|
+
command = "git pull"
|
|
49
|
+
GitHelper.execute(path, command, true)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def GitHelper.push(path, branch)
|
|
53
|
+
command = "git push origin #{Shellwords.escape(branch)}"
|
|
54
|
+
GitHelper.execute(path, command, true)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def GitHelper.push_f(path, branch)
|
|
58
|
+
command = "git push origin -f #{Shellwords.escape(branch)}"
|
|
59
|
+
GitHelper.execute(path, command, true)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def GitHelper.reset_hard(path)
|
|
63
|
+
command = "git reset --hard"
|
|
64
|
+
GitHelper.execute(path, command, true)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def GitHelper.fetch(path)
|
|
68
|
+
command = "git fetch"
|
|
69
|
+
GitHelper.execute(path, command, true)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def GitHelper.checkout(path, checkout_branch)
|
|
73
|
+
command = "git checkout #{Shellwords.escape(checkout_branch)}"
|
|
74
|
+
GitHelper.execute(path, command, true)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def GitHelper.tag_delete_local(path, tag)
|
|
78
|
+
command = "git tag -d #{Shellwords.escape(tag)}"
|
|
79
|
+
GitHelper.execute(path, command, false)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def GitHelper.tag_delete_origin(path, tag)
|
|
83
|
+
command = "git push origin :refs/tags/#{Shellwords.escape(tag)}"
|
|
84
|
+
GitHelper.execute(path, command, true)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def GitHelper.tag_local(path, tag)
|
|
88
|
+
command = "git tag #{Shellwords.escape(tag)}"
|
|
89
|
+
GitHelper.execute(path, command, true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def GitHelper.tag_origin(path, tag)
|
|
93
|
+
command = "git push origin #{Shellwords.escape(tag)}"
|
|
94
|
+
GitHelper.execute(path, command, true)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def GitHelper.push_tags(path)
|
|
98
|
+
command = "git push origin --tags"
|
|
99
|
+
GitHelper.execute(path, command, true)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# 远端是否存在指定分支
|
|
103
|
+
# @return [Boolean]
|
|
104
|
+
def GitHelper.remote_branch?(path, branch)
|
|
105
|
+
res = GitHelper.capture(path, "git ls-remote --heads origin #{Shellwords.escape(branch)}")
|
|
106
|
+
!res.strip.empty?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 解析某个引用的commit sha,解析失败返回nil
|
|
110
|
+
# @return [String, nil]
|
|
111
|
+
def GitHelper.revision(path, ref)
|
|
112
|
+
res = GitHelper.capture(path, "git rev-parse --verify #{Shellwords.escape(ref)}^{commit} 2>/dev/null")
|
|
113
|
+
res = res.strip
|
|
114
|
+
res.empty? ? nil : res
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# 判断ancestor是否为descendant的祖先(用于fast-forward可行性判断)
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def GitHelper.ancestor?(path, ancestor, descendant)
|
|
120
|
+
GitHelper.run_bool(path, "git merge-base --is-ancestor #{Shellwords.escape(ancestor)} #{Shellwords.escape(descendant)}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# 基于远端分支创建/重置本地分支并切换: git checkout -B branch origin/branch
|
|
124
|
+
def GitHelper.checkout_tracking(path, branch)
|
|
125
|
+
command = "git checkout -B #{Shellwords.escape(branch)} origin/#{Shellwords.escape(branch)}"
|
|
126
|
+
GitHelper.execute(path, command, true)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# fast-forward合并到指定引用,不能ff时命令失败
|
|
130
|
+
def GitHelper.merge_ff_only(path, ref)
|
|
131
|
+
command = "git merge --ff-only #{Shellwords.escape(ref)}"
|
|
132
|
+
GitHelper.execute(path, command, true)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# 把当前HEAD推送为远端指定分支名(用于远端目标分支不存在时创建)
|
|
136
|
+
def GitHelper.push_head_as(path, remote_branch)
|
|
137
|
+
command = "git push origin HEAD:#{Shellwords.escape(remote_branch)}"
|
|
138
|
+
GitHelper.execute(path, command, true)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ===== http归档git化专用(http归档组件在fork阶段就地建成全新git仓库) =====
|
|
142
|
+
|
|
143
|
+
# 初始化全新git仓库并写入本地提交者配置,不依赖机器全局git配置;
|
|
144
|
+
# 不指定初始分支名(兼容低版本git无init -b),推送时用 HEAD:master 显式对齐远端分支名
|
|
145
|
+
def GitHelper.init_repo(path)
|
|
146
|
+
GitHelper.execute(path, "git init", true)
|
|
147
|
+
GitHelper.execute(path, "git config user.name jenkins", true)
|
|
148
|
+
GitHelper.execute(path, "git config user.email jenkins@jenkins.com", true)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# 全新仓库添加origin远程地址
|
|
152
|
+
def GitHelper.remote_add(path, name, url)
|
|
153
|
+
command = "git remote add #{name} #{Shellwords.escape(url)}"
|
|
154
|
+
GitHelper.execute(path, command, true)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# 当前HEAD强推为远端master分支(全新git化仓库推送,不依赖本地分支名)
|
|
158
|
+
def GitHelper.push_head_master_f(path)
|
|
159
|
+
command = "git push origin -f HEAD:master"
|
|
160
|
+
GitHelper.execute(path, command, true)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# 强推指定tag(同名tag指向新commit时的覆盖推送)
|
|
164
|
+
def GitHelper.tag_origin_f(path, tag)
|
|
165
|
+
command = "git push origin -f #{tag}"
|
|
166
|
+
GitHelper.execute(path, command, true)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# 获取远端仓库全部tag名(ls-remote直查远端,不依赖本地克隆,剥离 ^{} 剥离引用)。
|
|
170
|
+
# 仓库不存在/不可达时返回nil,空仓库返回[]
|
|
171
|
+
# @return [Array<String>, nil]
|
|
172
|
+
def GitHelper.remote_tags(url)
|
|
173
|
+
res = %x(git ls-remote --tags #{Shellwords.escape(url)} 2>/dev/null)
|
|
174
|
+
return nil unless $?.success?
|
|
175
|
+
|
|
176
|
+
res.lines.map do |line|
|
|
177
|
+
ref = line.split("\t")[1].to_s.chomp
|
|
178
|
+
ref.sub(/\Arefs\/tags\//, '').sub(/\^\{\}\z/, '')
|
|
179
|
+
end.reject(&:empty?).uniq
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
# 执行命令并返回标准输出内容,不改变调用方目录
|
|
184
|
+
def GitHelper.capture(path, command)
|
|
185
|
+
current = Dir.pwd
|
|
186
|
+
Dir.chdir(path)
|
|
187
|
+
Build.log.debug("git命令(#{File.basename(path)}):#{command}")
|
|
188
|
+
res = %x(#{command})
|
|
189
|
+
Dir.chdir(current)
|
|
190
|
+
res
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# 执行命令并返回布尔结果,不因失败退出进程
|
|
194
|
+
# @return [Boolean]
|
|
195
|
+
def GitHelper.run_bool(path, command)
|
|
196
|
+
current = Dir.pwd
|
|
197
|
+
Dir.chdir(path)
|
|
198
|
+
Build.log.debug("git命令(#{File.basename(path)}):#{command}")
|
|
199
|
+
ok = system(command)
|
|
200
|
+
Dir.chdir(current)
|
|
201
|
+
ok == true
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
def GitHelper.execute(path, command, can_raise)
|
|
206
|
+
current = Dir.pwd
|
|
207
|
+
Dir.chdir(path)
|
|
208
|
+
|
|
209
|
+
Build.log.debug("git命令(#{File.basename(path)}):#{command}")
|
|
210
|
+
res = system command
|
|
211
|
+
|
|
212
|
+
if can_raise and not res
|
|
213
|
+
pusher = Push.instance
|
|
214
|
+
error = "失败: #{command}"
|
|
215
|
+
pusher.push("❌ 失败", error)
|
|
216
|
+
Process.exit(-1)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
Dir.chdir(current)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative 'repo'
|
|
3
|
+
require_relative 'config'
|
|
4
|
+
require_relative 'hosting'
|
|
5
|
+
require 'git'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require 'digest'
|
|
9
|
+
require 'shellwords'
|
|
10
|
+
require 'singleton'
|
|
11
|
+
|
|
12
|
+
# https://rubydoc.info/gems/git/2.3.0/Git/Base
|
|
13
|
+
|
|
14
|
+
module Build
|
|
15
|
+
class GithubFork
|
|
16
|
+
include Singleton
|
|
17
|
+
|
|
18
|
+
# @return [String] 仓库本机路径
|
|
19
|
+
attr_accessor :path
|
|
20
|
+
|
|
21
|
+
# @return [String] 组件名
|
|
22
|
+
attr_accessor :pod_name
|
|
23
|
+
|
|
24
|
+
# @return [String] fork前对应的github地址
|
|
25
|
+
attr_accessor :github_url
|
|
26
|
+
|
|
27
|
+
# @return [String] fork后对应的代码托管平台地址(Gitee/GitLab)
|
|
28
|
+
attr_accessor :source_repo_url
|
|
29
|
+
|
|
30
|
+
# @return [Git] 当前仓库 clone完毕后有值
|
|
31
|
+
attr_accessor :repo
|
|
32
|
+
|
|
33
|
+
# http归档信息(source.http组件);git源组件为nil
|
|
34
|
+
# @return [Hash, nil] {url:, type:, sha256:, sha1:, flatten:, version:}
|
|
35
|
+
attr_accessor :archive
|
|
36
|
+
|
|
37
|
+
# 下载到本地的podspec.json路径(播种进归档目录时使用)
|
|
38
|
+
attr_accessor :podspec_json_path
|
|
39
|
+
|
|
40
|
+
# 是否强制覆盖(镜像仓同名tag指向新commit时允许覆盖推送)
|
|
41
|
+
attr_accessor :force_cover
|
|
42
|
+
|
|
43
|
+
def setup(path, pod_name, github_url, source_repo_url, archive: nil, podspec_json_path: nil, force_cover: false)
|
|
44
|
+
self.path = path
|
|
45
|
+
self.pod_name = pod_name
|
|
46
|
+
self.github_url = github_url
|
|
47
|
+
self.source_repo_url = source_repo_url
|
|
48
|
+
self.archive = archive
|
|
49
|
+
self.podspec_json_path = podspec_json_path
|
|
50
|
+
self.force_cover = force_cover == true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fork_if_need(repo_cocoapods_source)
|
|
54
|
+
if archive
|
|
55
|
+
fork_http_archive(repo_cocoapods_source)
|
|
56
|
+
else
|
|
57
|
+
download_github
|
|
58
|
+
check_can_fork(repo_cocoapods_source)
|
|
59
|
+
create_if_need
|
|
60
|
+
upload_source
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# ===== http归档组件流程:下载归档 → 解压 → 播种podspec → 建镜像仓 → 防呆校验 → 就地git化推送 =====
|
|
67
|
+
# 背景:sqlite3等组件的podspec source为{http: 官方归档},没有上游git仓可克隆,
|
|
68
|
+
# 无法走常规fork流程。将归档内容git化成一个镜像仓(等价正常源码仓库)后,
|
|
69
|
+
# 发布podspec引用{git: 镜像仓, tag: 版本号},下游全部复用现有机制
|
|
70
|
+
def fork_http_archive(repo_cocoapods_source)
|
|
71
|
+
download_archive
|
|
72
|
+
seed_podspec
|
|
73
|
+
create_if_need(archive[:url])
|
|
74
|
+
check_published_refs_on_mirror(repo_cocoapods_source)
|
|
75
|
+
gitify_and_upload
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 下载归档文件:原始地址优先,github.com归档自动追加代理镜像候选,失败切换下一个
|
|
79
|
+
def download_archive
|
|
80
|
+
Build.log.step("下载http归档(#{File.basename(self.path)})")
|
|
81
|
+
# 上次失败可能残留半成品目录/文件,先清理
|
|
82
|
+
FileUtils.rm_rf(self.path)
|
|
83
|
+
FileUtils.rm_rf(archive_tmp_file)
|
|
84
|
+
FileUtils.mkdir_p(self.path)
|
|
85
|
+
|
|
86
|
+
urls = build_archive_urls
|
|
87
|
+
downloaded = false
|
|
88
|
+
urls.each_with_index do |url, index|
|
|
89
|
+
begin
|
|
90
|
+
Build.log.info("下载归档(#{index + 1}/#{urls.size} #{url})...")
|
|
91
|
+
# -f: HTTP错误码直接失败(避免把错误页存成文件) -L: 跟随重定向 -sS: 静默但保留错误输出
|
|
92
|
+
command = "curl -fsSL --retry 2 --connect-timeout 15 --max-time 600 " \
|
|
93
|
+
"-o #{Shellwords.escape(archive_tmp_file)} #{Shellwords.escape(url)}"
|
|
94
|
+
raise 'curl命令执行失败' unless system(command)
|
|
95
|
+
raise '归档文件未生成或为空' unless File.exist?(archive_tmp_file) && File.size(archive_tmp_file) > 0
|
|
96
|
+
|
|
97
|
+
downloaded = true
|
|
98
|
+
break
|
|
99
|
+
rescue => exception
|
|
100
|
+
Build.log.detail("归档下载失败:#{exception.message},切换下一个下载源")
|
|
101
|
+
FileUtils.rm_f(archive_tmp_file)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
unless downloaded
|
|
106
|
+
Build.log.error("所有归档下载源均失败,请检查网络后重试:#{archive[:url]}")
|
|
107
|
+
Process.exit(-1)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
verify_archive_digest
|
|
111
|
+
extract_archive
|
|
112
|
+
FileUtils.rm_f(archive_tmp_file)
|
|
113
|
+
Build.log.detail("归档解压完毕(#{Dir.children(self.path).size}个顶层条目)")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# 归档文件临时存放路径(与解压目录同级,解压后删除)
|
|
117
|
+
def archive_tmp_file
|
|
118
|
+
"#{self.path}.archive"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# 归档下载候选地址:原始地址优先;github.com归档自动追加代理镜像
|
|
122
|
+
# @return [Array<String>]
|
|
123
|
+
def build_archive_urls
|
|
124
|
+
urls = [archive[:url]]
|
|
125
|
+
if archive[:url] =~ %r{\Ahttps?://(www\.)?github\.com/}
|
|
126
|
+
urls << "https://ghproxy.net/#{archive[:url]}"
|
|
127
|
+
urls << "https://gh-proxy.com/#{archive[:url]}"
|
|
128
|
+
end
|
|
129
|
+
urls.uniq
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# 归档摘要校验:声明了sha256/sha1时与实际计算值比对,不一致直接终止;
|
|
133
|
+
# 实际sha256始终记录到GithubHelper(无论是否声明),供发布podspec的build_metadata溯源
|
|
134
|
+
def verify_archive_digest
|
|
135
|
+
actual_sha256 = Digest::SHA256.file(archive_tmp_file).hexdigest
|
|
136
|
+
GithubHelper.instance.archive_sha_actual = actual_sha256
|
|
137
|
+
|
|
138
|
+
if archive[:sha256].to_s.strip.empty? && archive[:sha1].to_s.strip.empty?
|
|
139
|
+
Build.log.detail("归档未声明摘要,已计算sha256: #{actual_sha256}")
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if !archive[:sha256].to_s.strip.empty?
|
|
144
|
+
if actual_sha256 != archive[:sha256].strip.downcase
|
|
145
|
+
fail_digest('sha256', archive[:sha256], actual_sha256)
|
|
146
|
+
end
|
|
147
|
+
elsif !archive[:sha1].to_s.strip.empty?
|
|
148
|
+
actual_sha1 = Digest::SHA1.file(archive_tmp_file).hexdigest
|
|
149
|
+
if actual_sha1 != archive[:sha1].strip.downcase
|
|
150
|
+
fail_digest('sha1', archive[:sha1], actual_sha1)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
Build.log.detail("归档摘要校验通过: sha256=#{actual_sha256}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def fail_digest(type, declared, actual)
|
|
157
|
+
Build.log.error("归档#{type}校验失败:声明#{declared},实际#{actual},归档内容可能被篡改或声明有误")
|
|
158
|
+
Process.exit(-1)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# 按归档类型解压到目标目录(保留归档原始顶层目录结构,flatten=true时上移)
|
|
162
|
+
def extract_archive
|
|
163
|
+
Build.log.detail("解压归档(#{archive[:type]})到#{File.basename(self.path)}")
|
|
164
|
+
escaped_file = Shellwords.escape(archive_tmp_file)
|
|
165
|
+
escaped_dir = Shellwords.escape(self.path)
|
|
166
|
+
command = case archive[:type]
|
|
167
|
+
when 'zip' then "unzip -q -o #{escaped_file} -d #{escaped_dir}"
|
|
168
|
+
when 'tar.gz', 'tgz' then "tar -xzf #{escaped_file} -C #{escaped_dir}"
|
|
169
|
+
when 'tar.bz2', 'tbz', 'tbz2' then "tar -xjf #{escaped_file} -C #{escaped_dir}"
|
|
170
|
+
when 'tar.xz', 'txz' then "tar -xJf #{escaped_file} -C #{escaped_dir}"
|
|
171
|
+
else
|
|
172
|
+
Build.log.error("不支持的归档类型: #{archive[:type]}")
|
|
173
|
+
Process.exit(-1)
|
|
174
|
+
end
|
|
175
|
+
unless system(command)
|
|
176
|
+
Build.log.error("归档解压失败: #{command}")
|
|
177
|
+
Process.exit(-1)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# 清理macOS压缩包自带的资源叉目录/文件,避免污染flatten判定与镜像仓内容
|
|
181
|
+
if archive[:type] == 'zip'
|
|
182
|
+
FileUtils.rm_rf(File.join(self.path, '__MACOSX'))
|
|
183
|
+
Dir.glob(File.join(self.path, '**', '.DS_Store'), File::FNM_DOTMATCH).each { |p| FileUtils.rm_f(p) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
flatten_archive_root if archive[:flatten]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# flatten语义:归档仅有一个顶层目录时,把其内容上移到根目录
|
|
190
|
+
# (与CocoaPods http下载flatten行为一致,便于source_files直接匹配根目录)
|
|
191
|
+
def flatten_archive_root
|
|
192
|
+
entries = Dir.children(self.path).reject { |e| e == '.DS_Store' }
|
|
193
|
+
return unless entries.size == 1 && File.directory?(File.join(self.path, entries.first))
|
|
194
|
+
|
|
195
|
+
top_dir = File.join(self.path, entries.first)
|
|
196
|
+
Dir.children(top_dir).each do |child|
|
|
197
|
+
FileUtils.mv(File.join(top_dir, child), File.join(self.path, child))
|
|
198
|
+
end
|
|
199
|
+
FileUtils.rmdir(top_dir)
|
|
200
|
+
Build.log.detail("flatten=true,已将归档顶层目录#{entries.first}内容上移到根目录")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# 播种podspec:把从Specs下载的podspec.json放入归档根目录。
|
|
204
|
+
# 归档内不携带podspec,git化后镜像仓自身携带才能让克隆侧定位到podspec
|
|
205
|
+
# (source_podspec_entry扫描与LocalDependencies的path集成均依赖它)
|
|
206
|
+
def seed_podspec
|
|
207
|
+
unless podspec_json_path && File.exist?(podspec_json_path)
|
|
208
|
+
Build.log.error("未找到已下载的podspec文件,无法播种到归档目录: #{podspec_json_path}")
|
|
209
|
+
Process.exit(-1)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
dest = File.join(self.path, "#{self.pod_name}.podspec.json")
|
|
213
|
+
FileUtils.cp(podspec_json_path, dest)
|
|
214
|
+
Build.log.detail("已播种podspec: #{File.basename(dest)}")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# 防呆校验:已发布源码spec引用的版本tag必须仍存在于镜像仓远端。
|
|
218
|
+
# http归档组件每次构建都是全新单提交历史,无法像git fork那样用上游全量历史校验,
|
|
219
|
+
# 改为ls-remote直查镜像仓远端refs——可拦截"镜像仓被删除重建导致历史tag丢失"
|
|
220
|
+
# 后静默发布引用失效spec的问题(旧版本切源码时会拉不到代码)
|
|
221
|
+
def check_published_refs_on_mirror(repo_cocoapods_source)
|
|
222
|
+
versions = repo_cocoapods_source.pod_name_versions(self.pod_name)
|
|
223
|
+
return unless versions
|
|
224
|
+
|
|
225
|
+
tag_refs = versions.map { |version| published_source_ref(repo_cocoapods_source.path, version) }
|
|
226
|
+
.compact
|
|
227
|
+
.select { |ref_type, _| ref_type == 'tag' }
|
|
228
|
+
.map { |_, ref_name| ref_name }
|
|
229
|
+
.uniq
|
|
230
|
+
return if tag_refs.empty?
|
|
231
|
+
|
|
232
|
+
Build.log.step("校验已发布版本tag在镜像仓仍存在(#{self.source_repo_url})")
|
|
233
|
+
remote_tags = GitHelper.remote_tags(self.source_repo_url.ssh_to_http)
|
|
234
|
+
if remote_tags.nil?
|
|
235
|
+
Build.log.detail("镜像仓暂不可达或尚未创建,跳过已发布tag校验(后续推送若失败会直接报错)")
|
|
236
|
+
return
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
missing = tag_refs - remote_tags
|
|
240
|
+
if missing.any?
|
|
241
|
+
Build.log.error("镜像仓#{self.source_repo_url}缺失已发布版本引用的tag:#{missing.join('、')}," \
|
|
242
|
+
"镜像仓可能被删除重建,旧版本切源码将拉不到代码,请先恢复历史tag或使用强制覆盖后重新git化")
|
|
243
|
+
Process.exit(-1)
|
|
244
|
+
end
|
|
245
|
+
Build.log.detail("已发布版本tag校验通过,共#{tag_refs.size}个")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# 就地git化并推送镜像仓:
|
|
249
|
+
# git init(本地提交者配置兜底) → 提交原始归档内容 → 打版本tag → 强推master → 推tag
|
|
250
|
+
# 只推送master与当前版本tag,禁用--mirror/--prune,保证不影响镜像仓里其它历史tag
|
|
251
|
+
def gitify_and_upload
|
|
252
|
+
Build.log.step("归档git化并推送镜像仓(#{File.basename(self.path)})")
|
|
253
|
+
tag = archive[:version]
|
|
254
|
+
|
|
255
|
+
GitHelper.init_repo(self.path)
|
|
256
|
+
GitHelper.add_stag(self.path)
|
|
257
|
+
digest = GithubHelper.instance.archive_sha_actual
|
|
258
|
+
commit_msg = "#{self.pod_name} #{tag} 官方归档导入(地址: #{archive[:url]} sha256: #{digest})"
|
|
259
|
+
GitHelper.commit(self.path, commit_msg)
|
|
260
|
+
GitHelper.tag_local(self.path, tag)
|
|
261
|
+
GitHelper.remote_add(self.path, 'origin', self.source_repo_url)
|
|
262
|
+
GitHelper.push_head_master_f(self.path)
|
|
263
|
+
|
|
264
|
+
# tag推送策略:归档内容固定,镜像仓已有同名tag时默认跳过;--force-cover时强制覆盖到新commit
|
|
265
|
+
existing_tags = GitHelper.remote_tags(self.source_repo_url.ssh_to_http) || []
|
|
266
|
+
if existing_tags.include?(tag)
|
|
267
|
+
if self.force_cover
|
|
268
|
+
GitHelper.tag_origin_f(self.path, tag)
|
|
269
|
+
Build.log.info("镜像仓tag#{tag}已存在,force_cover=true 已强制覆盖")
|
|
270
|
+
else
|
|
271
|
+
Build.log.info("镜像仓tag#{tag}已存在且归档内容固定,跳过tag推送")
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
GitHelper.tag_origin(self.path, tag)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
Build.log.detail("归档git化推送完毕(master + tag #{tag})")
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# ===== 以下为git源fork组件的原有流程 =====
|
|
281
|
+
|
|
282
|
+
def download_github
|
|
283
|
+
repo_path = Pathname(path)
|
|
284
|
+
Build.log.step("fork源码(#{File.basename(self.path)})")
|
|
285
|
+
|
|
286
|
+
# 克隆候选地址:优先原始地址直连,失败再依次切换镜像重试
|
|
287
|
+
clone_urls = build_clone_urls
|
|
288
|
+
cloned = false
|
|
289
|
+
clone_urls.each_with_index do |clone_url, index|
|
|
290
|
+
begin
|
|
291
|
+
Build.log.info("下载源码文件(#{index + 1}/#{clone_urls.size} #{clone_url})...")
|
|
292
|
+
# 上次克隆失败可能残留半成品目录,克隆前先清理,避免目录已存在导致再次克隆失败
|
|
293
|
+
FileUtils.rm_rf(repo_path)
|
|
294
|
+
# 单次克隆设置120秒超时,避免镜像不可达时长时间卡住打包流程
|
|
295
|
+
Git.clone(clone_url, repo_path, timeout: 120)
|
|
296
|
+
cloned = true
|
|
297
|
+
break
|
|
298
|
+
rescue => exception
|
|
299
|
+
Build.log.detail("源码下载失败:#{exception.message},切换下一个下载源")
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
unless cloned
|
|
304
|
+
Build.log.error("所有源码下载源均克隆失败,请检查网络后重试:#{self.github_url}")
|
|
305
|
+
Process.exit(-1)
|
|
306
|
+
end
|
|
307
|
+
Build.log.detail("上游源码克隆完毕")
|
|
308
|
+
|
|
309
|
+
Build.log.detail("检出所有分支")
|
|
310
|
+
self.repo = Git.open(repo_path)
|
|
311
|
+
# git gem 5.x 新API:branch_list 同时返回本地和远程跟踪分支(BranchInfo)。
|
|
312
|
+
# 遍历每个分支并检出:远程跟踪分支需先在本地创建同名分支,
|
|
313
|
+
# 这样后续 push --all 才能把上游全部分支镜像到目标仓库
|
|
314
|
+
self.repo.branch_list.each do |branch_info|
|
|
315
|
+
# 跳过 origin/HEAD 这类指向默认分支的符号引用,它不是真实分支
|
|
316
|
+
next if branch_info.symref?
|
|
317
|
+
|
|
318
|
+
branch_name = branch_info.short_name
|
|
319
|
+
if branch_info.remote? && !self.repo.local_branch?(branch_name)
|
|
320
|
+
self.repo.branch_new(branch_name, branch_info.refname)
|
|
321
|
+
end
|
|
322
|
+
self.repo.checkout(branch_name)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
self.repo.config_set('remote.origin.url', self.source_repo_url)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# 构造源码克隆候选地址:原始地址优先,其后依次为 kkgithub 域名替换、gh.llkk.cc 前缀代理
|
|
329
|
+
# @return [Array<String>]
|
|
330
|
+
def build_clone_urls
|
|
331
|
+
if self.github_url.start_with?("http")
|
|
332
|
+
http_url = self.github_url
|
|
333
|
+
else
|
|
334
|
+
# SSH 地址镜像不支持,先转成 https 形式再拼镜像
|
|
335
|
+
http_url = self.github_url.ssh_to_http.sub(/\Ahttp:/, "https:")
|
|
336
|
+
http_url = "#{http_url}.git" unless http_url.end_with?(".git")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
[
|
|
340
|
+
self.github_url,
|
|
341
|
+
http_url.gsub("github.com", "kkgithub.com"),
|
|
342
|
+
"https://gh.llkk.cc/#{http_url}"
|
|
343
|
+
].uniq
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# fork前校验:源码spec仓里每个已发布版本的podspec,其source引用的tag/branch/commit
|
|
347
|
+
# 必须在上游仓库真实存在,避免镜像force推送后老版本引用失效。
|
|
348
|
+
# 注意:不能拿版本号(如1.1.0)直接与tag名比较——三方库上游tag普遍带v前缀(如v1.1.0),
|
|
349
|
+
# 必须以已发布podspec里source声明的实际引用为准。
|
|
350
|
+
# 只校验源码spec仓:二进制spec的source指向二进制组件仓,与GitHub上游无关,不在此校验。
|
|
351
|
+
def check_can_fork(repo_cocoapods)
|
|
352
|
+
versions = repo_cocoapods.pod_name_versions(self.pod_name)
|
|
353
|
+
return unless versions
|
|
354
|
+
|
|
355
|
+
Build.log.step("校验已发布版本引用是否都存在于上游仓库(#{self.github_url})")
|
|
356
|
+
missing = []
|
|
357
|
+
versions.each do |version|
|
|
358
|
+
ref_type, ref_name = published_source_ref(repo_cocoapods.path, version)
|
|
359
|
+
if ref_type.nil?
|
|
360
|
+
Build.log.detail("版本#{version}未找到可校验的podspec source(tag/branch/commit),跳过")
|
|
361
|
+
next
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
if ref_exist?(ref_name)
|
|
365
|
+
Build.log.detail("版本#{version} 引用 #{ref_type} #{ref_name}:上游存在")
|
|
366
|
+
else
|
|
367
|
+
missing << "#{version}(#{ref_type}:#{ref_name})"
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
if missing.length > 0
|
|
372
|
+
Build.log.error("fork失败,以下已发布版本引用的tag/分支/commit在上游仓库#{self.github_url}中不存在:#{missing.join('、')},请手动处理后再打包。")
|
|
373
|
+
Process.exit(-1)
|
|
374
|
+
end
|
|
375
|
+
Build.log.detail("已发布版本引用校验通过,共#{versions.length}个版本")
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# 读取源码spec仓中某已发布版本podspec.json声明的源码引用
|
|
379
|
+
# @return [Array(String, String), nil] [tag/branch/commit, 引用名];无podspec或source非git引用时返回nil
|
|
380
|
+
def published_source_ref(spec_root, version)
|
|
381
|
+
json_path = File.join(spec_root, self.pod_name, version, "#{self.pod_name}.podspec.json")
|
|
382
|
+
unless File.exist?(json_path)
|
|
383
|
+
# 兼容非常规命名:版本目录下唯一的podspec.json
|
|
384
|
+
candidates = Dir.glob(File.join(spec_root, self.pod_name, version, '*.podspec.json'))
|
|
385
|
+
json_path = candidates.first if candidates.length == 1
|
|
386
|
+
end
|
|
387
|
+
return nil unless json_path && File.exist?(json_path)
|
|
388
|
+
|
|
389
|
+
source = JSON.parse(File.read(json_path))['source'] rescue nil
|
|
390
|
+
return nil unless source.is_a?(Hash)
|
|
391
|
+
|
|
392
|
+
if !source['tag'].to_s.empty?
|
|
393
|
+
['tag', source['tag'].to_s]
|
|
394
|
+
elsif !source['branch'].to_s.empty?
|
|
395
|
+
['branch', source['branch'].to_s]
|
|
396
|
+
elsif !source['commit'].to_s.empty?
|
|
397
|
+
['commit', source['commit'].to_s]
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# 校验某个git引用(tag名/分支名/commit号)在克隆下来的上游仓库中是否存在。
|
|
402
|
+
# 全量克隆包含上游所有历史,revparse 对tag/branch/commit均可解析;不存在时git gem抛FailedError
|
|
403
|
+
def ref_exist?(ref_name)
|
|
404
|
+
self.repo.revparse(ref_name)
|
|
405
|
+
true
|
|
406
|
+
rescue Git::FailedError
|
|
407
|
+
false
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def create_if_need(description = self.github_url)
|
|
411
|
+
provider = Hosting.provider_for(self.source_repo_url)
|
|
412
|
+
# fork源码镜像仓不存在时自动创建为私有仓库(固定私有)
|
|
413
|
+
provider.create_project_if_need(self.pod_name, self.source_repo_url.ssh_to_http, description: description)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def upload_source
|
|
417
|
+
Build.log.detail("推送并覆盖所有分支和标签(#{self.source_repo_url})")
|
|
418
|
+
# git gem 5.x: push(remote, refspec, opts),all/tags/force 在第三个参数
|
|
419
|
+
self.repo.push('origin', nil, {:all => true, :force => true})
|
|
420
|
+
self.repo.push('origin', nil, {:tags => true, :force => true})
|
|
421
|
+
Build.log.detail("分支和标签推送完毕")
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
end
|
|
425
|
+
end
|