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.
- checksums.yaml +4 -4
- data/lib/pindo/base/git_handler.rb +120 -38
- data/lib/pindo/command/android/autobuild.rb +92 -31
- data/lib/pindo/command/appstore/adhocbuild.rb +1 -1
- data/lib/pindo/command/appstore/autobuild.rb +1 -1
- data/lib/pindo/command/appstore/autoresign.rb +1 -1
- data/lib/pindo/command/appstore/updateid.rb +229 -0
- data/lib/pindo/command/appstore.rb +1 -0
- data/lib/pindo/command/ios/autobuild.rb +70 -33
- data/lib/pindo/command/ios/podpush.rb +1 -1
- data/lib/pindo/command/unity/autobuild.rb +38 -18
- data/lib/pindo/command/utils/allcopyconfig.rb +144 -0
- data/lib/pindo/command/utils/copyconfig.rb +207 -0
- data/lib/pindo/command/utils/icon.rb +2 -2
- data/lib/pindo/command/utils/renewbundleid.rb +199 -0
- data/lib/pindo/command/utils/renewcert.rb +56 -54
- data/lib/pindo/command/utils.rb +3 -0
- data/lib/pindo/command/web/autobuild.rb +10 -8
- data/lib/pindo/config/build_info_manager.rb +1 -3
- data/lib/pindo/module/android/android_build_helper.rb +198 -33
- data/lib/pindo/module/android/android_config_helper.rb +305 -88
- data/lib/pindo/module/android/android_project_helper.rb +124 -14
- data/lib/pindo/module/android/android_res_helper.rb +349 -51
- data/lib/pindo/module/android/keystore_helper.rb +611 -295
- data/lib/pindo/module/android/workflow_gradle_injector.rb +702 -0
- data/lib/pindo/module/appselect.rb +4 -4
- data/lib/pindo/module/appstore/bundleid_helper.rb +204 -14
- data/lib/pindo/module/build/build_helper.rb +76 -10
- data/lib/pindo/module/build/git_repo_helper.rb +4 -4
- data/lib/pindo/module/cert/mode/base_cert_operator.rb +12 -6
- data/lib/pindo/module/pgyer/pgyerhelper.rb +124 -39
- data/lib/pindo/module/task/model/build/android_build_dev_task.rb +64 -6
- data/lib/pindo/module/task/model/git/git_commit_task.rb +70 -54
- data/lib/pindo/module/task/model/git/git_tag_task.rb +13 -9
- data/lib/pindo/module/task/model/jps/jps_upload_task.rb +110 -3
- data/lib/pindo/module/task/model/unity/unity_export_task.rb +2 -1
- data/lib/pindo/module/task/model/unity/unity_update_task.rb +2 -1
- data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +2 -1
- data/lib/pindo/module/task/model/unity_task.rb +2 -1
- data/lib/pindo/module/unity/unity_helper.rb +13 -10
- data/lib/pindo/module/unity/unity_proc_helper.rb +27 -2
- data/lib/pindo/module/xcode/applovin_xcode_helper.rb +6 -2
- data/lib/pindo/module/xcode/res/xcode_res_constant.rb +72 -0
- data/lib/pindo/module/xcode/res/xcode_res_handler.rb +3 -3
- data/lib/pindo/module/xcode/xcode_build_config.rb +46 -17
- data/lib/pindo/module/xcode/xcode_build_helper.rb +186 -25
- data/lib/pindo/module/xcode/xcode_project_helper.rb +1 -1
- data/lib/pindo/module/xcode/xcode_res_helper.rb +32 -16
- data/lib/pindo/options/groups/build_options.rb +5 -5
- data/lib/pindo/options/groups/git_options.rb +7 -5
- data/lib/pindo/options/groups/unity_options.rb +11 -0
- data/lib/pindo/options/helpers/bundleid_selector.rb +25 -0
- data/lib/pindo/options/helpers/git_constants.rb +7 -6
- data/lib/pindo/version.rb +3 -3
- metadata +12 -7
|
@@ -1,41 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 = [
|
|
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
|
-
# @
|
|
104
|
-
|
|
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
|
|
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
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
if
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
417
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
730
|
+
config_block =~ %r{storeFile\s+file\(["']signing/}
|
|
460
731
|
|
|
461
732
|
# 检查是否使用了绝对路径(不推荐)
|
|
462
|
-
uses_absolute_path = config_block =~
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
772
|
+
config_block =~ %r{storeFile\s*=\s*file\(["']signing/}
|
|
504
773
|
|
|
505
774
|
# 检查是否使用了绝对路径(不推荐)
|
|
506
|
-
uses_absolute_path = config_block =~
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
708
|
-
|
|
709
|
-
config_block =
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
key_alias = config_block
|
|
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:
|
|
1007
|
+
store_file: store_file,
|
|
733
1008
|
store_password: store_password,
|
|
734
|
-
key_alias:
|
|
735
|
-
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
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
|
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
|
|
1059
|
+
return
|
|
772
1060
|
end
|
|
773
1061
|
|
|
774
1062
|
config_block = remove_groovy_comments(config_block)
|
|
775
1063
|
|
|
776
|
-
#
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
key_alias = config_block
|
|
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:
|
|
1074
|
+
store_file: store_file,
|
|
788
1075
|
store_password: store_password,
|
|
789
|
-
key_alias:
|
|
790
|
-
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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(
|
|
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
|
|
1233
|
+
return if store_file.blank?
|
|
920
1234
|
|
|
921
1235
|
# 如果已经是绝对路径,直接返回
|
|
922
|
-
return store_file if store_file.start_with?(
|
|
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
|
-
|
|
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(
|
|
937
|
-
store_file
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#{
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
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
|
-
#{
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
"#{
|
|
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(
|
|
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
|
-
"#{
|
|
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(
|
|
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
|
-
"#{
|
|
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
|
-
"#{
|
|
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
|