pindo 5.17.4 → 5.18.3

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 (55) 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 +92 -31
  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/unity/autobuild.rb +38 -18
  12. data/lib/pindo/command/utils/allcopyconfig.rb +144 -0
  13. data/lib/pindo/command/utils/copyconfig.rb +207 -0
  14. data/lib/pindo/command/utils/icon.rb +2 -2
  15. data/lib/pindo/command/utils/renewbundleid.rb +199 -0
  16. data/lib/pindo/command/utils/renewcert.rb +56 -54
  17. data/lib/pindo/command/utils.rb +3 -0
  18. data/lib/pindo/command/web/autobuild.rb +10 -8
  19. data/lib/pindo/config/build_info_manager.rb +1 -3
  20. data/lib/pindo/module/android/android_build_helper.rb +198 -33
  21. data/lib/pindo/module/android/android_config_helper.rb +305 -88
  22. data/lib/pindo/module/android/android_project_helper.rb +124 -14
  23. data/lib/pindo/module/android/android_res_helper.rb +349 -51
  24. data/lib/pindo/module/android/keystore_helper.rb +611 -295
  25. data/lib/pindo/module/android/workflow_gradle_injector.rb +702 -0
  26. data/lib/pindo/module/appselect.rb +4 -4
  27. data/lib/pindo/module/appstore/bundleid_helper.rb +204 -14
  28. data/lib/pindo/module/build/build_helper.rb +76 -10
  29. data/lib/pindo/module/build/git_repo_helper.rb +4 -4
  30. data/lib/pindo/module/cert/mode/base_cert_operator.rb +12 -6
  31. data/lib/pindo/module/pgyer/pgyerhelper.rb +124 -39
  32. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +64 -6
  33. data/lib/pindo/module/task/model/git/git_commit_task.rb +70 -54
  34. data/lib/pindo/module/task/model/git/git_tag_task.rb +13 -9
  35. data/lib/pindo/module/task/model/jps/jps_upload_task.rb +110 -3
  36. data/lib/pindo/module/task/model/unity/unity_export_task.rb +2 -1
  37. data/lib/pindo/module/task/model/unity/unity_update_task.rb +2 -1
  38. data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +2 -1
  39. data/lib/pindo/module/task/model/unity_task.rb +2 -1
  40. data/lib/pindo/module/unity/unity_helper.rb +13 -10
  41. data/lib/pindo/module/unity/unity_proc_helper.rb +27 -2
  42. data/lib/pindo/module/xcode/applovin_xcode_helper.rb +6 -2
  43. data/lib/pindo/module/xcode/res/xcode_res_constant.rb +72 -0
  44. data/lib/pindo/module/xcode/res/xcode_res_handler.rb +3 -3
  45. data/lib/pindo/module/xcode/xcode_build_config.rb +46 -17
  46. data/lib/pindo/module/xcode/xcode_build_helper.rb +186 -25
  47. data/lib/pindo/module/xcode/xcode_project_helper.rb +1 -1
  48. data/lib/pindo/module/xcode/xcode_res_helper.rb +32 -16
  49. data/lib/pindo/options/groups/build_options.rb +5 -5
  50. data/lib/pindo/options/groups/git_options.rb +7 -5
  51. data/lib/pindo/options/groups/unity_options.rb +11 -0
  52. data/lib/pindo/options/helpers/bundleid_selector.rb +25 -0
  53. data/lib/pindo/options/helpers/git_constants.rb +7 -6
  54. data/lib/pindo/version.rb +3 -3
  55. metadata +12 -7
@@ -83,8 +83,8 @@ module Pindo
83
83
 
84
84
  fixed_bundleid_array.each do |bundle_id|
85
85
  # begin
86
- if bundle_id.eql?("com.heroneverdie101.*")
87
- bundle_id = "com.heroneverdie101"
86
+ if bundle_id.include?("*")
87
+ bundle_id = bundle_id.gsub(".*", "")
88
88
  end
89
89
  fixed_cert(bundle_id:bundle_id, renew_flag:@renew_cert_flag, upload_flag:@upload_flag, fixed_bundleid_flag:@fixedid_flag)
90
90
  # rescue => err
@@ -123,58 +123,60 @@ module Pindo
123
123
  )
124
124
  end
125
125
 
126
- if @fast_flag
127
-
128
- if @dev_bundle_id_array.include?(bundle_id) || @tool_bundle_id_array.include?(bundle_id)
129
- args_temp = []
130
- args_temp << "--config=#{config_file_path}"
131
- args_temp << "--build_type=dev"
132
- if renew_flag
133
- args_temp << "--renew"
134
- end
135
- if upload_flag
136
- args_temp << "--upload"
137
- end
138
- Pindo::Command::Appstore::Cert::run(args_temp)
139
- end
140
-
141
- if @deploy_bundle_id_array.include?(bundle_id)
142
- args_temp = []
143
- args_temp << "--config=#{config_file_path}"
144
- args_temp << "--build_type=adhoc"
145
- if renew_flag
146
- args_temp << "--renew"
147
- end
148
- if upload_flag
149
- args_temp << "--upload"
150
- end
151
- Pindo::Command::Appstore::Cert::run(args_temp)
152
- end
153
-
154
- else
155
-
156
- args_temp = []
157
- args_temp << "--config=#{config_file_path}"
158
- args_temp << "--build_type=dev"
159
- if renew_flag
160
- args_temp << "--renew"
161
- end
162
- if upload_flag
163
- args_temp << "--upload"
164
- end
165
- Pindo::Command::Appstore::Cert::run(args_temp)
166
-
167
- args_temp = []
168
- args_temp << "--config=#{config_file_path}"
169
- args_temp << "--build_type=adhoc"
170
- if renew_flag
171
- args_temp << "--renew"
172
- end
173
- if upload_flag
174
- args_temp << "--upload"
175
- end
176
- Pindo::Command::Appstore::Cert::run(args_temp)
177
- end
126
+ Pindo::Command::Appstore::Bundleid::run([])
127
+
128
+ # if @fast_flag
129
+
130
+ # if @dev_bundle_id_array.include?(bundle_id) || @tool_bundle_id_array.include?(bundle_id)
131
+ # args_temp = []
132
+ # args_temp << "--config=#{config_file_path}"
133
+ # args_temp << "--build_type=dev"
134
+ # if renew_flag
135
+ # args_temp << "--renew"
136
+ # end
137
+ # if upload_flag
138
+ # args_temp << "--upload"
139
+ # end
140
+ # Pindo::Command::Appstore::Cert::run(args_temp)
141
+ # end
142
+
143
+ # if @deploy_bundle_id_array.include?(bundle_id)
144
+ # args_temp = []
145
+ # args_temp << "--config=#{config_file_path}"
146
+ # args_temp << "--build_type=adhoc"
147
+ # if renew_flag
148
+ # args_temp << "--renew"
149
+ # end
150
+ # if upload_flag
151
+ # args_temp << "--upload"
152
+ # end
153
+ # Pindo::Command::Appstore::Cert::run(args_temp)
154
+ # end
155
+
156
+ # else
157
+
158
+ # args_temp = []
159
+ # args_temp << "--config=#{config_file_path}"
160
+ # args_temp << "--build_type=dev"
161
+ # if renew_flag
162
+ # args_temp << "--renew"
163
+ # end
164
+ # if upload_flag
165
+ # args_temp << "--upload"
166
+ # end
167
+ # Pindo::Command::Appstore::Cert::run(args_temp)
168
+
169
+ # args_temp = []
170
+ # args_temp << "--config=#{config_file_path}"
171
+ # args_temp << "--build_type=adhoc"
172
+ # if renew_flag
173
+ # args_temp << "--renew"
174
+ # end
175
+ # if upload_flag
176
+ # args_temp << "--upload"
177
+ # end
178
+ # Pindo::Command::Appstore::Cert::run(args_temp)
179
+ # end
178
180
  end
179
181
 
180
182
 
@@ -7,6 +7,9 @@ require 'pindo/command/utils/device'
7
7
  # require 'pindo/command/utils/tgate'
8
8
  # require 'pindo/command/utils/boss'
9
9
  require 'pindo/command/utils/renewcert'
10
+ require 'pindo/command/utils/renewbundleid'
11
+ require 'pindo/command/utils/copyconfig'
12
+ require 'pindo/command/utils/allcopyconfig'
10
13
  require 'pindo/command/utils/repoinit'
11
14
  require 'pindo/command/utils/tag'
12
15
  require 'pindo/command/utils/updateconfig'
@@ -155,7 +155,7 @@ module Pindo
155
155
  pindo_project_dir = Dir.pwd
156
156
 
157
157
  # 加载 JPS 配置(如果存在)
158
- Pindo::BuildHelper.share_instance.load_jps_build_config(pindo_project_dir)
158
+ Pindo::BuildHelper.share_instance.load_jps_build_config(pindo_project_dir, conf: @args_conf)
159
159
 
160
160
  # 准备配置
161
161
  config = prepare_web_config(pindo_project_dir)
@@ -270,12 +270,14 @@ module Pindo
270
270
  )
271
271
  tasks << git_commit_task
272
272
 
273
- # 2. Git 标签任务(总是创建,依赖提交任务,在 Unity 导出之前创建)
274
- # 所有参数都从 GitCommitTask 获取
275
- git_tag_task = Pindo::TaskSystem::GitTagTask.new(config[:project_path])
276
- git_tag_task.dependencies << git_commit_task.id
277
- tasks << git_tag_task
278
- last_task = git_tag_task
273
+ # 2. Git 标签任务(skip_without_tag 模式下不创建)
274
+ git_tag_task = nil
275
+ unless process_type == Pindo::UncommittedFilesProcessType::SKIP_WITHOUT_TAG
276
+ git_tag_task = Pindo::TaskSystem::GitTagTask.new(config[:project_path])
277
+ git_tag_task.dependencies << git_commit_task.id
278
+ tasks << git_tag_task
279
+ last_task = git_tag_task
280
+ end
279
281
 
280
282
  # 3. Unity 更新必备库任务(可选)
281
283
  unless @args_skip_lib
@@ -417,7 +419,7 @@ module Pindo
417
419
  )
418
420
  # 依赖 Git Commit、Git Tag 和 Bind Package 任务
419
421
  workflow_message_task.dependencies << git_commit_task.id
420
- workflow_message_task.dependencies << git_tag_task.id
422
+ workflow_message_task.dependencies << git_tag_task.id if git_tag_task
421
423
  workflow_message_task.dependencies << bind_package_task.id
422
424
  tasks << workflow_message_task
423
425
  end
@@ -174,9 +174,7 @@ module Pindo
174
174
  end
175
175
  end
176
176
 
177
- private
178
-
179
- # 修改仓库设置(测试环境)
177
+ # 将仓库注册到 git_base_url.json,指定其所属 org
180
178
  def modify_repo_setting(repo_name:, owner_org:)
181
179
  pindo_setting_dir = pindo_single_config.pindo_env_configdir
182
180
  git_repo_file = pindo_single_config.git_base_url_fullname
@@ -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
 
@@ -40,10 +41,16 @@ module Pindo
40
41
  # 2. 处理 Unity 作为 lib 的原生工程(Unity 放在 Unity 目录下)
41
42
  elsif Pindo::AndroidProjectHelper.unity_as_lib_android_project?(project_dir)
42
43
  puts "处理 Unity 作为 lib 的原生工程..."
44
+
45
+ # 确保主工程能继承 Unity 导出的关键 gradle.properties 配置
46
+ # 包括 android.aapt2FromMavenOverride 与 org.gradle.java.home
47
+ Pindo::AndroidProjectHelper.sync_gradle_properties_from_unity_to_main(project_dir)
48
+
43
49
  unity_dir = File.join(project_dir, "Unity")
44
50
 
45
51
  if File.directory?(unity_dir)
46
52
  puts "找到 Unity 模块目录: Unity/"
53
+ validate_unity_libs_state!(unity_dir, stage: "导出前")
47
54
 
48
55
  # 如果 Unity 子目录是完整的 Unity 导出工程,对其进行处理
49
56
  if Pindo::AndroidProjectHelper.unity_android_project?(unity_dir)
@@ -67,6 +74,7 @@ module Pindo
67
74
  raise RuntimeError, "编译 Unity 模块 SO 库失败"
68
75
  end
69
76
  copy_so_files(unity_dir, project_dir)
77
+ validate_unity_libs_state!(project_dir, stage: "导出后")
70
78
 
71
79
  # SO 库拷贝完成后,重新配置主工程的 Java Home 和 Gradle 环境
72
80
  puts "Unity SO库编译完成,切换到主工程编译环境"
@@ -89,26 +97,44 @@ module Pindo
89
97
  # 检查并配置 local.properties(所有项目类型都需要)
90
98
  Pindo::AndroidProjectHelper.check_main_local_properties(project_dir)
91
99
 
92
- # 配置 Keystore 签名
100
+ # 配置 Keystore 签名(从 JPS 获取,alias_name 为 Application ID)
93
101
  build_type = debug ? "debug" : "release"
94
- if Pindo::KeystoreHelper.apply_keystore_config(project_dir, build_type)
95
- 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)
96
125
  else
97
- 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)
98
130
  end
99
-
100
- # 处理 Play Asset Delivery 配置(如果存在)
101
- setup_play_asset_delivery(project_dir)
102
-
103
- # 构建 APK
104
- build_apk(project_dir, debug)
105
131
  end
106
132
 
107
- def build_apk(project_path, debug)
133
+ def build_apk(project_path, debug, workflow_build_type: nil)
108
134
  raise ArgumentError, "项目路径不能为空" if project_path.nil? || project_path.empty?
109
135
 
110
136
  # 构建 AAB 文件
111
- unless build_aab(project_path, debug)
137
+ unless build_aab(project_path, debug, workflow_build_type: workflow_build_type)
112
138
  raise RuntimeError, "AAB 构建失败"
113
139
  end
114
140
 
@@ -116,7 +142,11 @@ module Pindo
116
142
  main_module = Pindo::AndroidProjectHelper.get_main_module(project_path)
117
143
  raise ArgumentError, "无法找到主模块" unless main_module
118
144
 
119
- 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
+ )
120
150
  raise ArgumentError, "无法从 build.gradle 中获取 keystore 信息" unless keystore_config
121
151
 
122
152
  build_tools = Pindo::AndroidProjectHelper.get_build_tools
@@ -132,7 +162,11 @@ module Pindo
132
162
  end
133
163
 
134
164
  # 准备输出路径
135
- 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
136
170
  main_module_name = File.basename(main_module)
137
171
  output_dir = File.join(project_path, "build/apks")
138
172
 
@@ -176,14 +210,37 @@ module Pindo
176
210
  # --- END ---
177
211
 
178
212
  # puts "解析 keystore 配置"
179
- ks = keystore_config[:store_file]
180
- # puts "读取 keystore path = #{ks}"
181
- ks_pass = keystore_config[:store_password]
182
- # puts "读取 keystore pass = #{ks_pass}"
183
- key_alias = keystore_config[:key_alias]
184
- # puts "读取 key alias = #{key_alias}"
185
- key_pass = keystore_config[:key_password]
186
- # 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}(口令不落日志)"
187
244
 
188
245
  # 构建 APK
189
246
  # 确保使用正确的 Java 版本 (Java 11+)
@@ -212,8 +269,12 @@ module Pindo
212
269
  raise RuntimeError, "AAB 文件不存在: #{paths[:bundle]}"
213
270
  end
214
271
 
272
+ if ks.nil? || ks.to_s.empty?
273
+ raise RuntimeError, "Keystore 路径为空:无法从 build.gradle 解析签名配置,且未从环境变量获取到 #{Pindo::KeystoreHelper::ENV_RELEASE_KEYSTORE_PATH}"
274
+ end
275
+
215
276
  unless File.exist?(ks)
216
- raise RuntimeError, "Keystore 文件不存在"
277
+ raise RuntimeError, "Keystore 文件不存在: #{ks}"
217
278
  end
218
279
 
219
280
  unless system(*bundletool_cmd)
@@ -238,12 +299,85 @@ module Pindo
238
299
  paths[:universal_apk]
239
300
  end
240
301
 
241
- def build_aab(project_path, debug)
302
+ def build_aab(project_path, debug, workflow_build_type: nil)
242
303
  # 使用子 Shell 切换目录执行,避免影响主进程当前目录 (Thread Safe)
243
- 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}"
244
325
  system(cmd)
245
326
  end
246
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
+
247
381
  def build_so_library(project_path)
248
382
  # 编译so库 (Thread Safe)
249
383
  cmd = "cd \"#{project_path}\" && ./gradlew unityLibrary:BuildIl2CppTask"
@@ -282,24 +416,55 @@ module Pindo
282
416
 
283
417
  # 递归合并拷贝:将源目录的内容合并到目标目录
284
418
  def copy_merge_recursive(src_dir, dst_dir)
285
- # 获取源目录中的所有文件和子目录
286
- Dir.glob(File.join(src_dir, "{*,.*}")).each do |src_item|
287
- # 跳过 . 和 ..
288
- next if File.basename(src_item) == '.' || File.basename(src_item) == '..'
289
-
290
- 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)
291
422
 
292
423
  if File.directory?(src_item)
293
424
  # 如果是目录,确保目标子目录存在,然后递归拷贝
294
425
  FileUtils.mkdir_p(dst_item)
295
426
  copy_merge_recursive(src_item, dst_item)
296
427
  else
297
- # 如果是文件,直接覆盖(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
+
298
435
  FileUtils.cp(src_item, dst_item)
299
436
  end
300
437
  end
301
438
  end
302
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
+
303
468
  # 设置 Play Asset Delivery:直接扫描导出工程中的资源目录,按照 Android 原生方式创建 .androidpack 目录
304
469
  # 仅适用于 Unity 项目,对于原生 Android 项目会静默跳过
305
470
  def setup_play_asset_delivery(project_dir)