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,439 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'digest'
6
+ require_relative 'repo_module'
7
+ require_relative 'util/file_helper'
8
+
9
+ module Build
10
+ class RepoModuleBinary < RepoModule
11
+
12
+ attr_accessor :framework_binary_path
13
+
14
+ # 合并二进制
15
+ def copy_framework(repo_xcode_package, repo_module_source)
16
+ build_real_device = build_item.real_device
17
+ build_simulator = build_item.simulator
18
+ if not build_real_device and not build_simulator
19
+ return
20
+ end
21
+
22
+ @framework_name = repo_module_source.framework_name
23
+
24
+ origin_framework_real_device = "#{repo_xcode_package.real_device_archive_path}/Products/Library/Frameworks/#{framework_name}.framework"
25
+ origin_framework_simulator = "#{repo_xcode_package.simulator_archive_path}/Products/Library/Frameworks/#{framework_name}.framework"
26
+
27
+ if build_real_device and not File.exist?(origin_framework_real_device)
28
+ error = "真机架构framework不存在"
29
+ pusher.push("❌ 打包失败", error)
30
+ Process.exit(-1)
31
+ end
32
+
33
+ if build_simulator and not File.exist?(origin_framework_simulator)
34
+ error = "模拟器架构framework不存在"
35
+ pusher.push("❌ 打包失败", error)
36
+ Process.exit(-1)
37
+ end
38
+
39
+ @framework_binary_path = "#{self.path}/#{framework_name}.xcframework"
40
+
41
+ command = "xcodebuild -create-xcframework"
42
+
43
+ if build_real_device
44
+ command += " -framework \"#{origin_framework_real_device}\""
45
+ end
46
+
47
+ if build_simulator
48
+ command += " -framework \"#{origin_framework_simulator}\""
49
+ end
50
+
51
+ command += " -output \"#{framework_binary_path}\""
52
+
53
+ Build.log.step("合并并拷贝framework(#{File.basename(self.path)})")
54
+ Build.log.debug(command)
55
+
56
+ res = system command
57
+
58
+ unless res == true
59
+ error = "合并framework失败"
60
+ pusher.push("❌ 打包失败", error)
61
+ Process.exit(-1)
62
+ end
63
+ end
64
+
65
+ def copy_resources(repo_xcode_package, repo_module_source)
66
+ Build.log.step("拷贝资源文件到framework中(#{File.basename(self.path)})")
67
+ bundle_path_array = repo_xcode_package.resource_bundles_paths
68
+ resource_paths = repo_xcode_package.resource_paths
69
+
70
+ if bundle_path_array.length == 0
71
+ Build.log.detail("源码podspec里未指定resource_bundles")
72
+ end
73
+ if resource_paths.length == 0
74
+ Build.log.detail("源码podspec里未指定resource")
75
+ end
76
+
77
+ result_array = bundle_path_array + resource_paths
78
+ result_array.each { |p| Build.log.debug("资源文件: #{p}") }
79
+ if result_array.length == 0
80
+ Build.log.detail("无资源文件,不拷贝")
81
+ return
82
+ end
83
+
84
+ # 获取framework路径
85
+ framework_path_array = Array.new
86
+ framework = "#{repo_module_source.framework_name}.framework"
87
+ FileHelper.recursion_find(self.path, framework, framework_path_array)
88
+ framework_path_array.each { |p| Build.log.debug("目标framework: #{p}") }
89
+
90
+ # 拷贝到目标路径
91
+ framework_path_array.each { |framework_path|
92
+ result_array.each { |bundle_path|
93
+ bundle_name = File.basename(bundle_path)
94
+ des_bundle_path = "#{framework_path}/#{bundle_name}"
95
+ # 如果不存在 则拷贝
96
+ unless File.exist?(des_bundle_path)
97
+ FileUtils.cp_r(bundle_path, framework_path)
98
+ end
99
+ }
100
+ }
101
+ Build.log.detail("资源文件拷贝完成")
102
+ end
103
+
104
+ def copy_podspec(repo_xcode_package, repo_module_source)
105
+
106
+ build_real_device = build_item.real_device
107
+ build_simulator = build_item.simulator
108
+ if not build_real_device and not build_simulator
109
+ return
110
+ end
111
+
112
+ Build.log.step("拷贝podspec文件到framework中(#{File.basename(self.path)})")
113
+ @podspec_json_path = "#{self.path}/#{self.pod_name}.podspec.json"
114
+
115
+ # xcframework 内 slice 目录名不固定(不同Xcode/构建参数产物不同),必须动态解析:
116
+ # 真机: 通常 ios-arm64
117
+ # 模拟器: 旧Intel-only为 ios-x86_64-simulator;Apple Silicon fat为 ios-arm64_x86_64-simulator;仅arm64为 ios-arm64-simulator
118
+ # 本方法返回的是 podspec resources glob 用的 slice 目录名(真机/模拟器二选一)
119
+ source_files_dir = xcframework_slice_dir(device: build_real_device, simulator: build_simulator)
120
+ unless source_files_dir
121
+ error = "无法在#{framework_binary_path}中解析到#{build_real_device ? '真机' : '模拟器'}slice目录"
122
+ pusher.push("❌ 打包失败", error)
123
+ Process.exit(-1)
124
+ end
125
+
126
+ # 原始podspec
127
+ @podspec_content = repo_module_source.podspec_content
128
+
129
+ podspec_content.delete("license")
130
+ podspec_content.delete("module_map")
131
+ podspec_content.delete("public_header_files")
132
+ podspec_content.delete("private_header_files")
133
+ podspec_content.delete("exclude_files")
134
+ podspec_content.delete("prefix_header_file")
135
+ # 二进制包仅含 xcframework,没有源码,prepare_command 既无意义也无处执行;
136
+ # 保留会导致安装期(如 SQLCipher 的 ./configure)在二进制仓 checkout 目录
137
+ # 因脚本不存在直接报错,因此所有组件的二进制 spec 统一移除
138
+ podspec_content.delete("prepare_command")
139
+
140
+ podspec_content["source"] = {} #二进制后, 只从tag读取,去除其他的标志信息
141
+ podspec_content["source"]["git"] = self.ssh_url
142
+ podspec_content["source"]["tag"] = self.pod_version_final
143
+
144
+ podspec_content["source_files"] = "**/*/#{source_files_dir}/*.framework/Headers/*.h"
145
+ podspec_content["version"] = self.pod_version_final
146
+ podspec_content["vendored_frameworks"] = "*.xcframework"
147
+
148
+ # 资源统一汇总root及所有subspec(多subspec整包构建后资源都在同一个framework内)
149
+ podspec_content["resources"] = []
150
+
151
+ # 1) 松散resources/resource glob:按glob在源码工作副本中找到真实文件后映射进framework
152
+ repo_module_source.all_resource_globs.each do |resource|
153
+ # 子目录podspec时先按podspec所在目录匹配,再回退仓库根目录
154
+ search_roots = [repo_module_source.component_root_dir, repo_module_source.path].uniq
155
+ real_paths = search_roots.flat_map { |root| Dir["#{root}/#{resource}"] }.uniq
156
+ real_paths.each do |real_path|
157
+ resource_name = File.basename(real_path)
158
+ podspec_content["resources"] << "**/*/#{source_files_dir}/*.framework/#{resource_name}"
159
+ end
160
+ end
161
+ podspec_content.delete("resource")
162
+
163
+ # 2) resource_bundles:root及各subspec声明的bundle名(整包构建后均在framework内)
164
+ repo_module_source.all_resource_bundle_names.each do |bundle_name|
165
+ podspec_content["resources"] << "**/*/#{source_files_dir}/*.framework/#{bundle_name}.bundle"
166
+ end
167
+ podspec_content.delete("resource_bundles")
168
+ podspec_content.delete("resource_bundle")
169
+ podspec_content["resources"].uniq!
170
+
171
+ # 资源文件-podspec
172
+ podspec_content["resources"] << "**/*/#{source_files_dir}/*.framework/*.podspec.json"
173
+
174
+ # 二进制扩展信息:源码podspec已带build_metadata基础字段,这里补充产物相关信息
175
+ metadata = podspec_content[PODSPEC_METADATA_KEY] || {}
176
+ # 代码地址改为二进制仓库地址
177
+ metadata["address"] = self.http_url
178
+ metadata["archive"] = repo_xcode_package.debug_mode ? "Debug" : "Release"
179
+ # 组件在整体App包中的实际占用体积(只统计真机slice的framework,排除dSYM/模拟器slice/podspec)
180
+ metadata["framework_size"] = framework_size_format
181
+ # 是否SNAPSHOT包
182
+ metadata["snapshot"] = build_item.snapshot
183
+ # xcframework实际包含的CPU架构,按真机/模拟器分类
184
+ metadata["architectures"] = xcframework_architectures
185
+ # 构建所用Xcode版本,如 16.4 (16F6)
186
+ metadata["xcode_version"] = xcode_version
187
+ # xcframework产物中是否包含dSYM调试符号
188
+ metadata["include_dsym"] = include_dsym?
189
+ # xcframework内Mach-O二进制的SHA256,用于产物完整性校验/比对
190
+ metadata["framework_checksum"] = xcframework_checksum
191
+ podspec_content[PODSPEC_METADATA_KEY] = metadata
192
+
193
+ # swift
194
+ if podspec_content.has_key?("swift_version")
195
+ podspec_content["pod_target_xcconfig"] = {"BUILD_LIBRARY_FOR_DISTRIBUTION": true}
196
+ end
197
+
198
+ # 处理子仓库
199
+ subspecs = podspec_content["subspecs"]
200
+ if subspecs.respond_to?(:each)
201
+ subspecs.each do |subspec|
202
+ subspec.delete("resources")
203
+ subspec.delete("resource_bundles")
204
+ subspec.delete("exclude_files")
205
+ subspec.delete("public_header_files")
206
+ subspec.delete("project_header_files")
207
+ if subspec["ios"] and subspec["ios"].class == Hash
208
+ sun = subspec["ios"]
209
+ sun.delete("resources")
210
+ sun.delete("resource_bundles")
211
+ sun.delete("exclude_files")
212
+ sun.delete("public_header_files")
213
+ sun.delete("project_header_files")
214
+ sun.delete("source_files")
215
+ end
216
+ subspec["source_files"] = "placeholder.m"
217
+ end
218
+ end
219
+
220
+ ## 重写文件
221
+ new_file = File.new(podspec_json_path, "w+")
222
+ if new_file
223
+ new_file.syswrite(JSON.pretty_generate(podspec_content))
224
+ end
225
+
226
+ # 拷贝podspec文件到实际存在的真机/模拟器slice(目录名动态解析)
227
+ [xcframework_slice_dir(device: true), xcframework_slice_dir(simulator: true)].compact.uniq.each do |slice_dir|
228
+ framework_slice_path = File.join(framework_binary_path, slice_dir, "#{framework_name}.framework")
229
+ FileUtils.cp(podspec_json_path, framework_slice_path) if File.directory?(framework_slice_path)
230
+ end
231
+ Build.log.detail("framework 产物拷贝完成")
232
+ end
233
+
234
+ # 从xcframework的Info.plist解析真机/模拟器slice的目录名(LibraryIdentifier)。
235
+ # 兼容三种命名: ios-arm64(真机) / ios-x86_64-simulator(旧Intel) /
236
+ # ios-arm64_x86_64-simulator(fat) / ios-arm64-simulator(仅arm64)
237
+ # @return [String, nil] slice目录名
238
+ def xcframework_slice_dir(device: false, simulator: false)
239
+ @xcframework_slices ||= parse_xcframework_slices
240
+ if simulator
241
+ @xcframework_slices[:simulator]
242
+ elsif device
243
+ @xcframework_slices[:device]
244
+ end
245
+ end
246
+
247
+ def parse_xcframework_slices
248
+ slices = { device: nil, simulator: nil }
249
+ plist_path = File.join(framework_binary_path, 'Info.plist')
250
+ json_text = `plutil -convert json -o - "#{plist_path}" 2>/dev/null`
251
+ libraries = JSON.parse(json_text)["AvailableLibraries"] || []
252
+ libraries.each do |library|
253
+ identifier = library["LibraryIdentifier"].to_s
254
+ if library["SupportedPlatformVariant"] == "simulator"
255
+ slices[:simulator] ||= identifier
256
+ elsif library["SupportedPlatform"] == "ios"
257
+ slices[:device] ||= identifier
258
+ end
259
+ end
260
+ slices
261
+ rescue => e
262
+ Build.log.warn("解析xcframework slice目录失败(#{framework_binary_path}):#{e.message}")
263
+ slices
264
+ end
265
+
266
+ # 解析xcframework实际包含的CPU架构,按真机/模拟器分类汇总
267
+ # @return [Hash] 如 {"device"=>["arm64"], "simulator"=>["arm64"]}
268
+ def xcframework_architectures
269
+ result = {"device" => [], "simulator" => []}
270
+ plist_path = "#{framework_binary_path}/Info.plist"
271
+
272
+ begin
273
+ # 使用系统自带 plutil 将 Info.plist 转成 JSON 读取 AvailableLibraries
274
+ json_text = `plutil -convert json -o - "#{plist_path}" 2>/dev/null`
275
+ libraries = JSON.parse(json_text)["AvailableLibraries"] || []
276
+ libraries.each do |library|
277
+ # xcframework中真机/模拟器的 SupportedPlatform 均为 "ios",
278
+ # 模拟器slice通过 SupportedPlatformVariant == "simulator" 区分
279
+ if library["SupportedPlatformVariant"] == "simulator"
280
+ platform = "simulator"
281
+ elsif library["SupportedPlatform"] == "ios"
282
+ platform = "device"
283
+ else
284
+ next
285
+ end
286
+ (library["SupportedArchitectures"] || []).each { |arch| result[platform] << arch }
287
+ end
288
+ rescue => exception
289
+ Build.log.warn("解析xcframework架构信息失败:#{exception.message},将按本次构建开关兜底为arm64")
290
+ end
291
+
292
+ # 兜底:plist解析失败时按本次构建开关记录arm64
293
+ if result["device"].empty? && result["simulator"].empty?
294
+ result["device"] = ["arm64"] if build_item.real_device
295
+ result["simulator"] = ["arm64"] if build_item.simulator
296
+ end
297
+
298
+ result.transform_values { |archs| archs.uniq.sort }
299
+ end
300
+
301
+ # 组件在整体App包中的实际占用体积:
302
+ # - framework组件:只统计真机slice的Foo.framework(Mach-O二进制+资源bundle+Info.plist),
303
+ # 排除dSYM、模拟器slice、podspec.json等不会进入App包的内容;仅模拟器构建时退化为统计模拟器slice。
304
+ # 注意这是"解压后占用"口径,非App Store压缩后的下载体积
305
+ # - 纯头文件组件:头文件仅参与编译,不进入App包,体积贡献为0
306
+ # - 解析不到framework目录时视为产物结构异常,直接打包失败
307
+ # @return [String] 体积描述(KB/MB格式)
308
+ def framework_size_format
309
+ return "0KB" if build_item.header_only
310
+
311
+ # 真机包才是发布给用户的产物;仅模拟器构建时用模拟器slice兜底
312
+ slice = xcframework_slice_dir(device: true) || xcframework_slice_dir(simulator: true)
313
+ framework_dir = slice ? File.join(framework_binary_path, slice, "#{framework_name}.framework") : nil
314
+
315
+ unless framework_dir && File.directory?(framework_dir)
316
+ error = "无法在#{framework_binary_path}中解析到进App包的framework目录,产物结构异常"
317
+ pusher.push("❌ 打包失败", error)
318
+ Process.exit(-1)
319
+ end
320
+
321
+ total = Dir.glob(File.join(framework_dir, '**', '*'))
322
+ .select { |f| File.file?(f) }
323
+ .sum { |f| File.size(f).to_f / 1024 }
324
+ total > 1024 ? "#{(total * 0.001).round(1)}MB" : "#{total.round(0)}KB"
325
+ end
326
+
327
+ # xcframework产物中是否包含dSYM调试符号
328
+ # @return [Boolean]
329
+ def include_dsym?
330
+ !Dir.glob("#{framework_binary_path}/**/*.dSYM").empty?
331
+ end
332
+
333
+ # 本次构建使用的Xcode版本,如 "16.4 (16F6)",获取失败返回空字符串
334
+ # @return [String]
335
+ def xcode_version
336
+ output = `xcodebuild -version 2>/dev/null`.strip
337
+ version = output[/Xcode\s+(\S+)/, 1]
338
+ build = output[/Build version\s+(\S+)/, 1]
339
+ return "" if version.nil?
340
+ build ? "#{version} (#{build})" : version
341
+ end
342
+
343
+ # 计算xcframework内各slice的Mach-O可执行文件的合并SHA256。
344
+ # 只摘要真实二进制内容(按相对路径排序),不受Info.plist时间戳/文件修改时间影响,
345
+ # 可用于产物完整性校验以及判断两次构建的二进制是否实质一致。
346
+ # @return [String] 64位SHA256十六进制串,无可用二进制时返回空字符串
347
+ def xcframework_checksum
348
+ # 各slice的可执行文件路径形如: ios-arm64/Foo.framework/Foo
349
+ binary_paths = Dir.glob("#{framework_binary_path}/**/*.framework/*").select do |file_path|
350
+ File.file?(file_path) && File.basename(file_path) == framework_name
351
+ end.sort
352
+
353
+ return "" if binary_paths.empty?
354
+
355
+ digest = Digest::SHA256.new
356
+ binary_paths.each do |binary_path|
357
+ relative_path = Pathname.new(binary_path).relative_path_from(Pathname.new(framework_binary_path)).to_s
358
+ digest.update(relative_path)
359
+ digest.update(File.binread(binary_path))
360
+ end
361
+ digest.hexdigest
362
+ end
363
+
364
+ # 纯头文件组件的二进制发布:二进制仓直接放头文件(保持目录结构) + 普通形态podspec.json。
365
+ # header-only无需编译,二进制形态与源码等价,不做任何framework转换
366
+ def publish_header_only(repo_module_source)
367
+ return unless build_item.header_only
368
+
369
+ Build.log.step("纯头文件组件发布(#{File.basename(self.path)})")
370
+
371
+ copy_header_only_files(repo_module_source)
372
+ write_header_only_podspec(repo_module_source)
373
+
374
+ Build.log.detail("纯头文件组件头文件拷贝完成")
375
+ end
376
+
377
+ private
378
+
379
+ # 按podspec的source_files(root+subspecs)把头文件从源码工作副本按相对目录拷到二进制仓
380
+ def copy_header_only_files(repo_module_source)
381
+ podspec_content = repo_module_source.podspec_content
382
+ globs = Array(podspec_content["source_files"])
383
+ Array(podspec_content["subspecs"]).each do |subspec|
384
+ globs.concat(Array(subspec["source_files"]))
385
+ end
386
+
387
+ source_root = repo_module_source.component_root_dir
388
+ copied = 0
389
+ globs.flatten.compact.uniq.each do |pattern|
390
+ # 与判定逻辑一致:source_files 指向目录时递归取其中文件,glob/单文件照常展开
391
+ FileHelper.source_files_under(File.join(source_root, pattern)).each do |src|
392
+ relative = Pathname.new(src).relative_path_from(Pathname.new(source_root)).to_s
393
+ dest = File.join(self.path, relative)
394
+ FileUtils.mkdir_p(File.dirname(dest))
395
+ FileUtils.cp(src, dest)
396
+ copied += 1
397
+ end
398
+ end
399
+ Build.log.detail("纯头文件组件共拷贝#{copied}个文件到二进制仓")
400
+ end
401
+
402
+ # 写普通形态podspec.json:source指向二进制仓tag,保留source_files等原字段
403
+ def write_header_only_podspec(repo_module_source)
404
+ # 深拷贝,避免污染源码侧的podspec_content
405
+ content = JSON.parse(JSON.generate(repo_module_source.podspec_content))
406
+
407
+ content.delete("license")
408
+ content["source"] = { "git" => self.ssh_url, "tag" => self.pod_version_final }
409
+ content["version"] = self.pod_version_final
410
+ # 纯头组件不涉及framework,删除仅编译产物相关字段,保留 source_files/public_header_files 等
411
+ content.delete("vendored_frameworks")
412
+
413
+ metadata = content[PODSPEC_METADATA_KEY] || {}
414
+ metadata["address"] = self.http_url
415
+ metadata["archive"] = "HeadersOnly"
416
+ metadata["snapshot"] = build_item.snapshot
417
+ # 头文件仅参与编译,不进入App包,组件在整体包大小中的实际占用为0
418
+ metadata["framework_size"] = "0KB"
419
+ metadata["architectures"] = { "device" => [], "simulator" => [] }
420
+ metadata["include_dsym"] = false
421
+ metadata["framework_checksum"] = ""
422
+ content[PODSPEC_METADATA_KEY] = metadata
423
+
424
+ @podspec_json_path = File.join(self.path, "#{self.pod_name}.podspec.json")
425
+ File.write(@podspec_json_path, JSON.pretty_generate(content))
426
+ # 供二进制仓 git_commit 读取 build_metadata 生成提交信息
427
+ @podspec_content = content
428
+ end
429
+
430
+ public
431
+
432
+ def git_tag
433
+ if build_item.real_device or build_item.simulator or build_item.header_only
434
+ super
435
+ end
436
+ end
437
+
438
+ end
439
+ end