pindo 5.17.5 → 5.18.4

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/git_handler.rb +120 -38
  3. data/lib/pindo/command/android/autobuild.rb +81 -40
  4. data/lib/pindo/command/appstore/adhocbuild.rb +1 -1
  5. data/lib/pindo/command/appstore/autobuild.rb +1 -1
  6. data/lib/pindo/command/appstore/autoresign.rb +1 -1
  7. data/lib/pindo/command/appstore/updateid.rb +229 -0
  8. data/lib/pindo/command/appstore.rb +1 -0
  9. data/lib/pindo/command/ios/autobuild.rb +70 -33
  10. data/lib/pindo/command/ios/podpush.rb +1 -1
  11. data/lib/pindo/command/jps/apptest.rb +2 -2
  12. data/lib/pindo/command/jps/bind.rb +1 -1
  13. data/lib/pindo/command/jps/media.rb +1 -1
  14. data/lib/pindo/command/jps/upload.rb +52 -22
  15. data/lib/pindo/command/unity/autobuild.rb +61 -44
  16. data/lib/pindo/command/utils/allcopyconfig.rb +144 -0
  17. data/lib/pindo/command/utils/copyconfig.rb +207 -0
  18. data/lib/pindo/command/utils/icon.rb +2 -2
  19. data/lib/pindo/command/utils/renewbundleid.rb +199 -0
  20. data/lib/pindo/command/utils/renewcert.rb +56 -54
  21. data/lib/pindo/command/utils.rb +3 -0
  22. data/lib/pindo/command/web/autobuild.rb +32 -26
  23. data/lib/pindo/config/build_info_manager.rb +1 -3
  24. data/lib/pindo/module/android/android_build_helper.rb +193 -33
  25. data/lib/pindo/module/android/android_config_helper.rb +305 -88
  26. data/lib/pindo/module/android/android_project_helper.rb +69 -14
  27. data/lib/pindo/module/android/android_res_helper.rb +349 -51
  28. data/lib/pindo/module/android/keystore_helper.rb +611 -295
  29. data/lib/pindo/module/android/workflow_gradle_injector.rb +702 -0
  30. data/lib/pindo/module/appselect.rb +4 -4
  31. data/lib/pindo/module/appstore/bundleid_helper.rb +204 -14
  32. data/lib/pindo/module/build/build_helper.rb +110 -10
  33. data/lib/pindo/module/build/git_repo_helper.rb +4 -4
  34. data/lib/pindo/module/cert/mode/base_cert_operator.rb +8 -6
  35. data/lib/pindo/module/pgyer/pgyerhelper.rb +105 -42
  36. data/lib/pindo/module/task/core/task_executor.rb +2 -0
  37. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +64 -6
  38. data/lib/pindo/module/task/model/git/git_commit_task.rb +70 -54
  39. data/lib/pindo/module/task/model/git/git_tag_task.rb +13 -9
  40. data/lib/pindo/module/task/model/jps/jps_upload_task.rb +104 -1
  41. data/lib/pindo/module/task/model/unity/unity_export_task.rb +2 -1
  42. data/lib/pindo/module/task/model/unity/unity_update_task.rb +2 -1
  43. data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +2 -1
  44. data/lib/pindo/module/task/model/unity_task.rb +2 -1
  45. data/lib/pindo/module/task/task_manager.rb +8 -0
  46. data/lib/pindo/module/unity/unity_helper.rb +13 -10
  47. data/lib/pindo/module/unity/unity_proc_helper.rb +27 -2
  48. data/lib/pindo/module/xcode/applovin_xcode_helper.rb +4 -2
  49. data/lib/pindo/module/xcode/res/xcode_res_constant.rb +72 -0
  50. data/lib/pindo/module/xcode/xcode_build_config.rb +36 -17
  51. data/lib/pindo/module/xcode/xcode_build_helper.rb +180 -23
  52. data/lib/pindo/module/xcode/xcode_project_helper.rb +1 -1
  53. data/lib/pindo/module/xcode/xcode_res_helper.rb +32 -16
  54. data/lib/pindo/options/groups/build_options.rb +16 -5
  55. data/lib/pindo/options/groups/git_options.rb +7 -5
  56. data/lib/pindo/options/groups/unity_options.rb +11 -0
  57. data/lib/pindo/options/helpers/bundleid_selector.rb +25 -0
  58. data/lib/pindo/options/helpers/git_constants.rb +7 -6
  59. data/lib/pindo/version.rb +2 -2
  60. metadata +10 -5
@@ -4,6 +4,7 @@ require_relative 'gradle_helper'
4
4
  require_relative 'gp_compliance_helper'
5
5
  require_relative 'java_env_helper'
6
6
  require_relative 'keystore_helper'
7
+ require_relative 'workflow_gradle_injector'
7
8
  require 'fileutils'
8
9
 
9
10
  module Pindo
@@ -19,7 +20,7 @@ module Pindo
19
20
  end
20
21
 
21
22
 
22
- def auto_build_apk(project_dir, debug = false, ignore_sub = false)
23
+ def auto_build_apk(project_dir, debug = false, ignore_sub = false, bundle_name: nil, workflow_name: nil)
23
24
  raise ArgumentError, "项目目录不能为空" if project_dir.nil?
24
25
  raise ArgumentError, "项目目录不存在" unless File.directory?(project_dir)
25
26
 
@@ -49,6 +50,7 @@ module Pindo
49
50
 
50
51
  if File.directory?(unity_dir)
51
52
  puts "找到 Unity 模块目录: Unity/"
53
+ validate_unity_libs_state!(unity_dir, stage: "导出前")
52
54
 
53
55
  # 如果 Unity 子目录是完整的 Unity 导出工程,对其进行处理
54
56
  if Pindo::AndroidProjectHelper.unity_android_project?(unity_dir)
@@ -72,6 +74,7 @@ module Pindo
72
74
  raise RuntimeError, "编译 Unity 模块 SO 库失败"
73
75
  end
74
76
  copy_so_files(unity_dir, project_dir)
77
+ validate_unity_libs_state!(project_dir, stage: "导出后")
75
78
 
76
79
  # SO 库拷贝完成后,重新配置主工程的 Java Home 和 Gradle 环境
77
80
  puts "Unity SO库编译完成,切换到主工程编译环境"
@@ -94,26 +97,44 @@ module Pindo
94
97
  # 检查并配置 local.properties(所有项目类型都需要)
95
98
  Pindo::AndroidProjectHelper.check_main_local_properties(project_dir)
96
99
 
97
- # 配置 Keystore 签名
100
+ # 配置 Keystore 签名(从 JPS 获取,alias_name 为 Application ID)
98
101
  build_type = debug ? "debug" : "release"
99
- if Pindo::KeystoreHelper.apply_keystore_config(project_dir, build_type)
100
- puts "✓ Keystore 签名配置成功"
102
+ raise "bundle_name 不能为空,Keystore 签名需要 Application ID" if bundle_name.nil? || bundle_name.empty?
103
+
104
+ unless Pindo::KeystoreHelper.apply_keystore_config(project_dir, build_type, bundle_id: bundle_name)
105
+ raise RuntimeError, "Keystore 签名配置失败"
106
+ end
107
+ puts "✓ Android 签名配置成功"
108
+
109
+ if workflow_name && !workflow_name.to_s.empty?
110
+ injector = Pindo::Android::WorkflowGradleInjector.new
111
+ ctx = injector.prepare!(
112
+ project_dir: project_dir,
113
+ workflow_name: workflow_name,
114
+ enable_pad: true
115
+ )
116
+ apk_path = nil
117
+ begin
118
+ apk_path = build_apk(project_dir, debug, workflow_build_type: ctx[:workflow_build_type])
119
+ ensure
120
+ injector.cleanup!(ctx)
121
+ end
122
+ # JKS 仅在 universal.apk 已成功产出后删除;失败则保留,便于排查(Gradle 注入已在 ensure 中恢复)
123
+ Pindo::KeystoreHelper.cleanup_managed_signing_paths! if apk_path && File.exist?(apk_path.to_s)
124
+ Pindo::Android::WorkflowGradleInjector.remove_project_pindo_tmp_dir!(project_dir)
101
125
  else
102
- puts "⚠ Keystore 签名配置跳过(使用工程原有配置)"
126
+ # 兼容旧行为:仍会永久写入 PAD 相关变更(如需临时恢复,请传 workflow_name 走注入管线)
127
+ setup_play_asset_delivery(project_dir)
128
+ apk_path = build_apk(project_dir, debug)
129
+ Pindo::KeystoreHelper.cleanup_managed_signing_paths! if apk_path && File.exist?(apk_path.to_s)
103
130
  end
104
-
105
- # 处理 Play Asset Delivery 配置(如果存在)
106
- setup_play_asset_delivery(project_dir)
107
-
108
- # 构建 APK
109
- build_apk(project_dir, debug)
110
131
  end
111
132
 
112
- def build_apk(project_path, debug)
133
+ def build_apk(project_path, debug, workflow_build_type: nil)
113
134
  raise ArgumentError, "项目路径不能为空" if project_path.nil? || project_path.empty?
114
135
 
115
136
  # 构建 AAB 文件
116
- unless build_aab(project_path, debug)
137
+ unless build_aab(project_path, debug, workflow_build_type: workflow_build_type)
117
138
  raise RuntimeError, "AAB 构建失败"
118
139
  end
119
140
 
@@ -121,7 +142,11 @@ module Pindo
121
142
  main_module = Pindo::AndroidProjectHelper.get_main_module(project_path)
122
143
  raise ArgumentError, "无法找到主模块" unless main_module
123
144
 
124
- keystore_config = Pindo::KeystoreHelper.get_keystore_config_from_project(project_path, debug)
145
+ keystore_config = Pindo::KeystoreHelper.get_keystore_config_from_project(
146
+ project_path,
147
+ debug,
148
+ workflow_build_type: workflow_build_type
149
+ )
125
150
  raise ArgumentError, "无法从 build.gradle 中获取 keystore 信息" unless keystore_config
126
151
 
127
152
  build_tools = Pindo::AndroidProjectHelper.get_build_tools
@@ -137,7 +162,11 @@ module Pindo
137
162
  end
138
163
 
139
164
  # 准备输出路径
140
- build_type = debug ? 'debug' : 'release'
165
+ build_type = if workflow_build_type && !workflow_build_type.to_s.empty?
166
+ workflow_build_type.to_s
167
+ else
168
+ debug ? 'debug' : 'release'
169
+ end
141
170
  main_module_name = File.basename(main_module)
142
171
  output_dir = File.join(project_path, "build/apks")
143
172
 
@@ -181,14 +210,37 @@ module Pindo
181
210
  # --- END ---
182
211
 
183
212
  # puts "解析 keystore 配置"
184
- ks = keystore_config[:store_file]
185
- # puts "读取 keystore path = #{ks}"
186
- ks_pass = keystore_config[:store_password]
187
- # puts "读取 keystore pass = #{ks_pass}"
188
- key_alias = keystore_config[:key_alias]
189
- # puts "读取 key alias = #{key_alias}"
190
- key_pass = keystore_config[:key_password]
191
- # puts "读取 key pass = #{key_pass}"
213
+ # Gradle 对 storeFile 的生效顺序一致:常见写法为
214
+ # storeFile file(System.getenv("RELEASE_KEYSTORE_PATH") ?: "signing/gxdebug.jks")
215
+ # apply_keystore_config 已在当前进程设置 RELEASE_*,Gradle 打 AAB 时走 ENV 指向的真实 jks;
216
+ # get_keystore_config_from_project 若只解析到 ?: 右侧占位路径,会与 AAB 实际签名不一致且文件可能不存在。
217
+ # 故:当 RELEASE_KEYSTORE_PATH 展开后文件存在时,bundletool 必须使用同一套 RELEASE_*(非「失败回退」)。
218
+ kh = Pindo::KeystoreHelper
219
+ env_ks = ENV[kh::ENV_RELEASE_KEYSTORE_PATH]
220
+ env_ks_abs = env_ks && !env_ks.to_s.empty? ? File.expand_path(env_ks) : nil
221
+ gradle_ks = keystore_config[:store_file]
222
+
223
+ use_release_env = env_ks_abs && File.exist?(env_ks_abs)
224
+ if use_release_env
225
+ ks = env_ks_abs
226
+ ks_pass = ENV[kh::ENV_RELEASE_KEYSTORE_PASSWORD] || keystore_config[:store_password]
227
+ key_alias = ENV[kh::ENV_RELEASE_KEY_ALIAS] || keystore_config[:key_alias]
228
+ key_pass = ENV[kh::ENV_RELEASE_KEY_PASSWORD] || keystore_config[:key_password]
229
+ else
230
+ ks = gradle_ks || env_ks
231
+ ks = File.expand_path(ks.to_s) if ks && !ks.to_s.empty?
232
+ ks_pass = keystore_config[:store_password] || ENV[kh::ENV_RELEASE_KEYSTORE_PASSWORD]
233
+ key_alias = keystore_config[:key_alias] || ENV[kh::ENV_RELEASE_KEY_ALIAS]
234
+ key_pass = keystore_config[:key_password] || ENV[kh::ENV_RELEASE_KEY_PASSWORD]
235
+ end
236
+
237
+ ks_display = ks && !ks.to_s.empty? ? File.expand_path(ks.to_s) : ks.to_s
238
+ # puts "[bundletool] universal.apk 实际使用的签名 keystore: #{ks_display}"
239
+ # if workflow_build_type && !workflow_build_type.to_s.empty?
240
+ # puts "[bundletool] workflow buildType: #{workflow_build_type}"
241
+ # end
242
+ # puts "[bundletool] 签名来源: #{use_release_env ? "RELEASE_KEYSTORE_PATH(文件存在,与 Gradle 打 AAB 一致)" : "build.gradle 解析 / ENV 兜底(非 RELEASE 路径或文件不存在时)"}"
243
+ # puts "[bundletool] keyAlias: #{key_alias.inspect}(口令不落日志)"
192
244
 
193
245
  # 构建 APK
194
246
  # 确保使用正确的 Java 版本 (Java 11+)
@@ -217,8 +269,12 @@ module Pindo
217
269
  raise RuntimeError, "AAB 文件不存在: #{paths[:bundle]}"
218
270
  end
219
271
 
272
+ if ks.nil? || ks.to_s.empty?
273
+ raise RuntimeError, "Keystore 路径为空:无法从 build.gradle 解析签名配置,且未从环境变量获取到 #{Pindo::KeystoreHelper::ENV_RELEASE_KEYSTORE_PATH}"
274
+ end
275
+
220
276
  unless File.exist?(ks)
221
- raise RuntimeError, "Keystore 文件不存在"
277
+ raise RuntimeError, "Keystore 文件不存在: #{ks}"
222
278
  end
223
279
 
224
280
  unless system(*bundletool_cmd)
@@ -243,12 +299,85 @@ module Pindo
243
299
  paths[:universal_apk]
244
300
  end
245
301
 
246
- def build_aab(project_path, debug)
302
+ def build_aab(project_path, debug, workflow_build_type: nil)
247
303
  # 使用子 Shell 切换目录执行,避免影响主进程当前目录 (Thread Safe)
248
- cmd = "cd \"#{project_path}\" && ./gradlew bundle#{debug ? 'Debug' : 'Release'}"
304
+ # --no-daemon:签名依赖当前进程注入的 RELEASE_* 环境变量;已存在的 Gradle Daemon
305
+ # 继承的是其首次启动时的环境,会看不到 pindo 刚设置的变量,从而回退到 build.gradle 里 ?: 的本地 jks。
306
+ main_module = Pindo::AndroidProjectHelper.get_main_module(project_path)
307
+ module_name = main_module ? File.basename(main_module) : nil
308
+ prefix = module_name && !module_name.empty? ? ":#{module_name}:" : ""
309
+
310
+ task_name = if workflow_build_type && !workflow_build_type.to_s.empty?
311
+ "bundle#{workflow_build_type.to_s[0].upcase}#{workflow_build_type.to_s[1..]}"
312
+ else
313
+ debug ? "bundleDebug" : "bundleRelease"
314
+ end
315
+
316
+ bundle_task = "#{prefix}#{task_name}"
317
+ skip_clean = ENV['PINDO_SKIP_GRADLE_CLEAN'] == '1'
318
+ # 任意 bundle 变体(含 debug)均先 clean,避免 aabresguard 等任务因残留输出报「文件已存在」;本地加速可设 PINDO_SKIP_GRADLE_CLEAN=1
319
+ puts ' AAB 构建前先执行 Gradle clean(环境变量 PINDO_SKIP_GRADLE_CLEAN=1 可跳过)' unless skip_clean
320
+ puts ' 已跳过 Gradle clean(PINDO_SKIP_GRADLE_CLEAN=1)' if skip_clean
321
+
322
+ gradle_clean_resilient!(project_path) unless skip_clean
323
+
324
+ cmd = "cd \"#{project_path}\" && ./gradlew --no-daemon #{bundle_task}"
249
325
  system(cmd)
250
326
  end
251
327
 
328
+ # clean 失败(占用、.DS_Store 竞态等)时:停 Daemon → 再删 .DS_Store → 物理删除各模块 build/,与 Gradle clean 产物一致,不中断后续 bundle
329
+ private def gradle_clean_resilient!(project_path)
330
+ prune_ds_store_before_gradle_clean!(project_path)
331
+ cmd = %(cd "#{project_path}" && ./gradlew --no-daemon clean)
332
+ return if system(cmd)
333
+
334
+ puts ' ⚠ Gradle clean 失败,正在自动恢复:--stop、清理 .DS_Store、删除各模块 build/ ...'
335
+ system(%(cd "#{project_path}" && ./gradlew --stop), out: File::NULL, err: File::NULL)
336
+ sleep 0.3
337
+ prune_ds_store_before_gradle_clean!(project_path)
338
+ remove_android_module_build_dirs!(project_path)
339
+ end
340
+
341
+ # 删除 Gradle 模块产物目录(与 clean 目标一致:根 build、一级子模块/build、常见二级子模块/build)
342
+ private def remove_android_module_build_dirs!(project_path)
343
+ return unless project_path && File.directory?(project_path)
344
+
345
+ paths = []
346
+ root_build = File.join(project_path, 'build')
347
+ paths << root_build if File.directory?(root_build)
348
+
349
+ Dir.each_child(project_path) do |name|
350
+ next if name.start_with?('.')
351
+
352
+ mod_build = File.join(project_path, name, 'build')
353
+ paths << mod_build if File.directory?(mod_build)
354
+
355
+ sub = File.join(project_path, name)
356
+ next unless File.directory?(sub)
357
+
358
+ Dir.each_child(sub) do |subname|
359
+ next if subname.start_with?('.')
360
+
361
+ nested = File.join(sub, subname, 'build')
362
+ paths << nested if File.directory?(nested)
363
+ end
364
+ end
365
+
366
+ paths.uniq.each do |p|
367
+ FileUtils.rm_rf(p)
368
+ end
369
+ rescue StandardError => e
370
+ puts " ⚠ 物理删除 build 目录时出现异常(将继续尝试 bundle): #{e.message}"
371
+ end
372
+
373
+ # macOS 在 clean 遍历删除 build 目录时 Finder 可能写入 .DS_Store,Gradle 会报 Unable to delete / New files were found
374
+ private def prune_ds_store_before_gradle_clean!(project_path)
375
+ return unless project_path && File.directory?(project_path)
376
+
377
+ system('find', project_path, '-name', '.DS_Store', '-type', 'f', '-delete',
378
+ out: File::NULL, err: File::NULL)
379
+ end
380
+
252
381
  def build_so_library(project_path)
253
382
  # 编译so库 (Thread Safe)
254
383
  cmd = "cd \"#{project_path}\" && ./gradlew unityLibrary:BuildIl2CppTask"
@@ -287,24 +416,55 @@ module Pindo
287
416
 
288
417
  # 递归合并拷贝:将源目录的内容合并到目标目录
289
418
  def copy_merge_recursive(src_dir, dst_dir)
290
- # 获取源目录中的所有文件和子目录
291
- Dir.glob(File.join(src_dir, "{*,.*}")).each do |src_item|
292
- # 跳过 . 和 ..
293
- next if File.basename(src_item) == '.' || File.basename(src_item) == '..'
294
-
295
- dst_item = File.join(dst_dir, File.basename(src_item))
419
+ Dir.each_child(src_dir) do |name|
420
+ src_item = File.join(src_dir, name)
421
+ dst_item = File.join(dst_dir, name)
296
422
 
297
423
  if File.directory?(src_item)
298
424
  # 如果是目录,确保目标子目录存在,然后递归拷贝
299
425
  FileUtils.mkdir_p(dst_item)
300
426
  copy_merge_recursive(src_item, dst_item)
301
427
  else
302
- # 如果是文件,直接覆盖(Unity 编译的文件应该覆盖旧的)
428
+ # Unity/EDM 常在 libs 下放置指向解析缓存的符号链接;BuildIl2CppTask 后可能出现断裂链接,
429
+ # FileUtils.cp 跟随链接会导致 No such file or directory。
430
+ unless File.exist?(src_item)
431
+ puts " ⚠ 跳过缺失或断裂的引用: #{name}"
432
+ next
433
+ end
434
+
303
435
  FileUtils.cp(src_item, dst_item)
304
436
  end
305
437
  end
306
438
  end
307
439
 
440
+ def validate_unity_libs_state!(android_root, stage:)
441
+ # 某些项目会将 unityLibrary 作为缓存/中间产物纳入仓库,但 libs 下的 .aar -> .srcaar
442
+ # 符号链接目标是由 EDM4U/导出过程生成,初次拉取代码时可能为断链。
443
+ # 默认仍严格失败;如确有需要,可通过环境变量临时跳过此校验继续构建。
444
+ return if ENV["PINDO_SKIP_UNITY_LIBS_VALIDATE"] == "1"
445
+
446
+ libs_dir = File.join(android_root, "unityLibrary", "libs")
447
+ return unless File.directory?(libs_dir)
448
+
449
+ broken_links = []
450
+ Dir.each_child(libs_dir) do |name|
451
+ file_path = File.join(libs_dir, name)
452
+ next unless File.symlink?(file_path)
453
+ next if File.exist?(file_path)
454
+
455
+ broken_links << "#{name} -> #{File.readlink(file_path)}"
456
+ end
457
+
458
+ return if broken_links.empty?
459
+
460
+ raise RuntimeError, <<~MSG
461
+ Unity 依赖校验失败(#{stage}):发现失效的符号链接,请先在 Unity 中执行 EDM4U Resolve 后重新导出。
462
+ 扫描目录: #{libs_dir}
463
+ 失效条目:
464
+ - #{broken_links.join("\n- ")}
465
+ MSG
466
+ end
467
+
308
468
  # 设置 Play Asset Delivery:直接扫描导出工程中的资源目录,按照 Android 原生方式创建 .androidpack 目录
309
469
  # 仅适用于 Unity 项目,对于原生 Android 项目会静默跳过
310
470
  def setup_play_asset_delivery(project_dir)