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.
@@ -0,0 +1,334 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'uri'
7
+ require_relative 'push'
8
+ require 'singleton'
9
+
10
+ module Build
11
+ class GithubHelper
12
+ include Singleton
13
+
14
+ attr_accessor :path
15
+ attr_accessor :pod_name
16
+ attr_accessor :pod_version
17
+ attr_accessor :third
18
+ attr_accessor :pusher
19
+ attr_accessor :github_tag
20
+ attr_accessor :github_url
21
+ attr_accessor :github_ssh_url
22
+ attr_accessor :podspec_content
23
+
24
+ # 是否为http归档组件(podspec source为{http: 归档地址}形态,无git源码仓)
25
+ attr_accessor :http_source
26
+ # http归档下载地址(source.http声明的原始地址)
27
+ attr_accessor :archive_url
28
+ # 归档类型(zip/tar.gz/tgz/tar.bz2/tar.xz)
29
+ attr_accessor :archive_type
30
+ # source.http中声明的校验摘要(未声明时为nil)
31
+ attr_accessor :archive_sha256_declared
32
+ attr_accessor :archive_sha1_declared
33
+ # 下载后实际计算的sha256(下载阶段写入,供发布podspec的build_metadata溯源)
34
+ attr_accessor :archive_sha_actual
35
+ # 是否解压后上移顶层目录(source.http.flatten=true)
36
+ attr_accessor :archive_flatten
37
+ # 下载到本地的podspec.json绝对路径(播种到归档目录时使用)
38
+ attr_accessor :podspec_json_path
39
+
40
+ def initialize
41
+ @http_source = false
42
+ end
43
+
44
+ def request_podspec(path, pod_name, pod_version)
45
+ @path = path
46
+ @pod_name = pod_name
47
+ @pod_version = pod_version
48
+ @pusher = Push.instance
49
+
50
+ download_podspec
51
+ analyzer_tag
52
+ end
53
+
54
+ # http归档信息汇总(http归档git化流程使用);非http组件返回nil
55
+ # @return [Hash, nil] {url:, type:, sha256:, sha1:, flatten:, version:}
56
+ def archive_info
57
+ return nil unless @http_source
58
+
59
+ {
60
+ url: @archive_url,
61
+ type: @archive_type,
62
+ sha256: @archive_sha256_declared,
63
+ sha1: @archive_sha1_declared,
64
+ flatten: @archive_flatten,
65
+ version: @github_tag
66
+ }
67
+ end
68
+
69
+ private
70
+ def download_podspec
71
+ Build.log.step("(#{File.basename(self.path)})从github上下载podspec文件")
72
+
73
+ unless Dir.exist?(self.path)
74
+ Dir.mkdir(self.path)
75
+ end
76
+
77
+ podspec_json_path = "#{self.path}/#{pod_name}.podspec.json"
78
+
79
+ pod_name_md5 = Digest::MD5.hexdigest(pod_name)
80
+ pod_name_md5_0 = pod_name_md5[0..0]
81
+ pod_name_md5_1 = pod_name_md5[1..1]
82
+ pod_name_md5_2 = pod_name_md5[2..2]
83
+ # 存为实例变量:analyzer_tag 方法也要用该路径打印所有版本 podspec 地址,
84
+ # 不能用局部变量(跨方法不可见会被解析为 self.split_path 方法调用并报 NameError)
85
+ @split_path = "#{pod_name_md5_0}/#{pod_name_md5_1}/#{pod_name_md5_2}"
86
+ # podspec 下载源候选地址,按可用性优先级排列,请求失败自动切换下一个
87
+ raw_json_url = "https://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/#{@split_path}/#{pod_name}/#{pod_version}/#{pod_name}.podspec.json"
88
+ podspec_json_urls = [
89
+ # jsDelivr Fastly 线路(CDN,命中的文件速度快;源上不存在时可能 301 回源 raw)
90
+ "https://fastly.jsdelivr.net/cocoa/Specs/#{@split_path}/#{pod_name}/#{pod_version}/#{pod_name}.podspec.json",
91
+ # jsDelivr GCore 线路
92
+ "https://gcore.jsdelivr.net/cocoa/Specs/#{@split_path}/#{pod_name}/#{pod_version}/#{pod_name}.podspec.json",
93
+ # ghproxy.net 代理(国内可达,如实透传 GitHub raw 的 200/404,版本不存在时可据此准确判定)
94
+ "https://ghproxy.net/#{raw_json_url}",
95
+ # gh-proxy.com 代理(同上,备用)
96
+ "https://gh-proxy.com/#{raw_json_url}"
97
+ ]
98
+
99
+ Build.log.detail("组件md5裁切:#{@split_path}")
100
+
101
+ # GitHub 上该组件的 podspec 目录(人工核对组件名大小写/版本是否存在时用)
102
+ specs_page_url = "https://github.com/CocoaPods/Specs/tree/master/Specs/#{@split_path}/#{pod_name}"
103
+
104
+ # 依次尝试候选下载源,任一成功即停止,全部失败才终止打包
105
+ podspec_content = nil
106
+ podspec_json_url = nil
107
+ not_found_count = 0
108
+ podspec_json_urls.each_with_index do |candidate_url, index|
109
+ begin
110
+ # 跟随 301/302:jsDelivr 对源上不存在的文件会 301 回源 raw.githubusercontent,
111
+ # 必须跟随到最终地址,以最终状态码判断是否真的 404
112
+ response = fetch_with_redirect(candidate_url)
113
+ # 非 2xx 状态码视为该下载源不可用,切换下一个下载源
114
+ unless response.is_a?(Net::HTTPSuccess)
115
+ not_found_count += 1 if response.code == '404'
116
+ Build.log.detail("下载源(#{index + 1}/#{podspec_json_urls.size})返回状态码 #{response.code}:#{candidate_url}")
117
+ next
118
+ end
119
+ podspec_content = JSON.parse(response.body)
120
+ podspec_json_url = candidate_url
121
+ break
122
+ rescue => exception
123
+ Build.log.detail("下载源(#{index + 1}/#{podspec_json_urls.size})请求失败:#{exception.message}")
124
+ Build.log.detail(" 地址:#{candidate_url}")
125
+ end
126
+ end
127
+
128
+ unless podspec_content
129
+ # 各源尝试时已逐个打印过具体 URL,失败块不再重复罗列,只给结论和权威核对地址
130
+ if not_found_count > 0
131
+ Build.log.error("有下载源返回 404,GitHub Specs 上大概率不存在组件 '#{pod_name}' 的 #{pod_version} 版本,请核对组件名大小写(如 fmdb 应为 FMDB)与版本号")
132
+ else
133
+ Build.log.error("所有podspec下载源均未成功(可能是网络不可达或镜像回源失败),请检查网络后重试;若网络正常仍失败,通常是组件名/版本不存在")
134
+ end
135
+ Build.log.error("请打开以下 GitHub 地址确认组件/版本是否存在:#{specs_page_url}")
136
+ Process.exit(-1)
137
+ end
138
+
139
+ Build.log.detail("当前版本podspec地址: #{podspec_json_url}")
140
+
141
+ res_pod_name = podspec_content["name"]
142
+ unless res_pod_name == pod_name
143
+ error = "获取到的podspec的名字#{res_pod_name}!= #{pod_name}"
144
+ pusher.push("❌ 打包失败", error)
145
+ Process.exit(-1)
146
+ end
147
+
148
+ podspec_content.delete("license")
149
+ podspec_content.delete("module_map")
150
+
151
+ # swift使用静态库
152
+ unless podspec_content.has_key?("swift_version")
153
+ podspec_content.delete("static_framework")
154
+ end
155
+
156
+ # 记录变量
157
+ @podspec_content = podspec_content
158
+
159
+ ## 写文件
160
+ new_file = File.new(podspec_json_path, "w+")
161
+ if new_file
162
+ new_file.syswrite(JSON.pretty_generate(podspec_content))
163
+ end
164
+ # 记录下载产物路径(http归档组件播种podspec到归档目录时使用)
165
+ @podspec_json_path = File.expand_path(podspec_json_path)
166
+
167
+ end
168
+
169
+ # 发起 GET 并自动跟随 301/302 重定向(最多 5 次),返回最终响应。
170
+ # jsDelivr 对源上不存在的文件会 301 回源 raw.githubusercontent,
171
+ # 只有跟随到终点拿到的状态码才能准确反映文件是否存在(404)。
172
+ def fetch_with_redirect(url, limit = 5)
173
+ raise '重定向次数超过上限' if limit <= 0
174
+
175
+ uri = URI(url)
176
+ http = Net::HTTP.new(uri.host, uri.port)
177
+ http.use_ssl = (uri.scheme == 'https')
178
+ # 设置10秒连接/读取超时,避免慢速镜像/被墙地址长时间卡住打包流程
179
+ http.open_timeout = 10
180
+ http.read_timeout = 10
181
+ response = http.request_get(uri.request_uri)
182
+
183
+ if response.is_a?(Net::HTTPRedirection) && response['location']
184
+ next_url = URI.join(uri, response['location']).to_s
185
+ Build.log.detail(" 跟随重定向(#{response.code}):#{next_url}")
186
+ return fetch_with_redirect(next_url, limit - 1)
187
+ end
188
+
189
+ response
190
+ end
191
+
192
+ def analyzer_tag
193
+
194
+ source = podspec_content["source"]
195
+ unless source
196
+ error = "podspec指定的 source字段为空"
197
+ pusher.push("❌ 解析失败", error)
198
+ Process.exit(-1)
199
+ end
200
+
201
+ # 复位http归档状态,避免同进程重复解析时残留上一次的归档信息
202
+ @http_source = false
203
+ @archive_url = nil
204
+ @archive_type = nil
205
+ @archive_sha256_declared = nil
206
+ @archive_sha1_declared = nil
207
+ @archive_flatten = false
208
+
209
+ # source.http 归档形态走归档git化流程,git 形态走原fork流程
210
+ if source["http"]
211
+ analyzer_http_archive(source)
212
+ else
213
+ analyzer_git_source(source)
214
+ end
215
+ end
216
+
217
+ # git源组件解析(source.git + tag/branch/commit),即原analyzer_tag逻辑
218
+ def analyzer_git_source(source)
219
+ @github_url = source["git"]
220
+ @github_tag = if source["tag"]
221
+ source["tag"]
222
+ else
223
+ source["branch"] ? source["branch"] : source["commit"]
224
+ end
225
+ unless @github_url
226
+ error = "podspec文件未使用source[git],暂未支持"
227
+ pusher.push("❌ 失败", error)
228
+ Process.exit(-1)
229
+ end
230
+
231
+ unless @github_tag
232
+ error = "podspec未使用source[tag] source[branch] source[commit], 暂未支持"
233
+ pusher.push("❌ 解析失败", error)
234
+ Process.exit(-1)
235
+ end
236
+
237
+ # 转成ssh地址
238
+ @github_ssh_url = @github_url.http_to_ssh
239
+
240
+ Build.log.detail("所有版本podspec地址:https://github.com/CocoaPods/Specs/tree/master/Specs/#{@split_path}/#{pod_name}")
241
+ Build.log.detail("当前版本源码地址: #{@github_url}")
242
+ Build.log.detail("当前版本源码tag: #{@github_tag}")
243
+ end
244
+
245
+ # http归档组件解析(source.http):
246
+ # 归档没有上游git仓,fork阶段会把下载解压后的归档就地git化并推送镜像仓,
247
+ # 因此这里不解析git地址,镜像仓克隆引用统一取版本号(与git化时打的tag一致)
248
+ # 支持两种source形态:
249
+ # 字符串: "http": "https://xxx.zip"
250
+ # 对象: "http": {"url": "...", "type": "zip", "sha256": "...", "flatten": true}
251
+ def analyzer_http_archive(source)
252
+ @http_source = true
253
+
254
+ entry = source["http"]
255
+ url = nil
256
+ declared_type = ''
257
+ sha256 = nil
258
+ sha1 = nil
259
+ flatten = false
260
+ case entry
261
+ when String
262
+ url = entry
263
+ when Hash
264
+ url = entry["url"].to_s
265
+ declared_type = entry["type"].to_s.strip
266
+ sha256 = entry["sha256"].to_s.strip
267
+ sha256 = nil if sha256.empty?
268
+ sha1 = entry["sha1"].to_s.strip
269
+ sha1 = nil if sha1.empty?
270
+ flatten = entry["flatten"] == true
271
+ end
272
+
273
+ if url.to_s.strip.empty?
274
+ error = "source[http]归档地址为空,无法解析下载地址"
275
+ pusher.push("❌ 解析失败", error)
276
+ Process.exit(-1)
277
+ end
278
+ url = url.strip
279
+
280
+ # 已知不支持的归档附加配置(如headers需携带鉴权下载、subdir需子目录裁切),直接明确报错
281
+ extra_keys = entry.is_a?(Hash) ? entry.keys - %w[url type sha256 sha1 flatten] : []
282
+ unless extra_keys.empty?
283
+ error = "source[http]归档使用了暂不支持的配置项: #{extra_keys.join('、')}(目前支持url/type/sha256/sha1/flatten)"
284
+ pusher.push("❌ 解析失败", error)
285
+ Process.exit(-1)
286
+ end
287
+
288
+ # 归档类型:显式声明优先,否则按URL扩展名推断
289
+ @archive_type = declared_type.strip.downcase
290
+ @archive_type = infer_archive_type(url) if @archive_type.empty?
291
+ unless %w[zip tar.gz tgz tar.bz2 tbz tbz2 tar.xz txz].include?(@archive_type)
292
+ error = "不支持的归档类型: #{@archive_type},目前支持 zip/tar.gz/tgz/tar.bz2/tar.xz,可使用source.http.type显式声明"
293
+ pusher.push("❌ 解析失败", error)
294
+ Process.exit(-1)
295
+ end
296
+
297
+ @archive_url = url
298
+ @archive_sha256_declared = sha256
299
+ @archive_sha1_declared = sha1
300
+ @archive_flatten = flatten
301
+
302
+ # 镜像仓克隆引用取版本号:git化时在镜像仓打的tag即版本号,后续git_clone/podspec改写均复用该引用
303
+ @github_tag = pod_version
304
+
305
+ Build.log.detail("所有版本podspec地址:https://github.com/CocoaPods/Specs/tree/master/Specs/#{@split_path}/#{pod_name}")
306
+ Build.log.detail("当前版本为http归档组件(无git源码仓),将下载归档后git化到镜像仓")
307
+ Build.log.detail("归档地址: #{@archive_url}")
308
+ Build.log.detail("归档类型: #{@archive_type} flatten: #{@archive_flatten}")
309
+ Build.log.detail("归档摘要声明: sha256=#{sha256} sha1=#{sha1}")
310
+ Build.log.detail("镜像仓tag(取版本号): #{@github_tag}")
311
+ end
312
+
313
+ # 按URL路径扩展名推断归档类型(忽略query串)
314
+ # @return [String] 识别不出返回空串,由调用方报错
315
+ def infer_archive_type(url)
316
+ path = begin
317
+ URI.parse(url).path
318
+ rescue StandardError
319
+ url
320
+ end
321
+ lower = path.to_s.downcase
322
+ if lower.end_with?('.zip') then 'zip'
323
+ elsif lower.end_with?('.tar.gz') then 'tar.gz'
324
+ elsif lower.end_with?('.tgz') then 'tgz'
325
+ elsif lower.end_with?('.tar.bz2') then 'tar.bz2'
326
+ elsif lower.end_with?('.tbz') || lower.end_with?('.tbz2') then 'tar.bz2'
327
+ elsif lower.end_with?('.tar.xz') then 'tar.xz'
328
+ elsif lower.end_with?('.txz') then 'tar.xz'
329
+ else ''
330
+ end
331
+ end
332
+
333
+ end
334
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Status
6
+ # 打包工程拷贝yaml文件后
7
+ XCODE_PACKAGE_COPY_YAML = "XCODE_PACKAGE_COPY_YAML".downcase
8
+ end
9
+
10
+ class HookManager
11
+
12
+ include Singleton
13
+
14
+ def initialize
15
+ @hooks = {}
16
+ end
17
+
18
+ # 提供注册钩子的方法
19
+ def register_hook(status, &block)
20
+ @hooks[status] ||= []
21
+ @hooks[status] << block
22
+ end
23
+
24
+ # 触发钩子的方法
25
+ def trigger_hook(status, *args)
26
+ if @hooks[status]
27
+ Build.log.debug("执行hook阶段#{status}逻辑")
28
+ @hooks[status].each { |hook| hook.call(*args) }
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'openssl'
6
+ require 'cgi'
7
+
8
+ module Build
9
+ # Gitee OpenAPI v5 的极简HTTP客户端(基于Net::HTTP,无额外gem依赖)
10
+ # 接口文档: https://gitee.com/api/v5/swagger
11
+ class GiteeClient
12
+ class Error < StandardError; end
13
+
14
+ # 资源不存在(404)
15
+ class NotFound < Error; end
16
+
17
+ # 令牌无效/权限不足(401/403)
18
+ class Unauthorized < Error; end
19
+
20
+ # @param endpoint [String] 如 https://gitee.com/api/v5
21
+ # @param token [String] 私人令牌(access_token)
22
+ def initialize(endpoint, token)
23
+ @endpoint = endpoint.to_s.sub(/\/\z/, '')
24
+ @token = token.to_s
25
+ end
26
+
27
+ def get(path, params = {})
28
+ request('GET', path, params)
29
+ end
30
+
31
+ def post(path, params = {})
32
+ request('POST', path, params)
33
+ end
34
+
35
+ def patch(path, params = {})
36
+ request('PATCH', path, params)
37
+ end
38
+
39
+ def put(path, params = {})
40
+ request('PUT', path, params)
41
+ end
42
+
43
+ def delete(path, params = {})
44
+ request('DELETE', path, params)
45
+ end
46
+
47
+ # 对URL path段进行转义
48
+ def self.escape_segment(value)
49
+ CGI.escape(value.to_s)
50
+ end
51
+
52
+ private
53
+
54
+ def request(method, path, params)
55
+ params = (params || {}).reject { |_k, v| v.nil? }
56
+
57
+ uri = URI("#{@endpoint}#{path}")
58
+ request_class = Net::HTTP.const_get(method.capitalize)
59
+
60
+ if %w[GET DELETE].include?(method)
61
+ uri.query = URI.encode_www_form(params.merge(access_token: @token))
62
+ req = request_class.new(uri.request_uri)
63
+ else
64
+ # Gitee v5 同时接受 query 中的 access_token 和表单 body
65
+ uri.query = URI.encode_www_form(access_token: @token)
66
+ req = request_class.new(uri.request_uri)
67
+ req.set_form_data(params) unless params.empty?
68
+ end
69
+
70
+ http = Net::HTTP.new(uri.host, uri.port)
71
+ if uri.scheme == 'https'
72
+ http.use_ssl = true
73
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
74
+ end
75
+ http.open_timeout = 15
76
+ http.read_timeout = 30
77
+
78
+ res = http.request(req)
79
+ parse_response(res)
80
+ end
81
+
82
+ def parse_response(res)
83
+ code = res.code.to_i
84
+ data = parse_body(res.body)
85
+
86
+ return data if (200..299).cover?(code)
87
+
88
+ message = extract_message(data)
89
+ exception =
90
+ case code
91
+ when 404 then NotFound
92
+ when 401, 403 then Unauthorized
93
+ else Error
94
+ end
95
+ raise exception, "Gitee API请求失败(#{code}): #{message}"
96
+ end
97
+
98
+ def parse_body(body)
99
+ return {} if body.nil? || body.to_s.strip.empty?
100
+
101
+ JSON.parse(body)
102
+ rescue JSON::ParserError
103
+ {}
104
+ end
105
+
106
+ def extract_message(data)
107
+ if data.is_a?(Hash)
108
+ data['message'] || data['error'] || data.to_s
109
+ else
110
+ data.to_s
111
+ end
112
+ end
113
+ end
114
+ end