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
@@ -1,41 +1,38 @@
1
- require 'fileutils'
2
- require 'json'
3
- require_relative '../../config/pindoconfig'
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "json"
6
+ require "tmpdir"
7
+ require "openssl"
8
+ require "base64"
9
+ require "jpsclient"
10
+ require_relative "../../config/pindoconfig"
11
+ require_relative "../utils/file_downloader"
4
12
 
5
13
  module Pindo
6
14
  module KeystoreHelper
7
- class << self
8
-
9
- # PindoConfig 统一配置中读取 Android 签名配置
10
- # @return [Hash, nil] 签名配置哈希,包含 debug 和 release 配置
11
- def get_android_sign_config
12
- begin
13
- # 使用 PindoConfig 单例获取配置
14
- pindo_config = Pindoconfig.instance
15
+ # 与工程约定一致:Gradle 内用 RELEASE_* 环境变量 + 本地 ?: 回退;pindo 拉取 JPS 后写入当前进程 ENV(不落盘明文)
16
+ ENV_RELEASE_KEYSTORE_PATH = "RELEASE_KEYSTORE_PATH"
17
+ ENV_RELEASE_KEYSTORE_PASSWORD = "RELEASE_KEYSTORE_PASSWORD"
18
+ ENV_RELEASE_KEY_ALIAS = "RELEASE_KEY_ALIAS"
19
+ ENV_RELEASE_KEY_PASSWORD = "RELEASE_KEY_PASSWORD"
15
20
 
16
- # pindo_user_config_json 中获取 android_sign_config
17
- android_sign_config = pindo_config.pindo_user_config_json["android_sign_config"]
21
+ FIELD_CRYPTO_ALGORITHM = "aes-128-gcm"
22
+ FIELD_CRYPTO_KEY = "WNldU35bhG!8TQtg"
23
+ FIELD_CRYPTO_IV_LENGTH = 12
24
+ FIELD_CRYPTO_TAG_LENGTH = 16
18
25
 
19
- if android_sign_config.nil? || android_sign_config.empty?
20
- Funlog.warning("配置中未找到 android_sign_config")
21
- return nil
22
- end
23
-
24
- # 验证配置结构
25
- unless validate_sign_config(android_sign_config)
26
- Funlog.error("android_sign_config 配置格式不正确")
27
- return nil
28
- end
29
-
30
- # 解析文件路径
31
- android_sign_config = resolve_keystore_paths(android_sign_config)
32
-
33
- return android_sign_config
34
-
35
- rescue => e
36
- Funlog.error("读取签名配置失败: #{e.message}")
37
- return nil
38
- end
26
+ class << self
27
+ # 从 JPS 获取 Android 签名配置(必须成功,否则抛出异常)
28
+ # @param bundle_id [String] Android 包名(Application ID)
29
+ # @return [Hash] 签名配置哈希,包含 debug 和 release 配置(共用同一个 JKS)
30
+ def get_android_sign_config(bundle_id:)
31
+ raise ArgumentError, "bundle_id 不能为空" if bundle_id.blank?
32
+
33
+ cfg = fetch_jks_from_jps(bundle_id: bundle_id)
34
+ # debug 和 release 使用同一个 JKS
35
+ { "debug" => cfg, "release" => cfg }
39
36
  end
40
37
 
41
38
  # 验证签名配置格式
@@ -52,7 +49,7 @@ module Pindo
52
49
  next if config[build_type].nil?
53
50
 
54
51
  cfg = config[build_type]
55
- required_fields = ["storeFile", "storePassword", "keyAlias", "keyPassword"]
52
+ required_fields = %w[storeFile storePassword keyAlias keyPassword]
56
53
 
57
54
  required_fields.each do |field|
58
55
  if cfg[field].nil? || cfg[field].to_s.empty?
@@ -82,14 +79,10 @@ module Pindo
82
79
  store_file = cfg["storeFile"]
83
80
 
84
81
  # 如果是相对路径,则基于 pindo_common_config 目录解析
85
- unless store_file.start_with?("/")
86
- cfg["storeFile"] = File.join(pindo_common_config_dir, store_file)
87
- end
82
+ cfg["storeFile"] = File.join(pindo_common_config_dir, store_file) unless store_file.start_with?("/")
88
83
 
89
84
  # 验证文件是否存在
90
- unless File.exist?(cfg["storeFile"])
91
- Funlog.warning("Keystore 文件不存在: #{cfg["storeFile"]}")
92
- end
85
+ Funlog.warning("Keystore 文件不存在: #{cfg["storeFile"]}") unless File.exist?(cfg["storeFile"])
93
86
 
94
87
  resolved_config[build_type] = cfg
95
88
  end
@@ -99,41 +92,54 @@ module Pindo
99
92
 
100
93
  # 从工程的 build.gradle 中读取已有的 keystore 配置
101
94
  # @param project_path [String] 项目路径
102
- # @param debug [Boolean] 是否为 debug 构建
103
- # @return [Hash, nil] keystore 配置哈希
104
- def get_keystore_config_from_project(project_path, debug = false)
95
+ # @param debug [Boolean] 是否为 debug 构建(仅当未传 workflow_build_type 时用于选择 debug/release)
96
+ # @param workflow_build_type [String, nil] Workflow 对应 Gradle buildType 名(如 jps);若提供则**仅**从 buildTypes.<name> 解析,不回退 debug/release
97
+ # @return [Hash, nil] keystore 配置哈希(workflow 模式下解析失败会 raise,不返回 nil)
98
+ def get_keystore_config_from_project(project_path, debug = false, workflow_build_type: nil)
105
99
  main_module = Pindo::AndroidProjectHelper.get_main_module(project_path)
106
- return nil unless main_module
100
+ return unless main_module
107
101
 
108
102
  gradle_kts_path = File.join(main_module, "build.gradle.kts")
109
103
  gradle_path = File.join(main_module, "build.gradle")
110
104
 
111
105
  if File.exist?(gradle_kts_path)
112
106
  puts "KTS 项目,读取 #{File.basename(gradle_kts_path)} 文件"
113
- get_keystore_config_kts(gradle_kts_path, project_path, debug)
107
+ get_keystore_config_kts(gradle_kts_path, project_path, debug, workflow_build_type: workflow_build_type)
114
108
  elsif File.exist?(gradle_path)
115
109
  puts "Groovy 项目,读取 #{File.basename(gradle_path)} 文件"
116
- get_keystore_config_groovy(gradle_path, project_path, debug)
110
+ get_keystore_config_groovy(gradle_path, project_path, debug, workflow_build_type: workflow_build_type)
117
111
  else
118
112
  puts "未找到 build.gradle 或 build.gradle.kts 文件"
119
113
  nil
120
114
  end
121
115
  end
122
116
 
117
+ # 打包结束后由 AndroidBuildHelper.ensure 调用,删除本次落到工程 signing/ 下的 JKS
118
+ def cleanup_managed_signing_paths!
119
+ (@pindo_managed_signing_paths || []).each do |p|
120
+ FileUtils.rm_f(p) if p && File.exist?(p)
121
+ end
122
+ @pindo_managed_signing_paths = []
123
+ end
124
+
123
125
  # 将签名配置应用到 Android 工程
124
- # 新策略:
125
- # 1. buildTypes 中找到 debug/release 引用的 signingConfig 名称
126
- # 2. 检查该 signingConfig 配置是否存在且完整
127
- # 3. 如果不完整,用 Pindo 配置更新该 signingConfig
128
- # 4. 不修改 buildTypes 中的引用名称
126
+ #
127
+ # 默认(与工程手写的 RELEASE_* 约定一致):只拉取 JPS、拷贝 jks 到项目 signing/,并设置当前进程
128
+ # RELEASE_KEYSTORE_PATH / RELEASE_KEYSTORE_PASSWORD / RELEASE_KEY_ALIAS / RELEASE_KEY_PASSWORD,不修改 build.gradle。
129
+ # RELEASE_KEYSTORE_PATH 使用 jks 的绝对路径,便于 app 子模块内 `file(System.getenv(...))` 引用。
130
+ #
131
+ # 若需恢复自动改写 signingConfigs,设置环境变量:PINDO_INJECT_ANDROID_SIGNING_GRADLE=1
132
+ #
129
133
  # @param project_dir [String] 项目目录
130
134
  # @param build_type [String] 构建类型 "debug" 或 "release"
131
135
  # @return [Boolean] 是否成功
132
- def apply_keystore_config(project_dir, build_type = "debug")
136
+ def apply_keystore_config(project_dir, build_type = "debug", bundle_id:)
133
137
  raise ArgumentError, "项目目录不能为空" if project_dir.nil?
134
138
  raise ArgumentError, "build_type 必须是 debug 或 release" unless ["debug", "release"].include?(build_type)
139
+ raise ArgumentError, "bundle_id 不能为空" if bundle_id.blank?
140
+
141
+ reset_managed_signing_paths!
135
142
 
136
- # 查找主模块的 build.gradle
137
143
  main_module = Pindo::AndroidProjectHelper.get_main_module(project_dir)
138
144
  unless main_module
139
145
  Funlog.error("无法找到主模块")
@@ -146,25 +152,289 @@ module Pindo
146
152
  return false
147
153
  end
148
154
 
149
- puts "检查并配置 keystore 签名..."
155
+ sign_config = get_android_sign_config(bundle_id: bundle_id)
156
+ copy_keystore_to_project(project_dir, build_type, sign_config[build_type])
157
+
158
+ # JPS 下载的临时 jks(系统临时目录下 pindo_jks),拷贝进工程后即可删除
159
+ tmp_jks = sign_config[build_type]["storeFile"].to_s
160
+ if File.exist?(tmp_jks)
161
+ tmp_exp = File.expand_path(tmp_jks)
162
+ tmp_root_exp = File.expand_path(File.join(Dir.tmpdir, "pindo_jks"))
163
+ under_pindo_tmp = tmp_exp == tmp_root_exp || tmp_exp.start_with?(tmp_root_exp + File::SEPARATOR)
164
+ FileUtils.rm_f(tmp_jks) if under_pindo_tmp
165
+ end
150
166
 
151
- # 获取签名配置并拷贝 keystore 文件到项目
152
- sign_config = get_android_sign_config
153
- if sign_config && sign_config[build_type]
154
- # 拷贝文件并更新配置路径
155
- copy_keystore_to_project(project_dir, build_type, sign_config[build_type])
167
+ cfg = sign_config[build_type]
168
+ rel_plain = cfg["relative_store_file"].to_s.sub(%r{\A\$rootDir/?}, "").delete_prefix("/")
169
+ if rel_plain.empty?
170
+ raise "JPS keystore 未拷贝到工程 signing/(无法解析路径),Gradle 将回退到 build.gradle 中的本地 jks;请检查 JPS 证书是否下载成功"
156
171
  end
157
172
 
158
- # 根据文件类型选择处理方法,传递修改后的配置
159
- if gradle_file.end_with?(".kts")
160
- ensure_keystore_config_kts(gradle_file, project_dir, build_type, sign_config)
173
+ abs_keystore = File.expand_path(File.join(project_dir, rel_plain))
174
+
175
+ inject_gradle = ENV["PINDO_INJECT_ANDROID_SIGNING_GRADLE"] == "1"
176
+ if inject_gradle
177
+ if gradle_file.end_with?(".kts")
178
+ ensure_keystore_config_kts(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
179
+ else
180
+ ensure_keystore_config_groovy(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
181
+ end
182
+ export_jps_release_signing_env!(cfg, keystore_path_for_env: rel_plain)
161
183
  else
162
- ensure_keystore_config_groovy(gradle_file, project_dir, build_type, sign_config)
184
+ export_jps_release_signing_env!(cfg, keystore_path_for_env: abs_keystore)
163
185
  end
186
+ true
164
187
  end
165
188
 
166
189
  private
167
190
 
191
+ def looks_like_field_crypto_b64?(value)
192
+ return false if value.nil?
193
+
194
+ str = value.to_s.strip
195
+ return false if str.empty?
196
+ return false unless str.match?(%r{\A[A-Za-z0-9+/=]+\z})
197
+
198
+ # 最小长度:Base64(12字节IV + 1字节密文 + 16字节tag) => 29 bytes
199
+ # 用字符串长度粗略过滤,避免对普通明文误判
200
+ str.length >= 40
201
+ end
202
+
203
+ # - 输入:Base64( IV(12) + ciphertext_and_tag )
204
+ # - 算法:AES/GCM/NoPadding (Ruby: aes-128-gcm)
205
+ # - KEY:UTF-8 16 bytes
206
+ def decrypt_field_crypto_b64!(ciphertext_b64)
207
+ raise ArgumentError, "ciphertext 不能为空" if ciphertext_b64.nil?
208
+
209
+ combined = Base64.strict_decode64(ciphertext_b64.to_s)
210
+ raise ArgumentError, "密文长度不合法(不足以包含 IV)" if combined.bytesize <= FIELD_CRYPTO_IV_LENGTH
211
+
212
+ iv = combined.byteslice(0, FIELD_CRYPTO_IV_LENGTH)
213
+ ciphertext_and_tag = combined.byteslice(FIELD_CRYPTO_IV_LENGTH, combined.bytesize - FIELD_CRYPTO_IV_LENGTH)
214
+ raise ArgumentError, "密文长度不合法(不足以包含 GCM tag)" if ciphertext_and_tag.bytesize < FIELD_CRYPTO_TAG_LENGTH
215
+
216
+ ciphertext = ciphertext_and_tag.byteslice(0, ciphertext_and_tag.bytesize - FIELD_CRYPTO_TAG_LENGTH)
217
+ tag = ciphertext_and_tag.byteslice(ciphertext_and_tag.bytesize - FIELD_CRYPTO_TAG_LENGTH,
218
+ FIELD_CRYPTO_TAG_LENGTH)
219
+
220
+ cipher = OpenSSL::Cipher.new(FIELD_CRYPTO_ALGORITHM)
221
+ cipher.decrypt
222
+ cipher.key = FIELD_CRYPTO_KEY.encode("utf-8")
223
+ cipher.iv = iv
224
+ cipher.auth_tag = tag
225
+ cipher.auth_data = ""
226
+
227
+ (cipher.update(ciphertext) + cipher.final).force_encoding("UTF-8")
228
+ end
229
+
230
+ def decrypt_field_crypto_if_needed(value)
231
+ return value if value.nil?
232
+
233
+ s = value.to_s
234
+ return s if s.empty?
235
+ return s unless looks_like_field_crypto_b64?(s)
236
+
237
+ decrypt_field_crypto_b64!(s)
238
+ rescue OpenSSL::Cipher::CipherError, ArgumentError
239
+ # 兼容历史明文/非该算法密文:解密失败则回退原值
240
+ s
241
+ end
242
+
243
+ def reset_managed_signing_paths!
244
+ @pindo_managed_signing_paths = []
245
+ end
246
+
247
+ def register_managed_signing_path!(path)
248
+ return if path.nil? || path.to_s.empty?
249
+
250
+ @pindo_managed_signing_paths ||= []
251
+ @pindo_managed_signing_paths << path
252
+ end
253
+
254
+ # 将 JPS 返回的路径/口令/别名写入当前进程环境变量,供 Gradle / bundletool 子进程继承(与 build.gradle 中 RELEASE_* 名称一致)
255
+ def export_jps_release_signing_env!(cfg, keystore_path_for_env:)
256
+ return unless cfg.is_a?(Hash)
257
+
258
+ if keystore_path_for_env && !keystore_path_for_env.to_s.empty?
259
+ ENV[ENV_RELEASE_KEYSTORE_PATH] =
260
+ keystore_path_for_env
261
+ end
262
+
263
+ sp = cfg["storePassword"]
264
+ kp = cfg["keyPassword"]
265
+ ka = cfg["keyAlias"]
266
+ # JPS 口令可能为 field_crypto.py 的 AES-GCM Base64 密文;这里解密后仅写入当前进程 ENV(不落盘明文)
267
+ sp_plain = decrypt_field_crypto_if_needed(sp) if sp && !sp.to_s.empty?
268
+ kp_plain = decrypt_field_crypto_if_needed(kp) if kp && !kp.to_s.empty?
269
+
270
+ ENV[ENV_RELEASE_KEYSTORE_PASSWORD] = sp_plain.to_s if sp_plain && !sp_plain.to_s.empty?
271
+ ENV[ENV_RELEASE_KEY_PASSWORD] = kp_plain.to_s if kp_plain && !kp_plain.to_s.empty?
272
+ ENV[ENV_RELEASE_KEY_ALIAS] = ka.to_s if ka && !ka.to_s.empty?
273
+ end
274
+
275
+ # Groovy:解析 storePassword/keyPassword(支持 System.getenv、可选 ?: 回退、或历史明文)
276
+ def extract_signing_password_groovy(config_block, field)
277
+ if (m = config_block.match(/#{Regexp.escape(field)}\s+System\.getenv\(\s*["']([^"']+)["']\s*\)(?:\s*\?:\s*["']([^"']+)["'])?/m))
278
+ v = ENV.fetch(m[1], nil)
279
+ return v if v.present?
280
+ return m[2] if m[2] && !m[2].to_s.empty?
281
+
282
+ return
283
+ end
284
+ return m[1] if (m = config_block.match(/#{Regexp.escape(field)}\s+["']([^"']+)["']/m))
285
+
286
+ nil
287
+ end
288
+
289
+ # Kotlin DSL:同上
290
+ def extract_signing_password_kts(config_block, field)
291
+ if (m = config_block.match(/#{Regexp.escape(field)}\s*=\s*System\.getenv\(\s*["']([^"']+)["']\s*\)(?:\s*\?:\s*["']([^"']+)["'])?/m))
292
+ v = ENV.fetch(m[1], nil)
293
+ return v if v.present?
294
+ return m[2] if m[2] && !m[2].to_s.empty?
295
+
296
+ return
297
+ end
298
+ return m[1] if (m = config_block.match(/#{Regexp.escape(field)}\s*=\s*["']([^"']+)["']/m))
299
+
300
+ nil
301
+ end
302
+
303
+ # =================== JPS 集成 ===================
304
+
305
+ # 初始化 JPSClient(幂等,只初始化一次)
306
+ def initialize_jps_client
307
+ return if @jps_client
308
+
309
+ config_file = File.join(File.expand_path(Pindoconfig.instance.pindo_common_configdir),
310
+ "jps_client_config.json")
311
+ raise "JPSClient 配置文件不存在: #{config_file}" unless File.exist?(config_file)
312
+
313
+ @jps_client = JPSClient::Client.new(config_file: config_file)
314
+ raise "JPSClient 登录失败" unless @jps_client.do_login(force_login: false)
315
+ end
316
+
317
+ # 从 JPS 获取 JKS 证书并下载到本地缓存
318
+ # @param bundle_id [String] Android 包名(Application ID)
319
+ # @param build_type [String] "debug" 或 "release"
320
+ # @return [Hash] { "storeFile", "storePassword", "keyAlias", "keyPassword" }
321
+ def fetch_jks_from_jps(bundle_id:, build_type: "debug")
322
+ initialize_jps_client
323
+
324
+ result = @jps_client.get_android_jks_detail(bundle_id: bundle_id)
325
+ data = result&.dig("data")
326
+
327
+ row = pick_jks_detail_row_for_bundle(data, bundle_id)
328
+ jks_url = resolve_jks_download_url(row, bundle_id)
329
+
330
+ raise "JPS 未找到 JKS 证书 (bundleId=#{bundle_id}),请先在 JPS 平台创建对应证书" unless row && jks_url && !jks_url.to_s.empty?
331
+
332
+ # 下载 JKS 文件到临时目录(仅当前 bundle 对应的一条 URL;ZIP 包会在下方拆成单个 .jks)
333
+ tmp_dir = File.join(Dir.tmpdir, "pindo_jks")
334
+ FileUtils.mkdir_p(tmp_dir)
335
+ local_jks_path = File.join(tmp_dir, "#{bundle_id}.jks")
336
+
337
+ success = Pindo::FileDownloader.download(url: jks_url, dest_path: local_jks_path, silent: true)
338
+ raise "JKS 文件下载失败 (bundleId=#{bundle_id})" unless success
339
+
340
+ materialize_single_jks_file!(local_jks_path, bundle_id)
341
+
342
+ {
343
+ "storeFile" => local_jks_path,
344
+ "storePassword" => row["storePassword"],
345
+ "keyAlias" => row["aliasName"] || bundle_id,
346
+ "keyPassword" => row["keyPassword"],
347
+ }
348
+ end
349
+
350
+ # 从 JPS data(单条 Hash、列表、或含 list 的包装)中取出与 bundle_id 匹配的一条记录(不取未匹配的多条中的首条)
351
+ def pick_jks_detail_row_for_bundle(data, bundle_id)
352
+ bid = bundle_id.to_s.strip
353
+ return if bid.empty?
354
+
355
+ case data
356
+ when Array
357
+ explicit = data.find { |r| r.is_a?(Hash) && (r["bundleId"] || r["bundle_id"]).to_s.strip == bid }
358
+ return explicit if explicit
359
+ # 仅当接口已按 bundle 过滤且只返回一条时,允许省略 bundleId 字段
360
+ return data.first if data.size == 1 && data.first.is_a?(Hash) && jks_url_present?(data.first)
361
+ when Hash
362
+ %w[list records androidJksList rows dataList].each do |key|
363
+ inner = data[key]
364
+ next unless inner.is_a?(Array)
365
+
366
+ picked = pick_jks_detail_row_for_bundle(inner, bundle_id)
367
+ return picked if picked
368
+ end
369
+ if jks_url_present?(data)
370
+ row_bid = (data["bundleId"] || data["bundle_id"]).to_s.strip
371
+ return data if row_bid.empty? || row_bid == bid
372
+ end
373
+ end
374
+ nil
375
+ end
376
+
377
+ def jks_url_present?(row)
378
+ return false unless row.is_a?(Hash)
379
+
380
+ url = row["jksFileUrl"] || row["jks_file_url"]
381
+ return true if url && !url.to_s.empty?
382
+
383
+ urls = row["jksFileUrls"] || row["jks_file_urls"]
384
+ urls.is_a?(Array) && urls.any?
385
+ end
386
+
387
+ # 解析可下载的 URL:单链接,或与 bundleIds 平行数组时的对应项(避免下载整包多 URL 时全部拉取)
388
+ def resolve_jks_download_url(row, bundle_id)
389
+ return unless row.is_a?(Hash)
390
+
391
+ url = row["jksFileUrl"] || row["jks_file_url"]
392
+ return url if url && !url.to_s.empty?
393
+
394
+ urls = row["jksFileUrls"] || row["jks_file_urls"]
395
+ bids = row["bundleIds"] || row["bundle_ids"]
396
+ if urls.is_a?(Array) && bids.is_a?(Array) && urls.size == bids.size
397
+ idx = bids.index { |b| b.to_s.strip == bundle_id.to_s.strip }
398
+ return urls[idx] if idx
399
+ end
400
+
401
+ nil
402
+ end
403
+
404
+ ZIP_MAGIC = "PK\x03\x04"
405
+
406
+ # 若下载结果为 ZIP(服务端偶发打成证书包),只保留当前 bundle 对应的一个 .jks,避免落盘整包多个证书
407
+ def materialize_single_jks_file!(local_path, bundle_id)
408
+ return unless File.file?(local_path) && File.size(local_path) >= 4
409
+
410
+ magic = File.binread(local_path, 4)
411
+ return if magic != ZIP_MAGIC
412
+
413
+ tmp_extract = Dir.mktmpdir("pindo_jks_unzip")
414
+ begin
415
+ ok = system("unzip", "-q", "-o", local_path, "-d", tmp_extract)
416
+ raise "解压 JKS 压缩包失败(需要 unzip 命令)" unless ok
417
+
418
+ all_jks = Dir.glob(File.join(tmp_extract, "**", "*.jks"))
419
+ raise "压缩包内未找到 .jks 文件 (bundleId=#{bundle_id})" if all_jks.empty?
420
+
421
+ bid = bundle_id.to_s.strip
422
+ chosen = all_jks.find { |f| File.basename(f, ".jks") == bid }
423
+ chosen ||= all_jks.find { |f| File.basename(f, ".jks").tr("_", ".") == bid }
424
+ chosen ||= all_jks.find { |f| File.basename(f, ".jks").tr(".", "_") == bid.tr(".", "_") }
425
+ chosen ||= ((all_jks.size == 1) ? all_jks.first : nil)
426
+
427
+ unless chosen
428
+ names = all_jks.map { |f| File.basename(f) }.join(", ")
429
+ raise "ZIP 内含多个 JKS(#{names}),无法匹配 bundleId=#{bundle_id},请将目标证书命名为 #{bid}.jks 或保证包内仅一个 .jks"
430
+ end
431
+
432
+ FileUtils.cp(chosen, local_path)
433
+ ensure
434
+ FileUtils.rm_rf(tmp_extract)
435
+ end
436
+ end
437
+
168
438
  # =================== Keystore 文件管理 ===================
169
439
 
170
440
  # 拷贝 keystore 文件到项目的 signing 目录
@@ -188,7 +458,6 @@ module Pindo
188
458
 
189
459
  # 拷贝文件(如果源文件和目标文件不同)
190
460
  unless File.exist?(target_file) && FileUtils.identical?(source_file, target_file)
191
- puts " 拷贝 keystore 文件到项目: signing/#{target_filename}"
192
461
  FileUtils.cp(source_file, target_file)
193
462
  # 设置文件权限为只读
194
463
  File.chmod(0444, target_file)
@@ -196,6 +465,7 @@ module Pindo
196
465
 
197
466
  # 更新配置中的路径为相对路径(用于后续生成配置)
198
467
  config["relative_store_file"] = "$rootDir/signing/#{target_filename}"
468
+ register_managed_signing_path!(target_file)
199
469
  end
200
470
 
201
471
  # =================== 确保 keystore 配置的核心方法 ===================
@@ -205,7 +475,7 @@ module Pindo
205
475
  # 1. 检查 buildTypes 中是否有 signingConfig 引用
206
476
  # 2. 如果有引用,始终更新对应的 signingConfigs 为相对路径
207
477
  # 3. 不再检查配置是否"完整",直接替换
208
- def ensure_keystore_config_groovy(gradle_file, project_dir, build_type, sign_config = nil)
478
+ def ensure_keystore_config_groovy(gradle_file, _project_dir, build_type, sign_config = nil, bundle_id: nil)
209
479
  content = File.read(gradle_file)
210
480
  original_content = content.dup
211
481
 
@@ -217,7 +487,7 @@ module Pindo
217
487
  puts " buildTypes.#{build_type} 引用的签名配置: #{signing_config_name}"
218
488
 
219
489
  # 获取 Pindo 配置
220
- sign_config ||= get_android_sign_config
490
+ sign_config ||= (bundle_id ? get_android_sign_config(bundle_id: bundle_id) : nil)
221
491
  pindo_config = sign_config ? sign_config[build_type] : nil
222
492
 
223
493
  unless pindo_config
@@ -244,11 +514,11 @@ module Pindo
244
514
  end
245
515
 
246
516
  # 写入文件
247
- if content != original_content
517
+ if content == original_content
518
+ puts " ✓ build.gradle 无需修改"
519
+ else
248
520
  File.write(gradle_file, content)
249
521
  puts " ✓ build.gradle 已更新"
250
- else
251
- puts " ✓ build.gradle 无需修改"
252
522
  end
253
523
 
254
524
  true
@@ -259,7 +529,7 @@ module Pindo
259
529
  # 1. 检查 buildTypes 中是否有 signingConfig 引用
260
530
  # 2. 如果有引用,始终更新对应的 signingConfigs 为相对路径
261
531
  # 3. 不再检查配置是否"完整",直接替换
262
- def ensure_keystore_config_kts(gradle_file, project_dir, build_type, sign_config = nil)
532
+ def ensure_keystore_config_kts(gradle_file, _project_dir, build_type, sign_config = nil, bundle_id: nil)
263
533
  content = File.read(gradle_file)
264
534
  original_content = content.dup
265
535
 
@@ -271,7 +541,7 @@ module Pindo
271
541
  puts " buildTypes.#{build_type} 引用的签名配置: #{signing_config_name}"
272
542
 
273
543
  # 获取 Pindo 配置
274
- sign_config ||= get_android_sign_config
544
+ sign_config ||= (bundle_id ? get_android_sign_config(bundle_id: bundle_id) : nil)
275
545
  pindo_config = sign_config ? sign_config[build_type] : nil
276
546
 
277
547
  unless pindo_config
@@ -298,11 +568,11 @@ module Pindo
298
568
  end
299
569
 
300
570
  # 写入文件
301
- if content != original_content
571
+ if content == original_content
572
+ puts " ✓ build.gradle.kts 无需修改"
573
+ else
302
574
  File.write(gradle_file, content)
303
575
  puts " ✓ build.gradle.kts 已更新"
304
- else
305
- puts " ✓ build.gradle.kts 无需修改"
306
576
  end
307
577
 
308
578
  true
@@ -313,7 +583,7 @@ module Pindo
313
583
  def extract_signing_config_reference_safely_groovy(content, build_type)
314
584
  # 提取整个 buildTypes 块
315
585
  build_types_match = content.match(/buildTypes\s*\{[\s\S]*?^\}/m)
316
- return nil unless build_types_match
586
+ return unless build_types_match
317
587
 
318
588
  build_types_block = build_types_match[0]
319
589
 
@@ -323,7 +593,7 @@ module Pindo
323
593
  # 2. buildTypes {\n debug {
324
594
  # 3. buildTypes {release { debug {
325
595
  if build_types_block =~ /#{build_type}\s*\{[^}]*signingConfig\s+signingConfigs\.(\w+)/m
326
- return $1
596
+ return ::Regexp.last_match(1)
327
597
  end
328
598
 
329
599
  nil
@@ -333,18 +603,16 @@ module Pindo
333
603
  def extract_signing_config_reference_from_build_types_groovy(content, build_type)
334
604
  # 提取 buildTypes 块
335
605
  build_types_match = content.match(/buildTypes\s*\{([\s\S]*?)^\s*\}/m)
336
- return nil unless build_types_match
606
+ return unless build_types_match
337
607
 
338
608
  build_types_content = build_types_match[1]
339
609
 
340
610
  # 提取指定 buildType 的块
341
611
  build_type_block = extract_build_type_block_from_content_groovy(build_types_content, build_type)
342
- return nil unless build_type_block
612
+ return unless build_type_block
343
613
 
344
614
  # 提取 signingConfig 引用
345
- if build_type_block =~ /signingConfig\s+signingConfigs\.(\w+)/
346
- return $1
347
- end
615
+ return ::Regexp.last_match(1) if build_type_block =~ /signingConfig\s+signingConfigs\.(\w+)/
348
616
 
349
617
  nil
350
618
  end
@@ -353,13 +621,13 @@ module Pindo
353
621
  def extract_signing_config_reference_safely_kts(content, build_type)
354
622
  # 提取整个 buildTypes 块
355
623
  build_types_match = content.match(/buildTypes\s*\{[\s\S]*?^\}/m)
356
- return nil unless build_types_match
624
+ return unless build_types_match
357
625
 
358
626
  build_types_block = build_types_match[0]
359
627
 
360
628
  # 使用更宽松的正则查找 buildType 块和其中的 signingConfig
361
629
  if build_types_block =~ /getByName\s*\(\s*"#{build_type}"\s*\)\s*\{[^}]*signingConfig\s*=\s*signingConfigs\.getByName\s*\(\s*"(\w+)"\s*\)/m
362
- return $1
630
+ return ::Regexp.last_match(1)
363
631
  end
364
632
 
365
633
  nil
@@ -369,17 +637,17 @@ module Pindo
369
637
  def extract_signing_config_reference_from_build_types_kts(content, build_type)
370
638
  # 提取 buildTypes 块
371
639
  build_types_match = content.match(/buildTypes\s*\{([\s\S]*?)^\s*\}/m)
372
- return nil unless build_types_match
640
+ return unless build_types_match
373
641
 
374
642
  build_types_content = build_types_match[1]
375
643
 
376
644
  # 提取指定 buildType 的块
377
645
  build_type_block = extract_build_type_block_from_content_kts(build_types_content, build_type)
378
- return nil unless build_type_block
646
+ return unless build_type_block
379
647
 
380
648
  # 提取 signingConfig 引用
381
649
  if build_type_block =~ /signingConfig\s*=\s*signingConfigs\.getByName\s*\(\s*"(\w+)"\s*\)/
382
- return $1
650
+ return ::Regexp.last_match(1)
383
651
  end
384
652
 
385
653
  nil
@@ -388,20 +656,20 @@ module Pindo
388
656
  # 从内容中提取 buildType 块(Groovy)
389
657
  def extract_build_type_block_from_content_groovy(content, build_type)
390
658
  match = content.match(/^\s*#{build_type}\s*\{/m)
391
- return nil unless match
659
+ return unless match
392
660
 
393
661
  start_pos = match.begin(0)
394
662
  brace_count = 0
395
663
  in_block = false
396
664
  end_pos = start_pos
397
665
 
398
- content[start_pos..-1].each_char.with_index(start_pos) do |char, i|
399
- if char == '{'
666
+ content[start_pos..].each_char.with_index(start_pos) do |char, i|
667
+ if char == "{"
400
668
  brace_count += 1
401
669
  in_block = true
402
- elsif char == '}'
670
+ elsif char == "}"
403
671
  brace_count -= 1
404
- if in_block && brace_count == 0
672
+ if in_block && brace_count.zero?
405
673
  end_pos = i + 1
406
674
  break
407
675
  end
@@ -412,22 +680,25 @@ module Pindo
412
680
  end
413
681
 
414
682
  # 从内容中提取 buildType 块(Kotlin DSL)
683
+ # 支持 getByName("debug") 与 create("jps")(Workflow 注入常用)
415
684
  def extract_build_type_block_from_content_kts(content, build_type)
416
- match = content.match(/^\s*getByName\s*\(\s*"#{build_type}"\s*\)\s*\{/m)
417
- return nil unless match
685
+ escaped = Regexp.escape(build_type.to_s)
686
+ match = content.match(/^\s*getByName\s*\(\s*"#{escaped}"\s*\)\s*\{/m) ||
687
+ content.match(/^\s*create\s*\(\s*"#{escaped}"\s*\)\s*\{/m)
688
+ return unless match
418
689
 
419
690
  start_pos = match.begin(0)
420
691
  brace_count = 0
421
692
  in_block = false
422
693
  end_pos = start_pos
423
694
 
424
- content[start_pos..-1].each_char.with_index(start_pos) do |char, i|
425
- if char == '{'
695
+ content[start_pos..].each_char.with_index(start_pos) do |char, i|
696
+ if char == "{"
426
697
  brace_count += 1
427
698
  in_block = true
428
- elsif char == '}'
699
+ elsif char == "}"
429
700
  brace_count -= 1
430
- if in_block && brace_count == 0
701
+ if in_block && brace_count.zero?
431
702
  end_pos = i + 1
432
703
  break
433
704
  end
@@ -441,10 +712,10 @@ module Pindo
441
712
  # 增强检查:不仅检查字段是否存在,还检查是否使用了相对路径
442
713
  def check_signing_config_groovy(content, config_name)
443
714
  signing_configs_block = extract_signing_configs_groovy(content)
444
- return nil unless signing_configs_block
715
+ return unless signing_configs_block
445
716
 
446
717
  config_block = extract_config_block_by_name_groovy(signing_configs_block, config_name)
447
- return nil unless config_block
718
+ return unless config_block
448
719
 
449
720
  config_block = remove_groovy_comments(config_block)
450
721
 
@@ -456,27 +727,25 @@ module Pindo
456
727
 
457
728
  # 检查是否使用了相对路径(rootProject.file 或 signing 目录)
458
729
  uses_relative_path = config_block =~ /storeFile\s+rootProject\.file/ ||
459
- config_block =~ /storeFile\s+file\(["']signing\//
730
+ config_block =~ %r{storeFile\s+file\(["']signing/}
460
731
 
461
732
  # 检查是否使用了绝对路径(不推荐)
462
- uses_absolute_path = config_block =~ /storeFile\s+file\(["']\/Users\// ||
463
- config_block =~ /storeFile\s+file\(["']\/home\// ||
464
- config_block =~ /storeFile\s+file\(["']C:\\/
465
-
466
- if has_store_file && has_store_password && has_key_alias && has_key_password
467
- if uses_absolute_path
468
- # 使用了绝对路径,需要更新
469
- puts " ⚠ 检测到使用绝对路径,需要更新为相对路径"
470
- nil
471
- elsif uses_relative_path
472
- # 使用了相对路径,配置正确
473
- { exists: true, relative_path: true }
474
- else
475
- # 可能使用了其他形式的路径,保守处理
476
- { exists: true }
477
- end
478
- else
733
+ uses_absolute_path = config_block =~ %r{storeFile\s+file\(["']/Users/} ||
734
+ config_block =~ %r{storeFile\s+file\(["']/home/} ||
735
+ config_block =~ /storeFile\s+file\(["']C:\\/
736
+
737
+ return unless has_store_file && has_store_password && has_key_alias && has_key_password
738
+
739
+ if uses_absolute_path
740
+ # 使用了绝对路径,需要更新
741
+ puts " ⚠ 检测到使用绝对路径,需要更新为相对路径"
479
742
  nil
743
+ elsif uses_relative_path
744
+ # 使用了相对路径,配置正确
745
+ { exists: true, relative_path: true }
746
+ else
747
+ # 可能使用了其他形式的路径,保守处理
748
+ { exists: true }
480
749
  end
481
750
  end
482
751
 
@@ -485,10 +754,10 @@ module Pindo
485
754
  # 不解析复杂的变量引用,统一用 Pindo 配置替换
486
755
  def check_signing_config_kts(content, config_name)
487
756
  signing_configs_block = extract_signing_configs_kts(content)
488
- return nil unless signing_configs_block
757
+ return unless signing_configs_block
489
758
 
490
759
  config_block = extract_config_block_by_name_kts(signing_configs_block, config_name)
491
- return nil unless config_block
760
+ return unless config_block
492
761
 
493
762
  config_block = remove_kts_comments(config_block)
494
763
 
@@ -500,27 +769,25 @@ module Pindo
500
769
 
501
770
  # 检查是否使用了相对路径(rootProject.file 或 signing 目录)
502
771
  uses_relative_path = config_block =~ /storeFile\s*=\s*rootProject\.file/ ||
503
- config_block =~ /storeFile\s*=\s*file\(["']signing\//
772
+ config_block =~ %r{storeFile\s*=\s*file\(["']signing/}
504
773
 
505
774
  # 检查是否使用了绝对路径(不推荐)
506
- uses_absolute_path = config_block =~ /storeFile\s*=\s*file\(["']\/Users\// ||
507
- config_block =~ /storeFile\s*=\s*file\(["']\/home\// ||
508
- config_block =~ /storeFile\s*=\s*file\(["']C:\\/
509
-
510
- if has_store_file && has_store_password && has_key_alias && has_key_password
511
- if uses_absolute_path
512
- # 使用了绝对路径,需要更新
513
- puts " ⚠ 检测到使用绝对路径,需要更新为相对路径"
514
- nil
515
- elsif uses_relative_path
516
- # 使用了相对路径,配置正确
517
- { exists: true, relative_path: true }
518
- else
519
- # 可能使用了其他形式的路径,保守处理
520
- { exists: true }
521
- end
522
- else
775
+ uses_absolute_path = config_block =~ %r{storeFile\s*=\s*file\(["']/Users/} ||
776
+ config_block =~ %r{storeFile\s*=\s*file\(["']/home/} ||
777
+ config_block =~ /storeFile\s*=\s*file\(["']C:\\/
778
+
779
+ return unless has_store_file && has_store_password && has_key_alias && has_key_password
780
+
781
+ if uses_absolute_path
782
+ # 使用了绝对路径,需要更新
783
+ puts " ⚠ 检测到使用绝对路径,需要更新为相对路径"
523
784
  nil
785
+ elsif uses_relative_path
786
+ # 使用了相对路径,配置正确
787
+ { exists: true, relative_path: true }
788
+ else
789
+ # 可能使用了其他形式的路径,保守处理
790
+ { exists: true }
524
791
  end
525
792
  end
526
793
 
@@ -552,18 +819,18 @@ module Pindo
552
819
  # 先找到 signingConfigs 块的位置
553
820
  if content =~ /signingConfigs\s*\{/
554
821
  # 找到 signingConfigs 块的开始位置
555
- start_match = $~
822
+ start_match = $LAST_MATCH_INFO
556
823
  start_pos = start_match.end(0)
557
824
 
558
825
  # 找到对应的结束大括号
559
826
  brace_count = 1
560
827
  end_pos = start_pos
561
- content[start_pos..-1].each_char.with_index do |char, index|
562
- if char == '{'
828
+ content[start_pos..].each_char.with_index do |char, index|
829
+ if char == "{"
563
830
  brace_count += 1
564
- elsif char == '}'
831
+ elsif char == "}"
565
832
  brace_count -= 1
566
- if brace_count == 0
833
+ if brace_count.zero?
567
834
  end_pos = start_pos + index
568
835
  break
569
836
  end
@@ -577,7 +844,7 @@ module Pindo
577
844
  modified_content = remove_config_block_from_content(signing_configs_content, config_name)
578
845
 
579
846
  # 重组完整内容
580
- content[0...start_pos] + modified_content + content[end_pos..-1]
847
+ content[0...start_pos] + modified_content + content[end_pos..]
581
848
  else
582
849
  content
583
850
  end
@@ -601,14 +868,12 @@ module Pindo
601
868
  # 如果在目标块中,计算大括号
602
869
  if in_target_block
603
870
  line.chars.each do |char|
604
- brace_count += 1 if char == '{'
605
- brace_count -= 1 if char == '}'
871
+ brace_count += 1 if char == "{"
872
+ brace_count -= 1 if char == "}"
606
873
  end
607
874
 
608
875
  # 如果大括号平衡了,说明块结束了
609
- if brace_count == 0
610
- in_target_block = false
611
- end
876
+ in_target_block = false if brace_count.zero?
612
877
  next
613
878
  end
614
879
 
@@ -625,18 +890,18 @@ module Pindo
625
890
  # 先找到 signingConfigs 块的位置
626
891
  if content =~ /signingConfigs\s*\{/
627
892
  # 找到 signingConfigs 块的开始位置
628
- start_match = $~
893
+ start_match = $LAST_MATCH_INFO
629
894
  start_pos = start_match.end(0)
630
895
 
631
896
  # 找到对应的结束大括号
632
897
  brace_count = 1
633
898
  end_pos = start_pos
634
- content[start_pos..-1].each_char.with_index do |char, index|
635
- if char == '{'
899
+ content[start_pos..].each_char.with_index do |char, index|
900
+ if char == "{"
636
901
  brace_count += 1
637
- elsif char == '}'
902
+ elsif char == "}"
638
903
  brace_count -= 1
639
- if brace_count == 0
904
+ if brace_count.zero?
640
905
  end_pos = start_pos + index
641
906
  break
642
907
  end
@@ -650,7 +915,7 @@ module Pindo
650
915
  modified_content = remove_config_block_from_content_kts(signing_configs_content, config_name)
651
916
 
652
917
  # 重组完整内容
653
- content[0...start_pos] + modified_content + content[end_pos..-1]
918
+ content[0...start_pos] + modified_content + content[end_pos..]
654
919
  else
655
920
  content
656
921
  end
@@ -674,14 +939,12 @@ module Pindo
674
939
  # 如果在目标块中,计算大括号
675
940
  if in_target_block
676
941
  line.chars.each do |char|
677
- brace_count += 1 if char == '{'
678
- brace_count -= 1 if char == '}'
942
+ brace_count += 1 if char == "{"
943
+ brace_count -= 1 if char == "}"
679
944
  end
680
945
 
681
946
  # 如果大括号平衡了,说明块结束了
682
- if brace_count == 0
683
- in_target_block = false
684
- end
947
+ in_target_block = false if brace_count.zero?
685
948
  next
686
949
  end
687
950
 
@@ -695,118 +958,142 @@ module Pindo
695
958
  # =================== 从工程读取 keystore 配置的辅助方法 ===================
696
959
 
697
960
  # 处理 Kotlin DSL (build.gradle.kts)
698
- # Pindo 策略:配置已经被统一为 signingConfigs.debug/release,直接读取即可
699
- def get_keystore_config_kts(gradle_kts_path, project_path, debug)
961
+ # workflow:按 signingConfigs.debug/release(含互相回退);有 workflow:仅从 buildTypes.<workflow> → 对应 signingConfigs,不回退
962
+ def get_keystore_config_kts(gradle_kts_path, project_path, debug, workflow_build_type: nil)
700
963
  content = File.read(gradle_kts_path)
701
964
  puts "读取 #{File.basename(gradle_kts_path)} 文件"
702
965
 
703
- build_type = debug ? 'debug' : 'release'
704
-
705
- # 直接从统一的 signingConfigs.debug/release 读取
706
966
  signing_configs_block = extract_signing_configs_kts(content)
707
- return nil unless signing_configs_block
708
-
709
- config_block = extract_config_block_by_name_kts(signing_configs_block, build_type)
967
+ return unless signing_configs_block
968
+
969
+ config_block = nil
970
+ if workflow_build_type && !workflow_build_type.to_s.empty?
971
+ w = workflow_build_type.to_s
972
+ puts " workflow 构建:仅从 buildTypes.#{w} 解析 signingConfig(不回退 debug/release)"
973
+ signing_config_name = extract_signing_config_reference_from_build_types_kts(content, w)
974
+ if signing_config_name.nil? || signing_config_name.to_s.empty?
975
+ raise ArgumentError,
976
+ "workflow 构建:无法在 buildTypes.#{w} 中解析 signingConfig(需要 signingConfig = signingConfigs.getByName(\"…\") 或等价配置)"
977
+ end
710
978
 
711
- # 如果找不到对应的配置,尝试使用另一个配置
712
- if config_block.nil?
713
- alt_build_type = debug ? 'release' : 'debug'
714
- puts " 未找到 signingConfigs.#{build_type},尝试使用 signingConfigs.#{alt_build_type}"
715
- config_block = extract_config_block_by_name_kts(signing_configs_block, alt_build_type)
716
- return nil unless config_block
979
+ # puts " 找到签名配置: signingConfigs.#{signing_config_name}"
980
+ config_block = extract_config_block_by_name_kts(signing_configs_block, signing_config_name)
981
+ if config_block.nil?
982
+ raise ArgumentError, "workflow 构建:未找到 signingConfigs.#{signing_config_name} 配置块"
983
+ end
984
+ else
985
+ build_type = debug ? "debug" : "release"
986
+ config_block = extract_config_block_by_name_kts(signing_configs_block, build_type)
987
+ if config_block.nil?
988
+ alt_build_type = debug ? "release" : "debug"
989
+ puts " 未找到 signingConfigs.#{build_type},尝试使用 signingConfigs.#{alt_build_type}"
990
+ config_block = extract_config_block_by_name_kts(signing_configs_block, alt_build_type)
991
+ end
992
+ return unless config_block
717
993
  end
718
994
 
719
995
  config_block = remove_kts_comments(config_block)
720
996
 
721
- # 直接提取字段值(不处理变量引用)
722
- # 支持 file() 和 rootProject.file() 两种格式
723
- store_file = config_block[/storeFile\s*=\s*(?:rootProject\.)?file\(["']([^"']+)["']\)/, 1]
724
- store_password = config_block[/storePassword\s*=\s*["']([^"']+)["']/, 1]
725
- key_alias = config_block[/keyAlias\s*=\s*["']([^"']+)["']/, 1]
726
- key_password = config_block[/keyPassword\s*=\s*["']([^"']+)["']/, 1]
997
+ # storeFile(RELEASE_KEYSTORE_PATH 风格或历史 file(...));口令/别名 getenv / ?: / 明文
998
+ store_file = resolve_keystore_path_from_signing_block(config_block, project_path)
999
+ store_password = extract_signing_password_kts(config_block, "storePassword")
1000
+ key_password = extract_signing_password_kts(config_block, "keyPassword")
1001
+ key_alias = extract_signing_password_kts(config_block, "keyAlias")
727
1002
 
728
1003
  # 解析相对路径
729
1004
  store_file = fix_store_file_path(store_file, project_path) if store_file
730
1005
 
731
1006
  {
732
- store_file: store_file,
1007
+ store_file: store_file,
733
1008
  store_password: store_password,
734
- key_alias: key_alias,
735
- key_password: key_password
1009
+ key_alias: key_alias,
1010
+ key_password: key_password,
736
1011
  }
737
1012
  end
738
1013
 
739
1014
  # 处理 Groovy DSL (build.gradle)
740
- def get_keystore_config_groovy(gradle_path, project_path, debug)
1015
+ # workflow:按 buildTypes.debug/release(含互相回退);有 workflow:仅从 buildTypes.<workflow> 解析,不回退
1016
+ def get_keystore_config_groovy(gradle_path, project_path, debug, workflow_build_type: nil)
741
1017
  content = File.read(gradle_path)
742
1018
  puts "读取 #{File.basename(gradle_path)} 文件"
743
1019
 
744
- build_type = debug ? 'debug' : 'release'
745
-
746
- # 先从 buildTypes 中找到实际引用的 signingConfig 名称
747
- signing_config_name = extract_signing_config_name_from_build_type(content, build_type)
1020
+ signing_config_name = nil
1021
+ if workflow_build_type && !workflow_build_type.to_s.empty?
1022
+ w = workflow_build_type.to_s
1023
+ puts " workflow 构建:仅从 buildTypes.#{w} 解析 signingConfig(不回退 debug/release)"
1024
+ signing_config_name = extract_signing_config_name_from_build_type(content, w)
1025
+ if signing_config_name.nil? || signing_config_name.to_s.empty?
1026
+ raise ArgumentError,
1027
+ "workflow 构建:无法在 buildTypes.#{w} 中解析 signingConfig(需要 signingConfig signingConfigs.xxx)"
1028
+ end
1029
+ else
1030
+ build_type = debug ? "debug" : "release"
1031
+ signing_config_name = extract_signing_config_name_from_build_type(content, build_type)
748
1032
 
749
- if signing_config_name.nil?
750
- # 如果当前 buildType 没有配置,尝试另一个
751
- alt_build_type = debug ? 'release' : 'debug'
752
- puts " buildTypes.#{build_type} 未配置 signingConfig,尝试 buildTypes.#{alt_build_type}"
753
- signing_config_name = extract_signing_config_name_from_build_type(content, alt_build_type)
754
- end
1033
+ if signing_config_name.nil?
1034
+ alt_build_type = debug ? "release" : "debug"
1035
+ puts " buildTypes.#{build_type} 未配置 signingConfig,尝试 buildTypes.#{alt_build_type}"
1036
+ signing_config_name = extract_signing_config_name_from_build_type(content, alt_build_type)
1037
+ end
755
1038
 
756
- if signing_config_name.nil?
757
- puts " 未找到任何 signingConfig 配置"
758
- return nil
1039
+ if signing_config_name.nil?
1040
+ puts " 未找到任何 signingConfig 配置"
1041
+ return
1042
+ end
759
1043
  end
760
1044
 
761
- puts " 找到签名配置: signingConfigs.#{signing_config_name}"
1045
+ # puts " 找到签名配置: signingConfigs.#{signing_config_name}"
762
1046
 
763
1047
  # 从 signingConfigs 中读取对应名称的配置
764
1048
  signing_configs_block = extract_signing_configs_groovy(content)
765
- return nil unless signing_configs_block
1049
+ return unless signing_configs_block
766
1050
 
767
1051
  config_block = extract_config_block_by_name_groovy(signing_configs_block, signing_config_name)
768
1052
 
769
1053
  if config_block.nil?
1054
+ if workflow_build_type && !workflow_build_type.to_s.empty?
1055
+ raise ArgumentError, "workflow 构建:未找到 signingConfigs.#{signing_config_name} 的详细配置块"
1056
+ end
1057
+
770
1058
  puts " 未找到 signingConfigs.#{signing_config_name} 的详细配置"
771
- return nil
1059
+ return
772
1060
  end
773
1061
 
774
1062
  config_block = remove_groovy_comments(config_block)
775
1063
 
776
- # 直接提取字段值(不处理变量引用)
777
- # 支持 file() 和 rootProject.file() 两种格式
778
- store_file = config_block[/storeFile\s+(?:rootProject\.)?file\(["']([^"']+)["']\)/, 1]
779
- store_password = config_block[/storePassword\s+["']([^"']+)["']/, 1]
780
- key_alias = config_block[/keyAlias\s+["']([^"']+)["']/, 1]
781
- key_password = config_block[/keyPassword\s+["']([^"']+)["']/, 1]
1064
+ # storeFile(RELEASE_KEYSTORE_PATH 风格或历史 file(...));口令/别名 getenv / ?: / 明文
1065
+ store_file = resolve_keystore_path_from_signing_block(config_block, project_path)
1066
+ store_password = extract_signing_password_groovy(config_block, "storePassword")
1067
+ key_password = extract_signing_password_groovy(config_block, "keyPassword")
1068
+ key_alias = extract_signing_password_groovy(config_block, "keyAlias")
782
1069
 
783
1070
  # 解析相对路径
784
1071
  store_file = fix_store_file_path(store_file, project_path) if store_file
785
1072
 
786
1073
  {
787
- store_file: store_file,
1074
+ store_file: store_file,
788
1075
  store_password: store_password,
789
- key_alias: key_alias,
790
- key_password: key_password
1076
+ key_alias: key_alias,
1077
+ key_password: key_password,
791
1078
  }
792
1079
  end
793
1080
 
794
1081
  def extract_signing_configs_kts(content)
795
1082
  # 提取 signingConfigs { ... },使用大括号计数来正确处理嵌套
796
1083
  if content =~ /signingConfigs\s*\{/
797
- start_match = $~
1084
+ start_match = $LAST_MATCH_INFO
798
1085
  start_pos = start_match.end(0)
799
1086
 
800
1087
  # 计算大括号平衡
801
1088
  brace_count = 1
802
1089
  end_pos = start_pos
803
1090
 
804
- content[start_pos..-1].each_char.with_index do |char, index|
805
- if char == '{'
1091
+ content[start_pos..].each_char.with_index do |char, index|
1092
+ if char == "{"
806
1093
  brace_count += 1
807
- elsif char == '}'
1094
+ elsif char == "}"
808
1095
  brace_count -= 1
809
- if brace_count == 0
1096
+ if brace_count.zero?
810
1097
  end_pos = start_pos + index
811
1098
  break
812
1099
  end
@@ -814,7 +1101,7 @@ module Pindo
814
1101
  end
815
1102
 
816
1103
  # 返回 signingConfigs 块的内容(不包括外层大括号)
817
- return content[start_pos...end_pos] if brace_count == 0
1104
+ return content[start_pos...end_pos] if brace_count.zero?
818
1105
  end
819
1106
 
820
1107
  nil
@@ -824,36 +1111,34 @@ module Pindo
824
1111
  def extract_signing_config_name_from_build_type(content, build_type)
825
1112
  # 先提取 buildTypes 块
826
1113
  if content =~ /buildTypes\s*\{/
827
- start_match = $~
1114
+ start_match = $LAST_MATCH_INFO
828
1115
  start_pos = start_match.end(0)
829
1116
 
830
1117
  # 计算大括号平衡来找到 buildTypes 块的结束
831
1118
  brace_count = 1
832
1119
  end_pos = start_pos
833
1120
 
834
- content[start_pos..-1].each_char.with_index do |char, index|
835
- if char == '{'
1121
+ content[start_pos..].each_char.with_index do |char, index|
1122
+ if char == "{"
836
1123
  brace_count += 1
837
- elsif char == '}'
1124
+ elsif char == "}"
838
1125
  brace_count -= 1
839
- if brace_count == 0
1126
+ if brace_count.zero?
840
1127
  end_pos = start_pos + index
841
1128
  break
842
1129
  end
843
1130
  end
844
1131
  end
845
1132
 
846
- build_types_block = content[start_pos...end_pos] if brace_count == 0
1133
+ build_types_block = content[start_pos...end_pos] if brace_count.zero?
847
1134
 
848
1135
  if build_types_block
849
1136
  # 提取特定 buildType 的配置块
850
1137
  build_type_block = extract_config_block_by_name_groovy(build_types_block, build_type)
851
1138
 
852
- if build_type_block
1139
+ if build_type_block && (build_type_block =~ /signingConfig\s+signingConfigs\.(\w+)/)
853
1140
  # 查找 signingConfig signingConfigs.XXX
854
- if build_type_block =~ /signingConfig\s+signingConfigs\.(\w+)/
855
- return $1
856
- end
1141
+ return ::Regexp.last_match(1)
857
1142
  end
858
1143
  end
859
1144
  end
@@ -864,19 +1149,19 @@ module Pindo
864
1149
  def extract_signing_configs_groovy(content)
865
1150
  # 提取 signingConfigs { ... },使用大括号计数来正确处理嵌套
866
1151
  if content =~ /signingConfigs\s*\{/
867
- start_match = $~
1152
+ start_match = $LAST_MATCH_INFO
868
1153
  start_pos = start_match.end(0)
869
1154
 
870
1155
  # 计算大括号平衡
871
1156
  brace_count = 1
872
1157
  end_pos = start_pos
873
1158
 
874
- content[start_pos..-1].each_char.with_index do |char, index|
875
- if char == '{'
1159
+ content[start_pos..].each_char.with_index do |char, index|
1160
+ if char == "{"
876
1161
  brace_count += 1
877
- elsif char == '}'
1162
+ elsif char == "}"
878
1163
  brace_count -= 1
879
- if brace_count == 0
1164
+ if brace_count.zero?
880
1165
  end_pos = start_pos + index
881
1166
  break
882
1167
  end
@@ -884,24 +1169,53 @@ module Pindo
884
1169
  end
885
1170
 
886
1171
  # 返回 signingConfigs 块的内容(不包括外层大括号)
887
- return content[start_pos...end_pos] if brace_count == 0
1172
+ return content[start_pos...end_pos] if brace_count.zero?
888
1173
  end
889
1174
 
890
1175
  nil
891
1176
  end
892
1177
 
1178
+ # 从第一个 `{` 起做大括号配对,返回内层正文(不含最外层花括号)。
1179
+ # 解决 workflow 注入的 signingConfig 内含 `if (...) { }` 时,非贪婪正则会在第一个 `}` 处截断、
1180
+ # 导致读不到 storeFile/keyAlias,bundletool 误用占位或错误别名(如表现为 debug 签名)。
1181
+ def extract_balanced_inner_after_open_brace(full_text, open_brace_index)
1182
+ return nil if open_brace_index.nil? || open_brace_index >= full_text.length
1183
+ return nil unless full_text[open_brace_index] == "{"
1184
+
1185
+ inner_start = open_brace_index + 1
1186
+ brace_count = 1
1187
+ i = inner_start
1188
+ while i < full_text.length
1189
+ ch = full_text[i]
1190
+ if ch == "{"
1191
+ brace_count += 1
1192
+ elsif ch == "}"
1193
+ brace_count -= 1
1194
+ return full_text[inner_start...i] if brace_count.zero?
1195
+ end
1196
+ i += 1
1197
+ end
1198
+ nil
1199
+ end
1200
+
893
1201
  # 根据配置名称提取配置块(Kotlin DSL)
894
1202
  def extract_config_block_by_name_kts(signing_configs_block, config_name)
895
- # 支持 create("xxx") { ... }
896
- match = signing_configs_block.match(/create\s*\(\s*["']#{config_name}["']\s*\)\s*\{([\s\S]*?)^\s*\}/m)
897
- match ? match[1] : nil
1203
+ name = config_name.to_s
1204
+ m = signing_configs_block.match(/create\s*\(\s*["']#{Regexp.escape(name)}["']\s*\)\s*\{/)
1205
+ return nil unless m
1206
+
1207
+ open_idx = m.end(0) - 1
1208
+ extract_balanced_inner_after_open_brace(signing_configs_block, open_idx)
898
1209
  end
899
1210
 
900
1211
  # 根据配置名称提取配置块(Groovy)
901
1212
  def extract_config_block_by_name_groovy(signing_configs_block, config_name)
902
- # 支持 xxx { ... }
903
- match = signing_configs_block.match(/#{config_name}\s*\{([\s\S]*?)^\s*\}/m)
904
- match ? match[1] : nil
1213
+ name = config_name.to_s
1214
+ m = signing_configs_block.match(/\b#{Regexp.escape(name)}\s*\{/)
1215
+ return nil unless m
1216
+
1217
+ open_idx = m.end(0) - 1
1218
+ extract_balanced_inner_after_open_brace(signing_configs_block, open_idx)
905
1219
  end
906
1220
 
907
1221
  def remove_kts_comments(block)
@@ -911,19 +1225,19 @@ module Pindo
911
1225
 
912
1226
  def remove_groovy_comments(block)
913
1227
  # 去除 // 和 /* ... */ 注释
914
- block = block.gsub(/\/\*[\s\S]*?\*\//, "")
1228
+ block = block.gsub(%r{/\*[\s\S]*?\*/}, "")
915
1229
  block.lines.reject { |line| line.strip.start_with?("//") }.join
916
1230
  end
917
1231
 
918
1232
  def fix_store_file_path(store_file, project_path)
919
- return nil if store_file.nil? || store_file.empty?
1233
+ return if store_file.blank?
920
1234
 
921
1235
  # 如果已经是绝对路径,直接返回
922
- return store_file if store_file.start_with?('/') && File.exist?(store_file)
1236
+ return store_file if store_file.start_with?("/") && File.exist?(store_file)
923
1237
 
924
1238
  # 处理相对路径(如 signing/xxx.keystore)
925
1239
  # 这种路径来自于 rootProject.file("signing/xxx.keystore")
926
- if !store_file.start_with?('/')
1240
+ unless store_file.start_with?("/")
927
1241
  # 相对路径,基于项目根目录解析
928
1242
  absolute_path = File.join(project_path, store_file)
929
1243
  return absolute_path if File.exist?(absolute_path)
@@ -933,72 +1247,72 @@ module Pindo
933
1247
  end
934
1248
 
935
1249
  # 兼容旧格式:$rootDir、${rootDir}、project.rootDir
936
- store_file = store_file.gsub(/^\$?\{?rootDir\}?\/?/, project_path + "/")
937
- store_file = store_file.gsub(/^project\.rootDir\/?/, project_path + "/")
1250
+ store_file = store_file.gsub(%r{^\$?\{?rootDir\}?/?}, "#{project_path}/")
1251
+ store_file.gsub(%r{^project\.rootDir/?}, "#{project_path}/")
1252
+ end
1253
+
1254
+ # 从 signingConfig 块解析 keystore 相对/绝对路径(优先 RELEASE_KEYSTORE_PATH + ENV,其次 ?: 回退,再兼容直接 file(...))
1255
+ def resolve_keystore_path_from_signing_block(config_block, project_path)
1256
+ if config_block.include?("RELEASE_KEYSTORE_PATH") &&
1257
+ (m = config_block.match(/System\.getenv\(\s*["']RELEASE_KEYSTORE_PATH["']\s*\)(?:\s*\?:\s*["']([^"']+)["'])?/m))
1258
+ fb = m[1]
1259
+ v = ENV.fetch(ENV_RELEASE_KEYSTORE_PATH, nil)
1260
+ rel = v.presence || fb
1261
+ return fix_store_file_path(rel, project_path) if rel && !rel.to_s.empty?
1262
+ end
938
1263
 
939
- store_file
1264
+ if (m = config_block.match(/storeFile\s*=\s*(?:rootProject\.)?file\(\s*["']([^"']+)["']\s*\)/m))
1265
+ return fix_store_file_path(m[1], project_path)
1266
+ end
1267
+ if (m = config_block.match(/storeFile\s+(?:rootProject\.)?file\(\s*["']([^"']+)["']\s*\)/m))
1268
+ return fix_store_file_path(m[1], project_path)
1269
+ end
1270
+
1271
+ nil
1272
+ end
1273
+
1274
+ def signing_keystore_fallback_relative(pindo_config)
1275
+ raw = pindo_config["relative_store_file"] || pindo_config["storeFile"]
1276
+ s = raw.to_s
1277
+ s = s.sub(%r{\A\$rootDir/?}, "").delete_prefix("/")
1278
+ s = File.basename(s) if s.start_with?("/") # 绝对路径时仅取文件名作弱回退
1279
+ s
1280
+ end
1281
+
1282
+ def escape_gradle_double_quoted(str)
1283
+ str.to_s.gsub("\\", "\\\\").gsub('"', '\\"')
940
1284
  end
941
1285
 
942
1286
  # =================== 写入 keystore 配置的辅助方法 ===================
943
1287
 
944
- # 生成签名配置代码块(Groovy)
1288
+ # 生成签名配置代码块(Groovy)——与工程约定:RELEASE_* + ?: 本地回退(明文仅回退串,来自 JPS 的敏感值走本次进程 ENV
945
1289
  def generate_signing_config_groovy(config_name, pindo_config)
946
- # 优先使用相对路径,如果不存在则使用绝对路径
947
- store_file = pindo_config["relative_store_file"] || pindo_config["storeFile"]
948
- store_password = pindo_config["storePassword"]
949
- key_alias = pindo_config["keyAlias"]
950
- key_password = pindo_config["keyPassword"]
951
-
952
- # 如果是相对路径($rootDir),使用不同的语法
953
- if store_file && store_file.start_with?("$rootDir")
954
- # 使用 rootProject.file() 或直接使用 $rootDir 变量
955
- store_file_line = "storeFile file(\"#{store_file.sub('$rootDir/', '')}\")"
956
- if store_file.include?("$rootDir/")
957
- # 对于 $rootDir/signing/xxx.keystore 格式
958
- relative_path = store_file.sub('$rootDir/', '')
959
- store_file_line = "storeFile rootProject.file(\"#{relative_path}\")"
960
- end
961
- else
962
- store_file_line = "storeFile file(\"#{store_file}\")"
963
- end
1290
+ rel_fb = escape_gradle_double_quoted(signing_keystore_fallback_relative(pindo_config))
1291
+ alias_fb = escape_gradle_double_quoted(pindo_config["keyAlias"])
964
1292
 
965
1293
  <<~GROOVY
966
1294
  #{config_name} {
967
- #{store_file_line}
968
- storePassword "#{store_password}"
969
- keyAlias "#{key_alias}"
970
- keyPassword "#{key_password}"
1295
+ def keystorePath = System.getenv("#{ENV_RELEASE_KEYSTORE_PATH}") ?: "#{rel_fb}"
1296
+ storeFile rootProject.file(keystorePath)
1297
+ storePassword System.getenv("#{ENV_RELEASE_KEYSTORE_PASSWORD}") ?: "123456"
1298
+ keyAlias System.getenv("#{ENV_RELEASE_KEY_ALIAS}") ?: "#{alias_fb}"
1299
+ keyPassword System.getenv("#{ENV_RELEASE_KEY_PASSWORD}") ?: "123456"
971
1300
  }
972
1301
  GROOVY
973
1302
  end
974
1303
 
975
1304
  # 生成签名配置代码块(Kotlin DSL)
976
1305
  def generate_signing_config_kts(config_name, pindo_config)
977
- # 优先使用相对路径,如果不存在则使用绝对路径
978
- store_file = pindo_config["relative_store_file"] || pindo_config["storeFile"]
979
- store_password = pindo_config["storePassword"]
980
- key_alias = pindo_config["keyAlias"]
981
- key_password = pindo_config["keyPassword"]
982
-
983
- # 如果是相对路径($rootDir),使用不同的语法
984
- if store_file && store_file.start_with?("$rootDir")
985
- # 对于 Kotlin DSL,使用 rootProject.file()
986
- if store_file.include?("$rootDir/")
987
- relative_path = store_file.sub('$rootDir/', '')
988
- store_file_line = "storeFile = rootProject.file(\"#{relative_path}\")"
989
- else
990
- store_file_line = "storeFile = file(\"#{store_file}\")"
991
- end
992
- else
993
- store_file_line = "storeFile = file(\"#{store_file}\")"
994
- end
1306
+ rel_fb = escape_gradle_double_quoted(signing_keystore_fallback_relative(pindo_config))
1307
+ alias_fb = escape_gradle_double_quoted(pindo_config["keyAlias"])
995
1308
 
996
1309
  <<~KOTLIN
997
1310
  create("#{config_name}") {
998
- #{store_file_line}
999
- storePassword = "#{store_password}"
1000
- keyAlias = "#{key_alias}"
1001
- keyPassword = "#{key_password}"
1311
+ val keystorePath = System.getenv("#{ENV_RELEASE_KEYSTORE_PATH}") ?: "#{rel_fb}"
1312
+ storeFile = rootProject.file(keystorePath)
1313
+ storePassword = System.getenv("#{ENV_RELEASE_KEYSTORE_PASSWORD}") ?: "123456"
1314
+ keyAlias = System.getenv("#{ENV_RELEASE_KEY_ALIAS}") ?: "#{alias_fb}"
1315
+ keyPassword = System.getenv("#{ENV_RELEASE_KEY_PASSWORD}") ?: "123456"
1002
1316
  }
1003
1317
  KOTLIN
1004
1318
  end
@@ -1009,14 +1323,15 @@ module Pindo
1009
1323
  if content =~ /signingConfigs\s*\{/
1010
1324
  # 在 signingConfigs 块内插入
1011
1325
  content.sub(/(signingConfigs\s*\{)/) do
1012
- "#{$1}\n #{signing_config_block.strip.gsub(/\n/, "\n ")}\n"
1326
+ "#{::Regexp.last_match(1)}\n #{signing_config_block.strip.gsub("\n", "\n ")}\n"
1013
1327
  end
1014
1328
  else
1015
1329
  # 没有 signingConfigs 块,在 android 块内创建
1016
1330
  android_match = content.match(/(android\s*\{)/m)
1017
1331
  if android_match
1018
1332
  insert_pos = content.index(android_match[0]) + android_match[0].length
1019
- signing_configs_block = "\n signingConfigs {\n #{signing_config_block.strip.gsub(/\n/, "\n ")}\n }\n"
1333
+ signing_configs_block = "\n signingConfigs {\n #{signing_config_block.strip.gsub("\n",
1334
+ "\n ")}\n }\n"
1020
1335
  content.insert(insert_pos, signing_configs_block)
1021
1336
  else
1022
1337
  content
@@ -1030,14 +1345,15 @@ module Pindo
1030
1345
  if content =~ /signingConfigs\s*\{/
1031
1346
  # 在 signingConfigs 块内插入
1032
1347
  content.sub(/(signingConfigs\s*\{)/) do
1033
- "#{$1}\n #{signing_config_block.strip.gsub(/\n/, "\n ")}\n"
1348
+ "#{::Regexp.last_match(1)}\n #{signing_config_block.strip.gsub("\n", "\n ")}\n"
1034
1349
  end
1035
1350
  else
1036
1351
  # 没有 signingConfigs 块,在 android 块内创建
1037
1352
  android_match = content.match(/(android\s*\{)/m)
1038
1353
  if android_match
1039
1354
  insert_pos = content.index(android_match[0]) + android_match[0].length
1040
- signing_configs_block = "\n signingConfigs {\n #{signing_config_block.strip.gsub(/\n/, "\n ")}\n }\n"
1355
+ signing_configs_block = "\n signingConfigs {\n #{signing_config_block.strip.gsub("\n",
1356
+ "\n ")}\n }\n"
1041
1357
  content.insert(insert_pos, signing_configs_block)
1042
1358
  else
1043
1359
  content
@@ -1062,7 +1378,7 @@ module Pindo
1062
1378
 
1063
1379
  # 第三步:在 buildType 块开始后添加 signingConfig 引用
1064
1380
  new_block = build_type_block.sub(/^(\s*#{build_type}\s*\{\s*)$/m) do
1065
- "#{$1}\n signingConfig signingConfigs.#{config_name}\n"
1381
+ "#{::Regexp.last_match(1)}\n signingConfig signingConfigs.#{config_name}\n"
1066
1382
  end
1067
1383
 
1068
1384
  # 第四步:替换 buildTypes 块中的内容
@@ -1090,7 +1406,7 @@ module Pindo
1090
1406
 
1091
1407
  # 第三步:在 buildType 块开始后添加 signingConfig 引用
1092
1408
  new_block = build_type_block.sub(/^(\s*getByName\s*\(\s*"#{build_type}"\s*\)\s*\{\s*)$/m) do
1093
- "#{$1}\n signingConfig = signingConfigs.getByName(\"#{config_name}\")\n"
1409
+ "#{::Regexp.last_match(1)}\n signingConfig = signingConfigs.getByName(\"#{config_name}\")\n"
1094
1410
  end
1095
1411
 
1096
1412
  # 第四步:替换 buildTypes 块中的内容
@@ -1110,9 +1426,9 @@ module Pindo
1110
1426
 
1111
1427
  return gradle_kts if File.exist?(gradle_kts)
1112
1428
  return gradle_groovy if File.exist?(gradle_groovy)
1429
+
1113
1430
  nil
1114
1431
  end
1115
-
1116
1432
  end
1117
1433
  end
1118
1434
  end