cocoapods-bb-PodAssistant 0.3.13.9 → 0.3.14.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abec00e48ec639b98614704c2de363bcdbc877221b5a1b353daec7f52bdaf19e
4
- data.tar.gz: 70064497c44c232d192d21714910e091d1791cf3174d6031f0261ef02022aa21
3
+ metadata.gz: e3f29af4e66656505b3e34fec77037f661dda9e6668973eb928dac26395cbbd8
4
+ data.tar.gz: d6eaa9e0d9eb55b828710b1163537c25849fbe31a5db6e32c381b2e6f803d65a
5
5
  SHA512:
6
- metadata.gz: a0c0a04aee990b1a4da82c388a3762077d55dafe774a83c050de1c81b9e82ba80ff01d516f5b4b7cbc1ffbe108a7f1bafdd4e6b414caaf4721e44e01b5890a1e
7
- data.tar.gz: 32b823a32a8ebaebe6bb9547e772a2fda950e5c7d3263f9f07a6e973acfc3956493a812e9c506f1ecdecda05acd20c51b0e39318745d999360e69bf7abbca3d4
6
+ metadata.gz: 8caa9d87d55d008b931d17100b60ded1871c7027a83d5c102ae139066efe2e2f38602c3b332bd34b81a20e92d0e30dcd5f9e60dfeeaacb642ac71c960bb40eeb
7
+ data.tar.gz: a0234edf691867633425b1a7e26dd57368f206a0e7bd7caa190b3d7359bc4197625d294e1c38cc36a02832b57ce6e992e2a8918c060156bdbb2c3009768c2109
data/bin/bb_tools CHANGED
@@ -96,4 +96,27 @@ command :podfile_tiled do |c|
96
96
  BBItools::PodfileTiled.podfile_tiled(args)
97
97
  end
98
98
  end
99
- exit run(ARGV)
99
+
100
+ # 生成 DocC 静态站点
101
+ desc "从 pod lib lint 的临时验证工程生成 DocC 静态站点"
102
+ arg 'podspec path'
103
+ command :docc do |c|
104
+ c.desc "DocC hosting base path (默认: Docs/<scheme>)"
105
+ c.flag [:"hosting-base-path"]
106
+
107
+ c.action do |global_options, options, args|
108
+ if args.empty?
109
+ puts "❌ 错误: 必须提供 podspec 文件路径".red
110
+ puts "用法: bb_tools docc <podspec>"
111
+ exit 1
112
+ end
113
+
114
+ gen_options = {
115
+ podspec: args[0],
116
+ hosting_base_path: options[:"hosting-base-path"] || options["hosting-base-path"]
117
+ }
118
+
119
+ BBItools::DocCGenerator.generate(gen_options)
120
+ end
121
+ end
122
+ exit run(ARGV)
@@ -18,7 +18,7 @@ module Pod
18
18
  参数说明:\n
19
19
  --dependencies=依赖组件\n
20
20
  --remove=需要移除组件\n
21
- --stable-specs-index=指定业务specs索引-0:公共(默认) 1:矩阵全线升级 2:拉布 3:科学 4:思维 5:abc 6:大全 7:世界\n
21
+ --stable-specs-index=指定业务specs索引-0:公共(默认) 1:矩阵全线升级 2:拉布 3:科学 4:思维 5:abc 6:大全 7:世界 8:阅读\n
22
22
  DESC
23
23
  # --update-matrix 更新矩阵产品公共业务线\n
24
24
  # --update-common 更新公共组件\n
@@ -31,7 +31,7 @@ module Pod
31
31
  [
32
32
  ['--dependencies', '依赖组件名称,多个使用`,`隔开'],
33
33
  ['--remove', '移除公共组件名称,多个使用`,`隔开'],
34
- ['--stable-specs-index', '指定业务specs索引-0:公共(默认) 1:矩阵全线升级 2:拉布 3:科学 4:思维 5:abc 6:大全 7:世界,参照podfile配置信息'],
34
+ ['--stable-specs-index', '指定业务specs索引-0:公共(默认) 1:矩阵全线升级 2:拉布 3:科学 4:思维 5:abc 6:大全 7:世界 8:阅读,参照podfile配置信息'],
35
35
  ].concat(super)
36
36
  end
37
37
 
@@ -97,7 +97,7 @@ module Pod
97
97
  end
98
98
  }
99
99
  end
100
- # 0:公共(默认) 1:矩阵全线升级 2:拉布 3:科学 4:思维 5:abc 6:大全 7:世界
100
+ # 0:公共(默认) 1:矩阵全线升级 2:拉布 3:科学 4:思维 5:abc 6:大全 7:世界 8:阅读
101
101
  private def get_stable_spec_name
102
102
  case @stable_specs_index
103
103
  when 1 # 全线矩阵产品公共stable目录
@@ -114,11 +114,13 @@ module Pod
114
114
  return 'br_stable_specs'
115
115
  when 7 # 世界产品公共stable目录
116
116
  return 'bw_stable_specs'
117
+ when 8 # 阅读产品公共stable目录
118
+ return 'read_stable_specs'
117
119
  else # 公共stable目录
118
120
  return 'stable_specs'
119
121
  end
120
122
  end
121
- # 0:公共(默认) 1:矩阵全线升级 2:拉布 3:科学 4:思维 5:abc 6:大全 7:世界
123
+ # 0:公共(默认) 1:矩阵全线升级 2:拉布 3:科学 4:思维 5:abc 6:大全 7:世界 8:阅读
122
124
  private def get_stable_project_name
123
125
  case @stable_specs_index
124
126
  when 1 # 全线矩阵产品公共stable目录
@@ -135,6 +137,8 @@ module Pod
135
137
  return 'br_Common_Stable'
136
138
  when 7 # 世界产品公共stable目录
137
139
  return 'bw_Common_Stable'
140
+ when 8 # 阅读产品公共stable目录
141
+ return 'read_Common_Stable'
138
142
  else # 公共stable目录
139
143
  @is_public_data = true
140
144
  return 'Common_Stable'
@@ -1,3 +1,3 @@
1
1
  module CocoapodsBbPodassistant
2
- VERSION = "0.3.13.9"
2
+ VERSION = "0.3.14.1"
3
3
  end
@@ -0,0 +1,1005 @@
1
+ require "fileutils"
2
+ require "find"
3
+ require "pathname"
4
+ require "timeout"
5
+
6
+ module BBItools
7
+ class DocCGenerator
8
+ PODSPEC_REPO = "babybus-babybus-ios-specs-babybus-specs"
9
+ DESTINATION = "generic/platform=iOS Simulator"
10
+ CONFIGURATION = "Debug"
11
+ KEYCHAIN_HOST = "git.babybus.co"
12
+
13
+ def self.generate(options)
14
+ new(options).run
15
+ end
16
+
17
+ def initialize(options)
18
+ @options = options
19
+ @podspec = options[:podspec]
20
+ @workdir = options[:workdir]
21
+ @output = options[:output]
22
+ @logs = options[:logs]
23
+ @derived_data = options[:derived_data]
24
+ @lint_root = options[:lint_root]
25
+ @hosting_base_path = options[:hosting_base_path]
26
+ end
27
+
28
+ def run
29
+ $stdout.sync = true
30
+ $stderr.sync = true
31
+
32
+ fail!("必须传入 podspec") if @podspec.to_s.strip.empty?
33
+
34
+ base_dir = Dir.pwd
35
+ podspec_path_abs = abs_path(@podspec.to_s.strip, base_dir: base_dir)
36
+ fail!("podspec 不存在: #{@podspec}") unless File.file?(podspec_path_abs)
37
+ podspec_path_rel = rel_path(podspec_path_abs, base_dir: base_dir)
38
+
39
+ podspec_basename = File.basename(podspec_path_abs)
40
+ workdir_opt = @workdir.to_s.strip
41
+ podspec_dir_abs =
42
+ if workdir_opt.empty?
43
+ File.dirname(podspec_path_abs)
44
+ else
45
+ abs_path(workdir_opt, base_dir: base_dir)
46
+ end
47
+ fail!("workdir 不存在: #{rel_path(podspec_dir_abs, base_dir: base_dir)}") unless File.directory?(podspec_dir_abs)
48
+
49
+ # 如果指定了 workdir,且 podspec 不在其中,则复制过去
50
+ podspec_in_workdir = File.join(podspec_dir_abs, podspec_basename)
51
+ if File.expand_path(podspec_in_workdir) != File.expand_path(podspec_path_abs)
52
+ FileUtils.cp(podspec_path_abs, podspec_in_workdir)
53
+ end
54
+
55
+ zip_push_repo = ENV["BB_DOCC_ZIP_PUSH_REPO"].to_s.strip
56
+ zip_push_repo = "https://git.babybus.co/babybus/ios/CommonBusiness/Tools/iOSWeb" if zip_push_repo.empty?
57
+ zip_push_branch = ENV["BB_DOCC_ZIP_PUSH_BRANCH"].to_s.strip
58
+ zip_push_branch = "main" if zip_push_branch.empty?
59
+ zip_push_subdir = ENV["BB_DOCC_ZIP_PUSH_SUBDIR"].to_s.strip
60
+ zip_push_subdir = "docc_zips" if zip_push_subdir.empty?
61
+
62
+ final_destination_root = nil
63
+ real_home = ENV["HOME"].to_s
64
+ scheme = extract_name_from_podspec(podspec_path_abs)
65
+ deployment_target = extract_ios_deployment_target_from_podspec(podspec_path_abs)
66
+
67
+ # ---------------------------------------------------------
68
+ # 2. 所有操作都在脚本同级目录下的 _docc_temp 中进行
69
+ # ---------------------------------------------------------
70
+ # 脚本所在目录 (Gem 安装位置可能只读,所以使用当前运行目录 Dir.pwd)
71
+ local_work_root = File.join(Dir.pwd, "_docc_temp", scheme)
72
+ FileUtils.rm_rf(local_work_root) # 清理旧的
73
+ FileUtils.mkdir_p(local_work_root)
74
+
75
+ log "🔨 本地工作区 (生成中): file://#{local_work_root}"
76
+
77
+ # 各个子目录都在本地工作区内
78
+ logs_dir = File.join(local_work_root, "logs")
79
+ FileUtils.mkdir_p(logs_dir)
80
+
81
+ derived_data_root = File.join(local_work_root, "DerivedData")
82
+ FileUtils.mkdir_p(derived_data_root)
83
+
84
+ lint_root = File.join(local_work_root, "Lint")
85
+ FileUtils.mkdir_p(lint_root)
86
+ lint_root_rel = lint_root
87
+
88
+ # 最终生成的站点先放在本地
89
+ local_site_output = File.join(local_work_root, "site_output")
90
+ FileUtils.mkdir_p(local_site_output)
91
+
92
+ # 模拟 component_dir_abs 供后续逻辑使用 (指向本地)
93
+ component_dir_abs = local_site_output
94
+ component_dir_rel = component_dir_abs
95
+
96
+ hosting_base_path = @options[:hosting_base_path].to_s.strip
97
+ if hosting_base_path.empty?
98
+ hosting_base_path = "Docs/#{scheme}"
99
+ end
100
+ hosting_base_path = hosting_base_path.sub(%r{\A/+}, "")
101
+
102
+ status_md = File.join(logs_dir, "status.md")
103
+ File.open(status_md, "w") do |f|
104
+ f.puts "# #{scheme}"
105
+ f.puts
106
+ f.puts "- 时间: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}"
107
+ f.puts "- podspec: #{podspec_path_rel}"
108
+ f.puts "- workdir: #{rel_path(podspec_dir_abs, base_dir: base_dir)}" unless workdir_opt.empty?
109
+ f.puts "- local_work_root: #{local_work_root}"
110
+ f.puts "- zip_push_repo: #{redact_git_url(zip_push_repo)}"
111
+ f.puts "- zip_push_branch: #{zip_push_branch}"
112
+ f.puts "- zip_push_subdir: #{zip_push_subdir}"
113
+ f.puts "- home: ~"
114
+ f.puts "- deployment_target: #{deployment_target}" unless deployment_target.to_s.strip.empty?
115
+ f.puts "- hosting_base_path: #{hosting_base_path}"
116
+ f.puts "- sources: #{PODSPEC_REPO}"
117
+ end
118
+
119
+ exported_source_root = File.join(component_dir_abs, "exported-source")
120
+ export_staging_dir = ""
121
+ cleanup_written = false
122
+ is_success = false
123
+
124
+ # Cleanup hook
125
+ at_exit_block = proc do
126
+ next if cleanup_written
127
+ cleanup_written = true
128
+
129
+ exit_code = $!.is_a?(SystemExit) ? $!.status : 0
130
+
131
+ # ---------------------------------------------------------
132
+ # 3. 任务结束:移动产物到 SMB 并清理本地
133
+ # ---------------------------------------------------------
134
+ if final_destination_root
135
+ # 只有明确标记成功 (is_success = true) 才执行同步
136
+ if is_success && exit_code.zero? && File.directory?(local_site_output)
137
+ # 关键校验:检查目标目录下是否存在 server.js
138
+ server_js_path = File.join(final_destination_root, "server.js")
139
+
140
+ if File.file?(server_js_path)
141
+ log "✅ 校验通过:发现 server.js"
142
+
143
+ # ---------------------------------------------------------
144
+ # 压缩 -> 移动 -> 删除旧版 -> 解压 -> 删除压缩包
145
+ # ---------------------------------------------------------
146
+
147
+ # 1. 准备压缩:将 site_output 重命名为 #{scheme},这样解压后直接就是正确的目录名
148
+ staging_dir = File.join(local_work_root, scheme)
149
+ FileUtils.rm_rf(staging_dir)
150
+ FileUtils.mv(local_site_output, staging_dir)
151
+
152
+ zip_name = "#{scheme}.zip"
153
+ local_zip_path = File.join(local_work_root, zip_name)
154
+
155
+ remote_zip_name = "#{scheme}_#{Time.now.strftime("%Y%m%d_%H%M%S")}_#{Process.pid}.zip"
156
+
157
+ zip_log = File.join(logs_dir, "zip.log")
158
+ zip_timeout_sec = (ENV["BB_DOCC_ZIP_TIMEOUT_SEC"].to_s.strip.empty? ? 1800 : ENV["BB_DOCC_ZIP_TIMEOUT_SEC"].to_i)
159
+
160
+ log "📦 正在压缩产物 (zip)... (timeout=#{zip_timeout_sec}s)"
161
+ log "📄 zip 日志: #{rel_path(zip_log, base_dir: base_dir)}"
162
+
163
+ zip_exit = nil
164
+ FileUtils.mkdir_p(File.dirname(zip_log))
165
+ File.open(zip_log, "w") do |f|
166
+ Dir.chdir(local_work_root) do
167
+ pid = Process.spawn({ "HOME" => real_home }, "zip", "-r", "-q", zip_name, scheme, out: f, err: f)
168
+ begin
169
+ Timeout.timeout(zip_timeout_sec) do
170
+ _pid, status = Process.wait2(pid)
171
+ zip_exit = status.exitstatus
172
+ end
173
+ rescue Timeout::Error
174
+ begin
175
+ Process.kill("TERM", pid)
176
+ rescue StandardError
177
+ end
178
+ sleep 2
179
+ begin
180
+ Process.kill("KILL", pid)
181
+ rescue StandardError
182
+ end
183
+ zip_exit = 124
184
+ end
185
+ end
186
+ end
187
+
188
+ log "📦 压缩结束 (exit=#{zip_exit})"
189
+
190
+ if zip_exit == 0 && File.exist?(local_zip_path)
191
+ lock_dir = File.join(final_destination_root, ".docc_deploy_lock_#{scheme}")
192
+ lock_timeout_sec = (ENV["BB_DOCC_DEPLOY_LOCK_TIMEOUT_SEC"].to_s.strip.empty? ? 600 : ENV["BB_DOCC_DEPLOY_LOCK_TIMEOUT_SEC"].to_i)
193
+ lock_stale_sec = (ENV["BB_DOCC_DEPLOY_LOCK_STALE_SEC"].to_s.strip.empty? ? 21600 : ENV["BB_DOCC_DEPLOY_LOCK_STALE_SEC"].to_i)
194
+
195
+ lock_start = Time.now
196
+ acquired_lock = false
197
+
198
+ until acquired_lock
199
+ begin
200
+ Dir.mkdir(lock_dir)
201
+ acquired_lock = true
202
+ rescue Errno::EEXIST
203
+ begin
204
+ if File.exist?(lock_dir) && (Time.now - File.mtime(lock_dir)) > lock_stale_sec
205
+ FileUtils.rm_rf(lock_dir)
206
+ next
207
+ end
208
+ rescue StandardError
209
+ end
210
+
211
+ if (Time.now - lock_start) > lock_timeout_sec
212
+ log "❌ 获取部署锁超时 (timeout=#{lock_timeout_sec}s):#{lock_dir}"
213
+ break
214
+ end
215
+ sleep 2
216
+ end
217
+ end
218
+
219
+ begin
220
+ if acquired_lock
221
+ log "🚚 正在将压缩包移动到 SMB..."
222
+ target_zip_path = File.join(final_destination_root, remote_zip_name)
223
+
224
+ copy_log = File.join(logs_dir, "copy.log")
225
+ copy_timeout_sec = (ENV["BB_DOCC_COPY_TIMEOUT_SEC"].to_s.strip.empty? ? 600 : ENV["BB_DOCC_COPY_TIMEOUT_SEC"].to_i)
226
+
227
+ log "🚚 正在复制到 SMB... (timeout=#{copy_timeout_sec}s)"
228
+ log "📄 copy 日志: #{rel_path(copy_log, base_dir: base_dir)}"
229
+
230
+ copy_exit = nil
231
+ File.open(copy_log, "w") do |f|
232
+ pid = Process.spawn({ "HOME" => real_home }, "cp", "-f", local_zip_path, target_zip_path, out: f, err: f)
233
+ begin
234
+ Timeout.timeout(copy_timeout_sec) do
235
+ _pid, status = Process.wait2(pid)
236
+ copy_exit = status.exitstatus
237
+ end
238
+ rescue Timeout::Error
239
+ begin
240
+ Process.kill("TERM", pid)
241
+ rescue StandardError
242
+ end
243
+ sleep 2
244
+ begin
245
+ Process.kill("KILL", pid)
246
+ rescue StandardError
247
+ end
248
+ copy_exit = 124
249
+ end
250
+ end
251
+
252
+ if copy_exit != 0
253
+ log "❌ 复制到 SMB 失败 (exit=#{copy_exit}):#{target_zip_path}"
254
+ else
255
+ target_dir = File.join(final_destination_root, scheme)
256
+
257
+ if File.exist?(target_dir)
258
+ log "🗑️ 发现旧版本,正在删除..."
259
+ FileUtils.rm_rf(target_dir)
260
+ end
261
+
262
+ unzip_log = File.join(logs_dir, "unzip.log")
263
+ unzip_timeout_sec = (ENV["BB_DOCC_UNZIP_TIMEOUT_SEC"].to_s.strip.empty? ? 600 : ENV["BB_DOCC_UNZIP_TIMEOUT_SEC"].to_i)
264
+
265
+ log "📦 正在解压并部署... (timeout=#{unzip_timeout_sec}s)"
266
+ log "📄 unzip 日志: #{rel_path(unzip_log, base_dir: base_dir)}"
267
+
268
+ unzip_exit = nil
269
+ File.open(unzip_log, "w") do |f|
270
+ Dir.chdir(final_destination_root) do
271
+ pid = Process.spawn({ "HOME" => real_home }, "unzip", "-q", "-o", remote_zip_name, out: f, err: f)
272
+ begin
273
+ Timeout.timeout(unzip_timeout_sec) do
274
+ _pid, status = Process.wait2(pid)
275
+ unzip_exit = status.exitstatus
276
+ end
277
+ rescue Timeout::Error
278
+ begin
279
+ Process.kill("TERM", pid)
280
+ rescue StandardError
281
+ end
282
+ sleep 2
283
+ begin
284
+ Process.kill("KILL", pid)
285
+ rescue StandardError
286
+ end
287
+ unzip_exit = 124
288
+ end
289
+ end
290
+ end
291
+
292
+ log "📦 解压结束 (exit=#{unzip_exit})"
293
+
294
+ FileUtils.rm_f(target_zip_path)
295
+
296
+ if unzip_exit == 0
297
+ target_logs = File.join(target_dir, "docc-logs")
298
+ FileUtils.mkdir_p(target_logs)
299
+ sync_dir_contents(logs_dir, target_logs)
300
+
301
+ log "✅ 部署完成: file://#{target_dir.gsub(' ', '%20')}"
302
+ else
303
+ log "❌ 解压失败 (exit=#{unzip_exit})"
304
+ end
305
+ end
306
+ end
307
+ ensure
308
+ FileUtils.rm_rf(lock_dir) if acquired_lock && File.exist?(lock_dir)
309
+ end
310
+ else
311
+ log "❌ 压缩失败,未找到 zip 文件"
312
+ end
313
+ else
314
+ log "❌ 校验失败:在目标目录未找到 server.js"
315
+ log "❌ 目标路径: #{final_destination_root}"
316
+ log "⚠️ 取消同步,生成的静态站点将被删除"
317
+ end
318
+ else
319
+ # ---------------------------------------------------------
320
+ # 失败处理:将日志同步到 Docs/log 目录
321
+ # ---------------------------------------------------------
322
+ log "⚠️ 任务失败或被中断,正在保存错误日志..."
323
+
324
+ # 确保 log 根目录存在
325
+ global_log_root = File.join(final_destination_root, "log")
326
+ if File.directory?(final_destination_root)
327
+ FileUtils.mkdir_p(global_log_root)
328
+
329
+ # 创建带时间戳的日志目录: Docs/log/Scheme_20230101_120000
330
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
331
+ fail_log_dir = File.join(global_log_root, "#{scheme}_#{timestamp}")
332
+ FileUtils.mkdir_p(fail_log_dir)
333
+
334
+ # 同步日志文件
335
+ if File.directory?(logs_dir)
336
+ sync_dir_contents(logs_dir, fail_log_dir)
337
+ log "📄 错误日志已保存至: file://#{fail_log_dir.gsub(' ', '%20')}"
338
+ else
339
+ log "❌ 无法保存日志:本地日志目录不存在"
340
+ end
341
+ else
342
+ log "❌ 无法保存日志:SMB 目标目录不可访问"
343
+ end
344
+ end
345
+ end
346
+
347
+ # 强制删除本地临时目录 (_docc_temp)
348
+ # local_work_root 是 _docc_temp/scheme
349
+ # 我们要删除的是 _docc_temp
350
+ temp_root = File.dirname(local_work_root)
351
+
352
+ if File.exist?(temp_root)
353
+ log "正在清理本地临时目录: file://#{temp_root}"
354
+ FileUtils.rm_rf(temp_root)
355
+
356
+ if File.exist?(temp_root)
357
+ log "⚠️ 清理失败,目录仍存在: file://#{temp_root}"
358
+ else
359
+ log "🗑️ 已清理本地临时目录"
360
+ end
361
+ end
362
+ end
363
+
364
+ # 注意:at_exit 是全局的,这里直接调用可能不太合适,最好是 ensure 块
365
+ # 但为了保持逻辑,我们使用 ensure
366
+
367
+ begin
368
+ require_cmd!("pod")
369
+ require_cmd!("xcodebuild")
370
+ require_cmd!("xcrun")
371
+ require_cmd!("git")
372
+
373
+ lint_log = File.join(logs_dir, "pod-lib-lint.log")
374
+ lint_log_rel = rel_path(lint_log, base_dir: base_dir)
375
+ lint_cmd = [
376
+ "pod", "lib", "lint", podspec_basename,
377
+ "--verbose", "--no-clean", "--skip-tests", "--skip-import-validation",
378
+ "--use-modular-headers", "--use-static-frameworks",
379
+ "--allow-warnings",
380
+ "--sources=#{PODSPEC_REPO}"
381
+ ]
382
+
383
+ File.open(status_md, "a") { |f| f.puts "- lint: #{lint_cmd.join(" ")}" }
384
+
385
+ log "执行 lint: #{podspec_basename}(cwd=#{rel_path(podspec_dir_abs, base_dir: base_dir)})"
386
+ lint_env = {
387
+ "HOME" => real_home,
388
+ "TMPDIR" => lint_root
389
+ }
390
+
391
+ # 移除所有手动注入的 GIT_CONFIG_ 配置,完全依赖用户本地环境的 Git 配置。
392
+ # 仅保留 GIT_TERMINAL_PROMPT=0 以防止在日志重定向模式下进程卡死。
393
+ # 如果用户本地 Keychain 配置正确,Git 会自动读取,无需我们干预。
394
+
395
+ if ENV["BB_DOCC_ALLOW_PROMPT"] != "1"
396
+ lint_env["GIT_TERMINAL_PROMPT"] = "0"
397
+ end
398
+
399
+ lint_exit = run_to_file(lint_cmd, cwd: podspec_dir_abs, env: lint_env, log_path: lint_log)
400
+
401
+ File.open(status_md, "a") do |f|
402
+ if lint_exit.zero?
403
+ f.puts "- lint: 成功"
404
+ else
405
+ f.puts "- lint: 失败(exit=#{lint_exit})"
406
+ end
407
+ f.puts
408
+ f.puts "**pod lib lint 日志(尾部)**"
409
+ f.puts "```"
410
+ tail_lines(lint_log, 200).each { |line| f.puts line }
411
+ f.puts "```"
412
+ end
413
+
414
+ unless lint_exit.zero?
415
+ File.open(status_md, "a") do |f|
416
+ f.puts "- docbuild: 跳过(lint 失败)"
417
+ f.puts "- export: 跳过(lint 失败)"
418
+ end
419
+
420
+ log "lint 失败,跳过 docbuild 与导出"
421
+ log "失败日志: #{lint_log_rel}"
422
+ log "状态汇总: #{rel_path(status_md, base_dir: base_dir)}"
423
+
424
+ puts "[docc] -------- pod lib lint 日志(尾部 200 行)--------"
425
+ puts tail_lines(lint_log, 200).join("\n")
426
+ puts "[docc] -------- 关键错误/失败行(尾部 400 行过滤)--------"
427
+
428
+ tail_lines(lint_log, 400).each_with_index do |line, idx|
429
+ next unless line.match?(/^\[!\]| error: | ERROR | fatal: |Undefined symbols|Ld /)
430
+ puts "#{idx + 1}:#{line}"
431
+ end
432
+
433
+ if safe_read_text(lint_log).include?("Authentication failed for 'https://#{KEYCHAIN_HOST}/")
434
+ puts "[docc] 提示: 检测到 git clone 认证失败(#{KEYCHAIN_HOST})"
435
+ puts "[docc] 处理方式: 确保系统 git 能在终端里无交互 clone 私有仓库(一般用 Token)"
436
+ end
437
+
438
+ # Early return instead of exit
439
+ return
440
+ end
441
+
442
+ workspace_path_abs = find_workspace_from_lint_log(lint_log, lint_root)
443
+
444
+ if workspace_path_abs.to_s.strip.empty?
445
+ fail!("未找到 *.xcworkspace(lint_root=#{lint_root_rel})")
446
+ end
447
+
448
+ workspace_path_rel = rel_path(workspace_path_abs.to_s, base_dir: base_dir)
449
+ fail!("workspace 不存在: #{workspace_path_rel}") unless File.directory?(workspace_path_abs)
450
+
451
+ validation_dir_abs = File.dirname(workspace_path_abs)
452
+ File.open(status_md, "a") do |f|
453
+ f.puts "- validation_dir: #{rel_path(validation_dir_abs, base_dir: base_dir)}"
454
+ f.puts "- workspace: #{workspace_path_rel}"
455
+ end
456
+
457
+ if deployment_target && !deployment_target.strip.empty?
458
+ changed = bump_deployment_targets_in_validation_dir(validation_dir_abs, deployment_target)
459
+ File.open(status_md, "a") { |f| f.puts "- bumped_deployment_target: #{deployment_target} (changed=#{changed})" }
460
+ end
461
+
462
+ exported_source = ""
463
+ dev_pod_dir = File.join(validation_dir_abs, "Pods", "Development Pods", scheme)
464
+ if File.directory?(dev_pod_dir)
465
+ FileUtils.rm_rf(exported_source_root)
466
+ FileUtils.mkdir_p(exported_source_root)
467
+ FileUtils.cp_r(dev_pod_dir, exported_source_root)
468
+ exported_source = File.join(exported_source_root, scheme)
469
+ end
470
+
471
+ File.open(status_md, "a") do |f|
472
+ if exported_source.empty?
473
+ f.puts "- exported_source: 未找到(跳过)"
474
+ else
475
+ f.puts "- exported_source: #{rel_path(exported_source, base_dir: base_dir)}"
476
+ end
477
+ end
478
+
479
+ derived_dir = File.join(derived_data_root, scheme)
480
+ FileUtils.mkdir_p(derived_dir)
481
+ derived_dir_rel = rel_path(derived_dir, base_dir: base_dir)
482
+
483
+ docbuild_log = File.join(logs_dir, "xcodebuild-docbuild.log")
484
+
485
+ workspace_for_docbuild = rel_path(workspace_path_abs, base_dir: podspec_dir_abs)
486
+ derived_dir_for_docbuild = rel_path(derived_dir, base_dir: podspec_dir_abs)
487
+ docbuild_cmd = [
488
+ "xcodebuild", "docbuild",
489
+ "-workspace", workspace_for_docbuild,
490
+ "-scheme", scheme,
491
+ "-destination", DESTINATION,
492
+ "-configuration", CONFIGURATION,
493
+ "-derivedDataPath", derived_dir_for_docbuild
494
+ ]
495
+ if deployment_target && !deployment_target.strip.empty?
496
+ docbuild_cmd << "IPHONEOS_DEPLOYMENT_TARGET=#{deployment_target}"
497
+ docbuild_cmd << "SWIFT_VERSION=5.0"
498
+ end
499
+ docbuild_cmd += ["CODE_SIGNING_ALLOWED=NO", "CODE_SIGNING_REQUIRED=NO", "CODE_SIGN_IDENTITY="]
500
+
501
+ docbuild_cmd_report = docbuild_cmd.dup
502
+ docbuild_cmd_report[docbuild_cmd_report.index(workspace_for_docbuild) || 0] = workspace_path_rel
503
+ docbuild_cmd_report[docbuild_cmd_report.index(derived_dir_for_docbuild) || 0] = derived_dir_rel
504
+ File.open(status_md, "a") { |f| f.puts "- docbuild: #{docbuild_cmd_report.join(" ")}" }
505
+
506
+ log "执行 docbuild: #{scheme}"
507
+ docbuild_exit = run_to_file(docbuild_cmd, cwd: podspec_dir_abs, env: { "HOME" => real_home }, log_path: docbuild_log)
508
+
509
+ unless docbuild_exit.zero?
510
+ File.open(status_md, "a") do |f|
511
+ f.puts "- docbuild: 失败(exit=#{docbuild_exit})"
512
+ f.puts
513
+ f.puts "**xcodebuild 日志(尾部)**"
514
+ f.puts "```"
515
+ tail_lines(docbuild_log, 200).each { |line| f.puts line }
516
+ f.puts "```"
517
+ end
518
+ fail!("docbuild 失败")
519
+ end
520
+
521
+ File.open(status_md, "a") { |f| f.puts "- docbuild: 成功" }
522
+
523
+ archives = []
524
+ Find.find(derived_dir) do |p|
525
+ next unless File.directory?(p)
526
+ next unless p.end_with?(".doccarchive")
527
+ archives << p
528
+ Find.prune
529
+ end
530
+
531
+ doccarchive_path =
532
+ archives
533
+ .select { |p| File.directory?(p) }
534
+ .sort_by { |p| File.mtime(p) }
535
+ .reverse
536
+ .find { |p| File.basename(p) == "#{scheme}.doccarchive" } || archives.max_by { |p| File.mtime(p) }
537
+
538
+ fail!("未找到 .doccarchive(derivedData=#{derived_dir_rel})") if doccarchive_path.to_s.strip.empty?
539
+
540
+ doccarchive_rel = rel_path(doccarchive_path, base_dir: base_dir)
541
+ File.open(status_md, "a") { |f| f.puts "- doccarchive: #{doccarchive_rel}" }
542
+
543
+ site_out = component_dir_abs
544
+ FileUtils.mkdir_p(site_out)
545
+
546
+ export_log = File.join(logs_dir, "docc-transform.log")
547
+
548
+ doccarchive_for_export = rel_path(doccarchive_path, base_dir: podspec_dir_abs)
549
+ export_staging_dir = File.join(derived_data_root, ".docc-site", scheme)
550
+ FileUtils.rm_rf(export_staging_dir)
551
+ FileUtils.mkdir_p(export_staging_dir)
552
+ site_out_for_export = rel_path(export_staging_dir, base_dir: podspec_dir_abs)
553
+ export_cmd = [
554
+ "xcrun", "docc", "process-archive", "transform-for-static-hosting",
555
+ doccarchive_for_export,
556
+ "--output-path", site_out_for_export,
557
+ "--hosting-base-path", hosting_base_path
558
+ ]
559
+
560
+ export_cmd_report = export_cmd.dup
561
+ export_cmd_report[export_cmd_report.index(doccarchive_for_export) || 0] = doccarchive_rel
562
+ export_cmd_report[export_cmd_report.index(site_out_for_export) || 0] = rel_path(site_out, base_dir: base_dir)
563
+ File.open(status_md, "a") { |f| f.puts "- export: #{export_cmd_report.join(" ")}" }
564
+
565
+ log "导出静态站点: #{component_dir_rel}"
566
+ export_exit = run_to_file(export_cmd, cwd: podspec_dir_abs, env: { "HOME" => real_home }, log_path: export_log)
567
+ unless export_exit.zero?
568
+ warn tail_lines(export_log, 200).join("\n")
569
+ fail!("docc 导出失败")
570
+ end
571
+
572
+ sync_dir_contents(export_staging_dir, site_out)
573
+
574
+ log "本地生成完成: file://#{component_dir_abs.gsub(' ', '%20')}"
575
+ is_success = true
576
+
577
+ staging_dir = File.join(local_work_root, scheme)
578
+ FileUtils.rm_rf(staging_dir)
579
+ FileUtils.mv(local_site_output, staging_dir)
580
+
581
+ zip_name = "#{scheme}_#{Time.now.strftime("%Y%m%d_%H%M%S")}_#{Process.pid}.zip"
582
+ local_zip_path = File.join(local_work_root, zip_name)
583
+
584
+ zip_log = File.join(logs_dir, "zip.log")
585
+ zip_timeout_sec = (ENV["BB_DOCC_ZIP_TIMEOUT_SEC"].to_s.strip.empty? ? 1800 : ENV["BB_DOCC_ZIP_TIMEOUT_SEC"].to_i)
586
+
587
+ log "📦 正在压缩产物 (zip)... (timeout=#{zip_timeout_sec}s)"
588
+ log "📄 zip 日志: #{rel_path(zip_log, base_dir: base_dir)}"
589
+
590
+ zip_exit = nil
591
+ FileUtils.mkdir_p(File.dirname(zip_log))
592
+ File.open(zip_log, "w") do |f|
593
+ Dir.chdir(local_work_root) do
594
+ pid = Process.spawn({ "HOME" => real_home }, "zip", "-r", "-q", zip_name, scheme, out: f, err: f)
595
+ begin
596
+ Timeout.timeout(zip_timeout_sec) do
597
+ _pid, status = Process.wait2(pid)
598
+ zip_exit = status.exitstatus
599
+ end
600
+ rescue Timeout::Error
601
+ begin
602
+ Process.kill("TERM", pid)
603
+ rescue StandardError
604
+ end
605
+ sleep 2
606
+ begin
607
+ Process.kill("KILL", pid)
608
+ rescue StandardError
609
+ end
610
+ zip_exit = 124
611
+ end
612
+ end
613
+ end
614
+
615
+ log "📦 压缩结束 (exit=#{zip_exit})"
616
+ fail!("压缩失败,未找到 zip 文件") unless zip_exit == 0 && File.exist?(local_zip_path)
617
+
618
+ zip_rel_path_in_repo = File.join(zip_push_subdir, scheme, zip_name)
619
+ git_log = File.join(logs_dir, "git-push.log")
620
+ git_timeout_sec = (ENV["BB_DOCC_GIT_TIMEOUT_SEC"].to_s.strip.empty? ? 600 : ENV["BB_DOCC_GIT_TIMEOUT_SEC"].to_i)
621
+
622
+ git_env = { "HOME" => real_home }
623
+ git_env["GIT_TERMINAL_PROMPT"] = "0" if ENV["BB_DOCC_ALLOW_PROMPT"].to_s.strip != "1"
624
+
625
+ log "🚚 正在推送 zip 到 Git: #{redact_git_url(zip_push_repo)} (branch=#{zip_push_branch})"
626
+ log "📄 git 日志: #{rel_path(git_log, base_dir: base_dir)}"
627
+
628
+ git_exit = push_zip_to_git(
629
+ repo_url: zip_push_repo,
630
+ branch: zip_push_branch,
631
+ scheme: scheme,
632
+ zip_path: local_zip_path,
633
+ zip_rel_path_in_repo: zip_rel_path_in_repo,
634
+ log_path: git_log,
635
+ env: git_env,
636
+ timeout_sec: git_timeout_sec
637
+ )
638
+ fail!("推送 zip 失败(exit=#{git_exit})") unless git_exit.zero?
639
+
640
+ ensure
641
+ at_exit_block.call
642
+ end
643
+ end
644
+
645
+ private
646
+
647
+ def log(msg)
648
+ puts "[docc] #{msg}"
649
+ end
650
+
651
+ def fail!(msg)
652
+ warn "[docc][error] #{msg}"
653
+ exit 1
654
+ end
655
+
656
+ def abs_path(path, base_dir:)
657
+ return "" if path.nil? || path.empty?
658
+ return File.expand_path(path) if path.start_with?("/")
659
+ File.expand_path(path, base_dir)
660
+ end
661
+
662
+ def rel_path(path, base_dir:)
663
+ p = path.to_s
664
+ return "" if p.empty?
665
+ return p unless p.start_with?("/")
666
+ base = base_dir.to_s
667
+ return p if base.empty?
668
+
669
+ Pathname.new(p).relative_path_from(Pathname.new(base)).to_s
670
+ rescue StandardError
671
+ p
672
+ end
673
+
674
+ def path_inside_dir?(path, dir)
675
+ p = File.expand_path(path.to_s)
676
+ d = File.expand_path(dir.to_s)
677
+ return false if p.empty? || d.empty?
678
+ return true if p == d
679
+ p.start_with?(d + File::SEPARATOR)
680
+ rescue StandardError
681
+ false
682
+ end
683
+
684
+ def cleanup_temp_dirs(keep_dir:, logs_dir:, dirs:)
685
+ keep_abs = File.expand_path(keep_dir.to_s)
686
+ logs_abs = File.expand_path(logs_dir.to_s)
687
+
688
+ cleaned = []
689
+ skipped = []
690
+
691
+ dirs.each do |dir|
692
+ p = dir.to_s.strip
693
+ next if p.empty?
694
+
695
+ abs = File.expand_path(p)
696
+ next if abs.empty?
697
+ next if abs == "/"
698
+ next if abs == keep_abs
699
+
700
+ if path_inside_dir?(abs, logs_abs)
701
+ skipped << abs
702
+ next
703
+ end
704
+
705
+ unless path_inside_dir?(abs, keep_abs)
706
+ skipped << abs
707
+ next
708
+ end
709
+
710
+ FileUtils.rm_rf(abs)
711
+ cleaned << abs
712
+ rescue StandardError
713
+ skipped << p
714
+ end
715
+
716
+ [cleaned.uniq, skipped.uniq]
717
+ end
718
+
719
+ def which(cmd)
720
+ path = ENV["PATH"].to_s
721
+ path.split(":").each do |dir|
722
+ p = File.join(dir, cmd)
723
+ return p if File.file?(p) && File.executable?(p)
724
+ end
725
+ nil
726
+ end
727
+
728
+ def require_cmd!(cmd)
729
+ return if which(cmd)
730
+ fail!("缺少命令: #{cmd}")
731
+ end
732
+
733
+ def safe_read_text(path)
734
+ File.read(path, mode: "r:bom|utf-8", invalid: :replace, undef: :replace, replace: "")
735
+ rescue StandardError
736
+ ""
737
+ end
738
+
739
+ def parse_version(v)
740
+ parts = v.to_s.strip.split(".")
741
+ major = parts[0].to_s.match?(/\A\d+\z/) ? parts[0].to_i : 0
742
+ minor = parts[1].to_s.match?(/\A\d+\z/) ? parts[1].to_i : 0
743
+ [major, minor]
744
+ end
745
+
746
+ def tail_lines(path, max_lines)
747
+ return [] unless File.file?(path)
748
+
749
+ lines = []
750
+ chunk_size = 8192
751
+
752
+ File.open(path, "rb") do |f|
753
+ f.seek(0, IO::SEEK_END)
754
+ pos = f.pos
755
+ buffer = +""
756
+
757
+ while lines.length <= max_lines && pos.positive?
758
+ read_size = [chunk_size, pos].min
759
+ pos -= read_size
760
+ f.seek(pos, IO::SEEK_SET)
761
+ buffer.prepend(f.read(read_size))
762
+ parts = buffer.split(/\r?\n/, -1)
763
+ buffer = parts.shift.to_s
764
+ lines = parts + lines
765
+ end
766
+
767
+ lines.unshift(buffer) unless buffer.empty?
768
+ end
769
+
770
+ # Fix: Convert lines to UTF-8 and scrub invalid bytes to avoid encoding errors
771
+ lines.last(max_lines).map do |line|
772
+ line.force_encoding("UTF-8").scrub("?")
773
+ end
774
+ end
775
+
776
+ def extract_name_from_podspec(spec_path)
777
+ begin
778
+ require "cocoapods"
779
+ name = Pod::Spec.from_file(spec_path).name.to_s
780
+ return name unless name.empty?
781
+ rescue StandardError
782
+ end
783
+
784
+ text = safe_read_text(spec_path)
785
+ if (m = text.match(/^[[:space:]]*s\.name[[:space:]]*=[[:space:]]*['"]([^'"]+)['"]/))
786
+ return m[1].to_s.strip
787
+ end
788
+
789
+ File.basename(spec_path, ".podspec")
790
+ end
791
+
792
+ def extract_ios_deployment_target_from_podspec(spec_path)
793
+ begin
794
+ require "cocoapods"
795
+ target = Pod::Spec.from_file(spec_path).ios.deployment_target.to_s
796
+ return target unless target.empty?
797
+ rescue StandardError
798
+ end
799
+
800
+ text = safe_read_text(spec_path)
801
+ if (m = text.match(/^[[:space:]]*s\.ios\.deployment_target[[:space:]]*=[[:space:]]*['"]([^'"]+)['"]/))
802
+ return m[1].to_s.strip
803
+ end
804
+
805
+ if (m = text.match(/^[[:space:]]*s\.platform[[:space:]]*=[[:space:]]*:ios[[:space:]]*,[[:space:]]*['"]([^'"]+)['"]/))
806
+ return m[1].to_s.strip
807
+ end
808
+
809
+ "13.0"
810
+ end
811
+
812
+ def run_to_file(cmd, cwd:, env:, log_path:)
813
+ FileUtils.mkdir_p(File.dirname(log_path))
814
+ File.open(log_path, "w") do |f|
815
+ pid = Process.spawn(env, *cmd, chdir: cwd, out: f, err: f)
816
+ _pid, status = Process.wait2(pid)
817
+ status.exitstatus
818
+ end
819
+ end
820
+
821
+ def sync_dir_contents(src_dir, dst_dir)
822
+ FileUtils.mkdir_p(dst_dir)
823
+ Dir.children(src_dir).each do |name|
824
+ src = File.join(src_dir, name)
825
+ dst = File.join(dst_dir, name)
826
+ FileUtils.rm_rf(dst)
827
+ FileUtils.cp_r(src, dst, preserve: true)
828
+ end
829
+ end
830
+
831
+ def redact_git_url(url)
832
+ s = url.to_s
833
+ return s if s.empty?
834
+
835
+ if (m = s.match(/\A(https?:\/\/)([^@\/]+)@(.+)\z/))
836
+ "#{m[1]}***@#{m[3]}"
837
+ else
838
+ s
839
+ end
840
+ end
841
+
842
+ def run_to_file_with_timeout(cmd, cwd:, env:, log_path:, timeout_sec:)
843
+ FileUtils.mkdir_p(File.dirname(log_path))
844
+ File.open(log_path, "a") do |f|
845
+ f.puts
846
+ f.puts "$ #{cmd.join(" ")}"
847
+
848
+ pid = Process.spawn(env, *cmd, chdir: cwd, out: f, err: f)
849
+ exitstatus = nil
850
+
851
+ begin
852
+ Timeout.timeout(timeout_sec) do
853
+ _pid, status = Process.wait2(pid)
854
+ exitstatus = status.exitstatus
855
+ end
856
+ rescue Timeout::Error
857
+ begin
858
+ Process.kill("TERM", pid)
859
+ rescue StandardError
860
+ end
861
+ sleep 2
862
+ begin
863
+ Process.kill("KILL", pid)
864
+ rescue StandardError
865
+ end
866
+ exitstatus = 124
867
+ end
868
+
869
+ f.puts "=> exit=#{exitstatus}"
870
+ exitstatus
871
+ end
872
+ end
873
+
874
+ def push_zip_to_git(repo_url:, branch:, scheme:, zip_path:, zip_rel_path_in_repo:, log_path:, env:, timeout_sec:)
875
+ repo_dir = File.join(File.dirname(log_path.to_s), "zip_push_repo")
876
+ FileUtils.rm_rf(repo_dir)
877
+ FileUtils.mkdir_p(File.dirname(repo_dir))
878
+
879
+ clone_cmd = ["git", "clone", "--depth", "1", "--branch", branch, repo_url, repo_dir]
880
+ clone_exit = run_to_file_with_timeout(clone_cmd, cwd: Dir.pwd, env: env, log_path: log_path, timeout_sec: timeout_sec)
881
+ return clone_exit unless clone_exit.zero?
882
+
883
+ target_path = File.join(repo_dir, zip_rel_path_in_repo)
884
+ target_dir_abs = File.dirname(target_path)
885
+ target_dir_rel = File.dirname(zip_rel_path_in_repo)
886
+ target_dir_rel = "" if target_dir_rel == "."
887
+
888
+ FileUtils.mkdir_p(target_dir_abs)
889
+ FileUtils.cp(zip_path, target_path)
890
+
891
+ if !target_dir_rel.empty?
892
+ Dir.glob(File.join(target_dir_abs, "*.zip")).each do |p|
893
+ next if File.expand_path(p) == File.expand_path(target_path)
894
+ FileUtils.rm_f(p)
895
+ end
896
+ end
897
+
898
+ pathspec = target_dir_rel.empty? ? zip_rel_path_in_repo : target_dir_rel
899
+ add_exit = run_to_file_with_timeout(["git", "add", "-A", "--", pathspec], cwd: repo_dir, env: env, log_path: log_path, timeout_sec: timeout_sec)
900
+ return add_exit unless add_exit.zero?
901
+
902
+ diff_exit = run_to_file_with_timeout(["git", "diff", "--cached", "--quiet"], cwd: repo_dir, env: env, log_path: log_path, timeout_sec: timeout_sec)
903
+ return 0 if diff_exit.zero?
904
+
905
+ author_name = ENV["BB_DOCC_GIT_AUTHOR_NAME"].to_s.strip
906
+ author_name = "docc-bot" if author_name.empty?
907
+ author_email = ENV["BB_DOCC_GIT_AUTHOR_EMAIL"].to_s.strip
908
+ author_email = "docc-bot@babybus.co" if author_email.empty?
909
+
910
+ commit_message = ENV["BB_DOCC_GIT_COMMIT_MESSAGE"].to_s.strip
911
+ commit_message = "添加#{scheme}静态站点" if commit_message.empty?
912
+
913
+ commit_cmd = ["git", "-c", "user.name=#{author_name}", "-c", "user.email=#{author_email}", "commit", "-m", commit_message]
914
+ commit_exit = run_to_file_with_timeout(commit_cmd, cwd: repo_dir, env: env, log_path: log_path, timeout_sec: timeout_sec)
915
+ return commit_exit unless commit_exit.zero?
916
+
917
+ push_cmd = ["git", "push", "origin", "HEAD:#{branch}"]
918
+ push_exit = nil
919
+
920
+ 3.times do |i|
921
+ push_exit = run_to_file_with_timeout(push_cmd, cwd: repo_dir, env: env, log_path: log_path, timeout_sec: timeout_sec)
922
+ return 0 if push_exit.zero?
923
+
924
+ pull_exit = run_to_file_with_timeout(["git", "pull", "--rebase", "origin", branch], cwd: repo_dir, env: env, log_path: log_path, timeout_sec: timeout_sec)
925
+ return push_exit unless pull_exit.zero?
926
+
927
+ sleep 1 + i
928
+ end
929
+
930
+ push_exit
931
+ end
932
+
933
+ def find_workspace_from_lint_log(lint_log, lint_root)
934
+ workspace_path = nil
935
+
936
+ # Fix: explicitly specify encoding to avoid ASCII-8BIT to UTF-8 errors
937
+ File.foreach(lint_log, encoding: "UTF-8", invalid: :replace, undef: :replace, replace: "?") do |line|
938
+ if (m = line.match(/Pods workspace available at `([^`]+)`/))
939
+ workspace_path = m[1].to_s.strip
940
+ elsif (m = line.match(%r{(/[^ ]+App\.xcworkspace)}))
941
+ workspace_path = m[1].to_s.strip
942
+ end
943
+ end
944
+
945
+ if workspace_path && File.directory?(workspace_path)
946
+ return workspace_path
947
+ end
948
+
949
+ candidates = []
950
+ root = File.expand_path(lint_root)
951
+ root_depth = root.split(File::SEPARATOR).length
952
+
953
+ Find.find(root) do |p|
954
+ if File.directory?(p) && p.end_with?(".xcworkspace")
955
+ depth = p.split(File::SEPARATOR).length - root_depth
956
+ candidates << p if depth <= 4
957
+ Find.prune
958
+ end
959
+ end
960
+
961
+ candidates
962
+ .select { |p| File.directory?(p) }
963
+ .max_by { |p| File.mtime(p) }
964
+ end
965
+
966
+ def bump_deployment_targets_in_validation_dir(validation_dir, min_target)
967
+ min_v = parse_version(min_target)
968
+ pattern = /(IPHONEOS_DEPLOYMENT_TARGET\s*=\s*)([0-9]+(?:\.[0-9]+)?)(\s*;?)/
969
+
970
+ files = []
971
+ pods_pbxproj = File.join(validation_dir, "Pods", "Pods.xcodeproj", "project.pbxproj")
972
+ files << pods_pbxproj if File.file?(pods_pbxproj)
973
+
974
+ tsf_dir = File.join(validation_dir, "Pods", "Target Support Files")
975
+ if File.directory?(tsf_dir)
976
+ Find.find(tsf_dir) do |p|
977
+ files << p if File.file?(p) && p.end_with?(".xcconfig")
978
+ end
979
+ end
980
+
981
+ changed = 0
982
+ files.each do |path|
983
+ text = safe_read_text(path)
984
+ next if text.empty?
985
+
986
+ new_text = text.gsub(pattern) do
987
+ prefix = Regexp.last_match(1)
988
+ cur = Regexp.last_match(2)
989
+ suffix = Regexp.last_match(3)
990
+ if (parse_version(cur) <=> min_v) == -1
991
+ changed += 1
992
+ "#{prefix}#{min_target}#{suffix}"
993
+ else
994
+ Regexp.last_match(0)
995
+ end
996
+ end
997
+
998
+ next if new_text == text
999
+ File.write(path, new_text)
1000
+ end
1001
+
1002
+ changed
1003
+ end
1004
+ end
1005
+ end
@@ -7,4 +7,5 @@ require "cocoapods-bb-PodAssistant/tools/file_handle"
7
7
  require "cocoapods-bb-PodAssistant/tools/class_unuse_finder"
8
8
  require "cocoapods-bb-PodAssistant/tools/count_code_line"
9
9
  require "cocoapods-bb-PodAssistant/tools/git_sets"
10
- require "cocoapods-bb-PodAssistant/tools/podfile_tiled"
10
+ require "cocoapods-bb-PodAssistant/tools/podfile_tiled"
11
+ require "cocoapods-bb-PodAssistant/tools/docc_generator"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocoapods-bb-PodAssistant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.13.9
4
+ version: 0.3.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - humin
@@ -189,6 +189,7 @@ files:
189
189
  - lib/cocoapods-bb-PodAssistant/tools.rb
190
190
  - lib/cocoapods-bb-PodAssistant/tools/class_unuse_finder.rb
191
191
  - lib/cocoapods-bb-PodAssistant/tools/count_code_line.rb
192
+ - lib/cocoapods-bb-PodAssistant/tools/docc_generator.rb
192
193
  - lib/cocoapods-bb-PodAssistant/tools/file_handle.rb
193
194
  - lib/cocoapods-bb-PodAssistant/tools/find_unuse_img.rb
194
195
  - lib/cocoapods-bb-PodAssistant/tools/get_size.rb