pindo 5.18.5 → 5.18.9
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/command/android/autobuild.rb +16 -3
- data/lib/pindo/command/appstore/adhocbuild.rb +1 -1
- data/lib/pindo/command/ios/autobuild.rb +1 -1
- data/lib/pindo/command/jps/upload.rb +1 -1
- data/lib/pindo/command/unity/autobuild.rb +2 -2
- data/lib/pindo/command/utils/gitconfig.rb +268 -0
- data/lib/pindo/command/utils/repoinit.rb +32 -15
- data/lib/pindo/command/utils/tag.rb +1 -1
- data/lib/pindo/command/utils.rb +1 -0
- data/lib/pindo/module/android/android_build_helper.rb +109 -69
- data/lib/pindo/module/android/android_config_helper.rb +1 -1
- data/lib/pindo/module/android/android_project_helper.rb +88 -0
- data/lib/pindo/module/android/gradle_helper.rb +27 -2
- data/lib/pindo/module/android/keystore_helper.rb +99 -30
- data/lib/pindo/module/android/workflow_gradle_injector.rb +82 -6
- data/lib/pindo/module/pgyer/pgyerhelper.rb +6 -14
- data/lib/pindo/module/task/model/build/android_build_dev_task.rb +1 -1
- data/lib/pindo/module/task/model/build/ios_build_adhoc_task.rb +3 -3
- data/lib/pindo/module/task/model/build/ios_build_appstore_task.rb +3 -4
- data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +1 -2
- data/lib/pindo/module/task/model/git/git_commit_task.rb +6 -6
- data/lib/pindo/module/task/model/git/git_tag_task.rb +1 -1
- data/lib/pindo/module/task/model/git_task.rb +1 -1
- data/lib/pindo/module/task/model/unity/unity_export_task.rb +14 -0
- data/lib/pindo/module/utils/git_hook_helper.rb +654 -0
- data/lib/pindo/module/{build → utils}/git_repo_helper.rb +1 -1
- data/lib/pindo/module/xcode/xcode_build_config.rb +15 -3
- data/lib/pindo/options/groups/build_options.rb +0 -33
- data/lib/pindo/options/groups/utils_options.rb +11 -0
- data/lib/pindo/version.rb +2 -2
- metadata +10 -8
|
@@ -73,16 +73,21 @@ module Pindo
|
|
|
73
73
|
unless build_so_library(unity_dir)
|
|
74
74
|
raise RuntimeError, "编译 Unity 模块 SO 库失败"
|
|
75
75
|
end
|
|
76
|
-
copy_so_files(unity_dir, project_dir)
|
|
77
|
-
validate_unity_libs_state!(project_dir, stage: "导出后")
|
|
78
|
-
|
|
79
|
-
# SO 库拷贝完成后,重新配置主工程的 Java Home 和 Gradle 环境
|
|
80
|
-
puts "Unity SO库编译完成,切换到主工程编译环境"
|
|
81
|
-
Pindo::JavaEnvHelper.setup_java_home_from_project(project_dir, use_main: true)
|
|
82
|
-
Pindo::GradleHelper.check_gradle_with_project(project_dir, configure_main: true)
|
|
83
76
|
else
|
|
84
77
|
puts "未检测到 IL2CPP 目录,可能使用 Mono 后端,跳过 SO 库构建"
|
|
85
78
|
end
|
|
79
|
+
|
|
80
|
+
# 无论 IL2CPP 或 Mono,均需要将 Unity 导出工程的关键产物同步回主工程:
|
|
81
|
+
# - unityLibrary/libs(Java/Kotlin 库)
|
|
82
|
+
# - unityLibrary/src/main/assets(StreamingAssets 等资源)
|
|
83
|
+
# - unityLibrary/src/main/jniLibs(若存在)
|
|
84
|
+
copy_so_files(unity_dir, project_dir)
|
|
85
|
+
validate_unity_libs_state!(project_dir, stage: "导出后")
|
|
86
|
+
|
|
87
|
+
# 同步完成后,重新配置主工程的 Java Home 和 Gradle 环境
|
|
88
|
+
puts "Unity 产物同步完成,切换到主工程编译环境"
|
|
89
|
+
Pindo::JavaEnvHelper.setup_java_home_from_project(project_dir, use_main: true)
|
|
90
|
+
Pindo::GradleHelper.check_gradle_with_project(project_dir, configure_main: true)
|
|
86
91
|
else
|
|
87
92
|
puts "警告:Unity 作为 lib 的工程,但未找到 Unity 目录"
|
|
88
93
|
end
|
|
@@ -101,33 +106,81 @@ module Pindo
|
|
|
101
106
|
build_type = debug ? "debug" : "release"
|
|
102
107
|
raise "bundle_name 不能为空,Keystore 签名需要 Application ID" if bundle_name.nil? || bundle_name.empty?
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if workflow_name && !workflow_name.to_s.empty?
|
|
110
|
-
injector = Pindo::Android::WorkflowGradleInjector.new
|
|
111
|
-
ctx = injector.prepare!(
|
|
112
|
-
project_dir: project_dir,
|
|
113
|
-
workflow_name: workflow_name,
|
|
114
|
-
enable_pad: true
|
|
115
|
-
)
|
|
116
|
-
apk_path = nil
|
|
117
|
-
begin
|
|
118
|
-
apk_path = build_apk(project_dir, debug, workflow_build_type: ctx[:workflow_build_type])
|
|
119
|
-
ensure
|
|
120
|
-
injector.cleanup!(ctx)
|
|
109
|
+
apk_path = nil
|
|
110
|
+
begin
|
|
111
|
+
unless Pindo::KeystoreHelper.apply_keystore_config(project_dir, build_type, bundle_id: bundle_name)
|
|
112
|
+
raise RuntimeError, "Keystore 签名配置失败"
|
|
121
113
|
end
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
114
|
+
puts "✓ Android 签名配置成功"
|
|
115
|
+
|
|
116
|
+
if workflow_name && !workflow_name.to_s.empty?
|
|
117
|
+
pad_persist = pad_persist_enabled?
|
|
118
|
+
|
|
119
|
+
# 永久 PAD:每次构建前清理旧的 *_pack,并从当前导出产物重新生成
|
|
120
|
+
# 注意:PAD 会移动 unityLibrary/src/main/assets 下的目录到 *_pack;
|
|
121
|
+
# 因此要保证在此之前已经把 Unity/ 导出产物同步回主工程(Unity as lib 分支已同步)。
|
|
122
|
+
setup_play_asset_delivery_persistent(project_dir) if pad_persist
|
|
123
|
+
|
|
124
|
+
injector = Pindo::Android::WorkflowGradleInjector.new
|
|
125
|
+
ctx = injector.prepare!(
|
|
126
|
+
project_dir: project_dir,
|
|
127
|
+
workflow_name: workflow_name,
|
|
128
|
+
enable_pad: !pad_persist
|
|
129
|
+
)
|
|
130
|
+
begin
|
|
131
|
+
apk_path = build_apk(project_dir, debug, workflow_build_type: ctx[:workflow_build_type])
|
|
132
|
+
ensure
|
|
133
|
+
injector.cleanup!(ctx)
|
|
134
|
+
Pindo::Android::WorkflowGradleInjector.remove_project_pindo_tmp_dir!(project_dir)
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
# 兼容旧行为:仍会永久写入 PAD 相关变更(如需临时恢复,请传 workflow_name 走注入管线)
|
|
138
|
+
setup_play_asset_delivery(project_dir)
|
|
139
|
+
apk_path = build_apk(project_dir, debug)
|
|
140
|
+
end
|
|
141
|
+
ensure
|
|
142
|
+
# 安全优先:无论成功/失败,均清理本次下载/拷贝到工程 signing/ 的签名文件,并恢复本次改动的 Gradle 签名配置与进程 ENV。
|
|
143
|
+
# 说明:这会降低“失败后保留签名文件便于排查”的便利性,但可以显著降低泄露风险。
|
|
144
|
+
Pindo::KeystoreHelper.cleanup_managed_signing_paths!
|
|
145
|
+
Pindo::KeystoreHelper.restore_managed_signing_config!
|
|
146
|
+
Pindo::KeystoreHelper.cleanup_managed_signing_env!
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private def pad_persist_enabled?
|
|
151
|
+
# 默认开启永久 PAD(避免 workflow 注入管线在 cleanup 时删除 *_pack)
|
|
152
|
+
# 如需回到“临时 PAD(构建结束自动回滚)”,可显式设置:
|
|
153
|
+
# export PINDO_PAD_PERSIST=0
|
|
154
|
+
val = ENV.fetch("PINDO_PAD_PERSIST", nil)
|
|
155
|
+
return true if val.nil?
|
|
156
|
+
|
|
157
|
+
v = val.to_s.strip.downcase
|
|
158
|
+
return true if v.empty?
|
|
159
|
+
return false if %w[0 false no off].include?(v)
|
|
160
|
+
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# 永久 PAD:先清理旧的 *_pack,再从当前 assets 生成新的 pack
|
|
165
|
+
private def setup_play_asset_delivery_persistent(project_dir)
|
|
166
|
+
# 仅 Unity 导出工程才有 unityLibrary/src/main/assets
|
|
167
|
+
unity_assets = File.join(project_dir, "unityLibrary", "src", "main", "assets")
|
|
168
|
+
return unless File.directory?(unity_assets)
|
|
169
|
+
|
|
170
|
+
# 清理旧 pack:避免 assets 内容变化后残留旧 pack 目录导致重复/脏数据
|
|
171
|
+
Dir.glob(File.join(project_dir, "*_pack")).each do |dir|
|
|
172
|
+
next unless File.directory?(dir)
|
|
173
|
+
|
|
174
|
+
# 尽量只删除我们生成的模块:要求 build.gradle 存在且包含 asset-pack 插件
|
|
175
|
+
gradle = File.join(dir, "build.gradle")
|
|
176
|
+
next unless File.file?(gradle)
|
|
177
|
+
content = File.read(gradle)
|
|
178
|
+
next unless content.include?("com.android.asset-pack")
|
|
179
|
+
|
|
180
|
+
FileUtils.rm_rf(dir)
|
|
130
181
|
end
|
|
182
|
+
|
|
183
|
+
setup_play_asset_delivery(project_dir)
|
|
131
184
|
end
|
|
132
185
|
|
|
133
186
|
def build_apk(project_path, debug, workflow_build_type: nil)
|
|
@@ -142,13 +195,6 @@ module Pindo
|
|
|
142
195
|
main_module = Pindo::AndroidProjectHelper.get_main_module(project_path)
|
|
143
196
|
raise ArgumentError, "无法找到主模块" unless main_module
|
|
144
197
|
|
|
145
|
-
keystore_config = Pindo::KeystoreHelper.get_keystore_config_from_project(
|
|
146
|
-
project_path,
|
|
147
|
-
debug,
|
|
148
|
-
workflow_build_type: workflow_build_type
|
|
149
|
-
)
|
|
150
|
-
raise ArgumentError, "无法从 build.gradle 中获取 keystore 信息" unless keystore_config
|
|
151
|
-
|
|
152
198
|
build_tools = Pindo::AndroidProjectHelper.get_build_tools
|
|
153
199
|
unless build_tools
|
|
154
200
|
Funlog.error("无法继续构建:缺少必要的构建工具")
|
|
@@ -209,29 +255,27 @@ module Pindo
|
|
|
209
255
|
end
|
|
210
256
|
# --- END ---
|
|
211
257
|
|
|
212
|
-
# puts "解析 keystore 配置"
|
|
213
|
-
# 与 Gradle 对 storeFile 的生效顺序一致:常见写法为
|
|
214
|
-
# storeFile file(System.getenv("RELEASE_KEYSTORE_PATH") ?: "signing/gxdebug.jks")
|
|
215
|
-
# apply_keystore_config 已在当前进程设置 RELEASE_*,Gradle 打 AAB 时走 ENV 指向的真实 jks;
|
|
216
|
-
# get_keystore_config_from_project 若只解析到 ?: 右侧占位路径,会与 AAB 实际签名不一致且文件可能不存在。
|
|
217
|
-
# 故:当 RELEASE_KEYSTORE_PATH 展开后文件存在时,bundletool 必须使用同一套 RELEASE_*(非「失败回退」)。
|
|
218
258
|
kh = Pindo::KeystoreHelper
|
|
219
259
|
env_ks = ENV[kh::ENV_RELEASE_KEYSTORE_PATH]
|
|
220
260
|
env_ks_abs = env_ks && !env_ks.to_s.empty? ? File.expand_path(env_ks) : nil
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
# 签名信息只允许从环境变量读取,禁止从 build.gradle 解析值作为回退来源。
|
|
262
|
+
# 这样可以避免「环境变量缺失时悄悄退回到 Gradle 文本里的占位/历史明文」导致签名不一致。
|
|
263
|
+
ks = env_ks_abs
|
|
264
|
+
ks_pass = ENV.fetch(kh::ENV_RELEASE_KEYSTORE_PASSWORD, nil)
|
|
265
|
+
key_alias = ENV.fetch(kh::ENV_RELEASE_KEY_ALIAS, nil)
|
|
266
|
+
key_pass = ENV.fetch(kh::ENV_RELEASE_KEY_PASSWORD, nil)
|
|
267
|
+
|
|
268
|
+
missing_env = []
|
|
269
|
+
missing_env << kh::ENV_RELEASE_KEYSTORE_PATH if env_ks_abs.nil? || env_ks_abs.to_s.empty?
|
|
270
|
+
missing_env << kh::ENV_RELEASE_KEYSTORE_PASSWORD if ks_pass.to_s.empty?
|
|
271
|
+
missing_env << kh::ENV_RELEASE_KEY_ALIAS if key_alias.to_s.empty?
|
|
272
|
+
missing_env << kh::ENV_RELEASE_KEY_PASSWORD if key_pass.to_s.empty?
|
|
273
|
+
unless missing_env.empty?
|
|
274
|
+
raise RuntimeError, "缺少 Android release 签名所需环境变量:#{missing_env.join(', ')}"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
unless File.exist?(ks)
|
|
278
|
+
raise RuntimeError, "Keystore 文件不存在: #{ks}"
|
|
235
279
|
end
|
|
236
280
|
|
|
237
281
|
ks_display = ks && !ks.to_s.empty? ? File.expand_path(ks.to_s) : ks.to_s
|
|
@@ -239,7 +283,7 @@ module Pindo
|
|
|
239
283
|
# if workflow_build_type && !workflow_build_type.to_s.empty?
|
|
240
284
|
# puts "[bundletool] workflow buildType: #{workflow_build_type}"
|
|
241
285
|
# end
|
|
242
|
-
# puts "[bundletool] 签名来源:
|
|
286
|
+
# puts "[bundletool] 签名来源: RELEASE_* 环境变量(强制)"
|
|
243
287
|
# puts "[bundletool] keyAlias: #{key_alias.inspect}(口令不落日志)"
|
|
244
288
|
|
|
245
289
|
# 构建 APK
|
|
@@ -270,7 +314,7 @@ module Pindo
|
|
|
270
314
|
end
|
|
271
315
|
|
|
272
316
|
if ks.nil? || ks.to_s.empty?
|
|
273
|
-
raise RuntimeError, "Keystore
|
|
317
|
+
raise RuntimeError, "Keystore 路径为空:未从环境变量获取到 #{Pindo::KeystoreHelper::ENV_RELEASE_KEYSTORE_PATH}"
|
|
274
318
|
end
|
|
275
319
|
|
|
276
320
|
unless File.exist?(ks)
|
|
@@ -314,12 +358,9 @@ module Pindo
|
|
|
314
358
|
end
|
|
315
359
|
|
|
316
360
|
bundle_task = "#{prefix}#{task_name}"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
puts ' 已跳过 Gradle clean(--skipclean)' if skip_clean
|
|
321
|
-
|
|
322
|
-
gradle_clean_resilient!(project_path) unless skip_clean
|
|
361
|
+
# 任意 bundle 变体(含 debug)均先 clean,避免 aabresguard 等任务因残留输出报「文件已存在」
|
|
362
|
+
puts ' AAB 构建前先执行 Gradle clean'
|
|
363
|
+
gradle_clean_resilient!(project_path)
|
|
323
364
|
|
|
324
365
|
cmd = "cd \"#{project_path}\" && ./gradlew --no-daemon #{bundle_task}"
|
|
325
366
|
system(cmd)
|
|
@@ -440,8 +481,7 @@ module Pindo
|
|
|
440
481
|
def validate_unity_libs_state!(android_root, stage:)
|
|
441
482
|
# 某些项目会将 unityLibrary 作为缓存/中间产物纳入仓库,但 libs 下的 .aar -> .srcaar
|
|
442
483
|
# 符号链接目标是由 EDM4U/导出过程生成,初次拉取代码时可能为断链。
|
|
443
|
-
#
|
|
444
|
-
return if Pindo::Options::GlobalOptionsState.instance[:skipvalidate]
|
|
484
|
+
# 这里必须严格失败,避免后续构建阶段才出现不可读/缺失依赖导致更难定位的问题
|
|
445
485
|
|
|
446
486
|
libs_dir = File.join(android_root, "unityLibrary", "libs")
|
|
447
487
|
return unless File.directory?(libs_dir)
|
|
@@ -6,7 +6,7 @@ require_relative 'android_project_helper'
|
|
|
6
6
|
require_relative '../../base/executable'
|
|
7
7
|
require_relative 'android_res_helper'
|
|
8
8
|
require 'pindo/module/utils/file_downloader'
|
|
9
|
-
require 'pindo/module/
|
|
9
|
+
require 'pindo/module/utils/git_repo_helper'
|
|
10
10
|
|
|
11
11
|
module Pindo
|
|
12
12
|
|
|
@@ -1,8 +1,96 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "pathname"
|
|
3
|
+
|
|
1
4
|
module Pindo
|
|
2
5
|
class AndroidProjectHelper
|
|
3
6
|
class << self
|
|
4
7
|
|
|
5
8
|
|
|
9
|
+
# 确保导出的 Android 工程 `unityLibrary/libs` 里物理存在 Firebase Unity AAR(firebase-*-unity-*.aar)。
|
|
10
|
+
#
|
|
11
|
+
# 某些 Unity 工程会通过 EDM4U 将 Firebase Unity AAR 解析到 `Assets/GeneratedLocalRepo/**/m2repository`,
|
|
12
|
+
# 并在导出工程中使用本地 maven repo 方式引用它们,导致 `unityLibrary/libs` 为空。
|
|
13
|
+
# 当下游流程只携带“导出目录”时,这会表现为 Firebase AAR “缺失”。
|
|
14
|
+
#
|
|
15
|
+
# 本方法会从以下来源收集 firebase-*-unity-*.aar 并复制到导出工程:
|
|
16
|
+
# - Unity 工程的 `ProjectSettings/AndroidResolverDependencies.xml`(优先)
|
|
17
|
+
# - `Assets/GeneratedLocalRepo/**/m2repository/com/google/firebase/**/firebase-*-unity-*.aar`(兜底扫描)
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<String>] 实际拷贝/确认存在的文件名列表
|
|
20
|
+
def ensure_export_has_firebase_unity_aars!(unity_root_path:, export_path:)
|
|
21
|
+
raise ArgumentError, "unity_root_path 不能为空" if unity_root_path.to_s.empty?
|
|
22
|
+
raise ArgumentError, "export_path 不能为空" if export_path.to_s.empty?
|
|
23
|
+
raise ArgumentError, "Unity 工程目录不存在: #{unity_root_path}" unless File.directory?(unity_root_path)
|
|
24
|
+
raise ArgumentError, "导出目录不存在: #{export_path}" unless File.directory?(export_path)
|
|
25
|
+
|
|
26
|
+
libs_dir = File.join(export_path, "unityLibrary", "libs")
|
|
27
|
+
FileUtils.mkdir_p(libs_dir)
|
|
28
|
+
|
|
29
|
+
# 若导出工程已包含所需的 firebase-*-unity-*.aar,则无需再从 Unity 工程侧反查来源(含 GeneratedLocalRepo)。
|
|
30
|
+
already = Dir.glob(File.join(libs_dir, "firebase-*-unity-*.aar")).select { |p| File.file?(p) }
|
|
31
|
+
return already.map { |p| File.basename(p) }.uniq unless already.empty?
|
|
32
|
+
|
|
33
|
+
resolver_xml = File.join(unity_root_path, "ProjectSettings", "AndroidResolverDependencies.xml")
|
|
34
|
+
|
|
35
|
+
candidates = []
|
|
36
|
+
if File.file?(resolver_xml)
|
|
37
|
+
begin
|
|
38
|
+
require "rexml/document"
|
|
39
|
+
doc = REXML::Document.new(File.read(resolver_xml))
|
|
40
|
+
doc.elements.each("dependencies/files/file") do |e|
|
|
41
|
+
rel = e.text.to_s.strip
|
|
42
|
+
next if rel.empty?
|
|
43
|
+
next unless rel.end_with?(".aar")
|
|
44
|
+
next unless rel.match?(/firebase-.*-unity-.*\.aar\z/i)
|
|
45
|
+
|
|
46
|
+
abs = Pathname.new(rel).absolute? ? rel : File.join(unity_root_path, rel)
|
|
47
|
+
candidates << abs
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
raise Informative, "解析 AndroidResolverDependencies.xml 失败: #{e.message}\n请在 Unity 中执行 EDM4U Force Resolve 后重试。"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if candidates.empty?
|
|
55
|
+
local_repo = File.join(unity_root_path, "Assets", "GeneratedLocalRepo")
|
|
56
|
+
if File.directory?(local_repo)
|
|
57
|
+
glob = File.join(local_repo, "**", "m2repository", "com", "google", "firebase", "**", "firebase-*-unity-*.aar")
|
|
58
|
+
candidates.concat(Dir.glob(glob))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
candidates.uniq!
|
|
63
|
+
existing = candidates.select { |p| File.file?(p) }
|
|
64
|
+
|
|
65
|
+
if existing.empty?
|
|
66
|
+
raise Informative, <<~MSG
|
|
67
|
+
Firebase Unity AAR 依赖拷贝失败:未找到 firebase-*-unity-*.aar
|
|
68
|
+
Unity 工程: #{unity_root_path}
|
|
69
|
+
导出目录: #{export_path}
|
|
70
|
+
期望来源:
|
|
71
|
+
- #{resolver_xml}
|
|
72
|
+
- Assets/GeneratedLocalRepo/**/m2repository/com/google/firebase/**/firebase-*-unity-*.aar
|
|
73
|
+
|
|
74
|
+
请在 Unity 中执行:Assets → External Dependency Manager → Android Resolver → Force Resolve
|
|
75
|
+
然后重新导出再构建。
|
|
76
|
+
MSG
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
copied = []
|
|
80
|
+
existing.each do |src|
|
|
81
|
+
dst = File.join(libs_dir, File.basename(src))
|
|
82
|
+
if File.file?(dst) && File.size(dst) == File.size(src)
|
|
83
|
+
copied << File.basename(dst)
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
FileUtils.cp(src, dst)
|
|
88
|
+
copied << File.basename(dst)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
copied.uniq
|
|
92
|
+
end
|
|
93
|
+
|
|
6
94
|
def unity_android_project?(project_path)
|
|
7
95
|
# 检查 unityLibrary 模块是否存在
|
|
8
96
|
unity_library_path = File.join(project_path, "unityLibrary")
|
|
@@ -205,7 +205,9 @@ module Pindo
|
|
|
205
205
|
end
|
|
206
206
|
|
|
207
207
|
# 生成 Wrapper
|
|
208
|
-
generate_gradle_wrapper(gradle_dir, gradle_version)
|
|
208
|
+
ok = generate_gradle_wrapper(gradle_dir, gradle_version)
|
|
209
|
+
ensure_gradlew_runnable!(gradle_dir)
|
|
210
|
+
ok
|
|
209
211
|
end
|
|
210
212
|
|
|
211
213
|
# 解析gradle版本
|
|
@@ -244,7 +246,30 @@ module Pindo
|
|
|
244
246
|
end
|
|
245
247
|
|
|
246
248
|
# 生成 Wrapper
|
|
247
|
-
generate_gradle_wrapper(gradle_dir, gradle_version)
|
|
249
|
+
ok = generate_gradle_wrapper(gradle_dir, gradle_version)
|
|
250
|
+
ensure_gradlew_runnable!(gradle_dir)
|
|
251
|
+
ok
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# 确保工程根目录下 gradlew 可被当前环境执行:补齐执行位;在 macOS 上尝试移除
|
|
255
|
+
# com.apple.quarantine(无该属性时 xattr 失败可忽略)。用于避免
|
|
256
|
+
# 「bad interpreter: Operation not permitted」类错误,无用户交互。
|
|
257
|
+
def ensure_gradlew_runnable!(gradle_dir)
|
|
258
|
+
return if gradle_dir.nil? || gradle_dir.to_s.empty?
|
|
259
|
+
|
|
260
|
+
gradlew = File.join(gradle_dir, "gradlew")
|
|
261
|
+
return unless File.file?(gradlew)
|
|
262
|
+
|
|
263
|
+
begin
|
|
264
|
+
st = File.stat(gradlew)
|
|
265
|
+
File.chmod(st.mode | 0o111, gradlew)
|
|
266
|
+
rescue StandardError
|
|
267
|
+
# 只读卷等场景下忽略
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
return unless RUBY_PLATFORM.match?(/darwin/i)
|
|
271
|
+
|
|
272
|
+
system("xattr", "-d", "com.apple.quarantine", gradlew, out: File::NULL, err: File::NULL)
|
|
248
273
|
end
|
|
249
274
|
|
|
250
275
|
# =================== 私有辅助方法 ===================
|
|
@@ -122,14 +122,34 @@ module Pindo
|
|
|
122
122
|
@pindo_managed_signing_paths = []
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# 打包结束后恢复本次被修改的 Gradle 签名配置(仅恢复被本次任务改动过的文件)
|
|
126
|
+
def restore_managed_signing_config!
|
|
127
|
+
backups = @pindo_managed_gradle_backups || {}
|
|
128
|
+
backups.each do |path, original_content|
|
|
129
|
+
next if path.nil? || path.to_s.empty?
|
|
130
|
+
next unless original_content.is_a?(String)
|
|
131
|
+
next unless File.exist?(path)
|
|
132
|
+
|
|
133
|
+
File.write(path, original_content)
|
|
134
|
+
end
|
|
135
|
+
ensure
|
|
136
|
+
@pindo_managed_gradle_backups = {}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# 清理本次写入到进程 ENV 的签名变量(避免后续任务误继承)
|
|
140
|
+
def cleanup_managed_signing_env!
|
|
141
|
+
keys = @pindo_managed_env_keys || []
|
|
142
|
+
keys.each { |k| ENV.delete(k.to_s) if k }
|
|
143
|
+
ensure
|
|
144
|
+
@pindo_managed_env_keys = []
|
|
145
|
+
end
|
|
146
|
+
|
|
125
147
|
# 将签名配置应用到 Android 工程
|
|
126
148
|
#
|
|
127
149
|
# 默认(与工程手写的 RELEASE_* 约定一致):只拉取 JPS、拷贝 jks 到项目 signing/,并设置当前进程
|
|
128
150
|
# RELEASE_KEYSTORE_PATH / RELEASE_KEYSTORE_PASSWORD / RELEASE_KEY_ALIAS / RELEASE_KEY_PASSWORD,不修改 build.gradle。
|
|
129
151
|
# RELEASE_KEYSTORE_PATH 使用 jks 的绝对路径,便于 app 子模块内 `file(System.getenv(...))` 引用。
|
|
130
152
|
#
|
|
131
|
-
# 若需恢复自动改写 signingConfigs,设置环境变量:PINDO_INJECT_ANDROID_SIGNING_GRADLE=1
|
|
132
|
-
#
|
|
133
153
|
# @param project_dir [String] 项目目录
|
|
134
154
|
# @param build_type [String] 构建类型 "debug" 或 "release"
|
|
135
155
|
# @return [Boolean] 是否成功
|
|
@@ -139,6 +159,8 @@ module Pindo
|
|
|
139
159
|
raise ArgumentError, "bundle_id 不能为空" if bundle_id.blank?
|
|
140
160
|
|
|
141
161
|
reset_managed_signing_paths!
|
|
162
|
+
reset_managed_gradle_backups!
|
|
163
|
+
reset_managed_env_keys!
|
|
142
164
|
|
|
143
165
|
main_module = Pindo::AndroidProjectHelper.get_main_module(project_dir)
|
|
144
166
|
unless main_module
|
|
@@ -170,19 +192,16 @@ module Pindo
|
|
|
170
192
|
raise "JPS keystore 未拷贝到工程 signing/(无法解析路径),Gradle 将回退到 build.gradle 中的本地 jks;请检查 JPS 证书是否下载成功"
|
|
171
193
|
end
|
|
172
194
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
inject_gradle = Pindo::Options::GlobalOptionsState.instance[:injectsigning]
|
|
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)
|
|
195
|
+
if gradle_file.end_with?(".kts")
|
|
196
|
+
ensure_keystore_config_kts(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
|
|
183
197
|
else
|
|
184
|
-
|
|
198
|
+
ensure_keystore_config_groovy(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
|
|
185
199
|
end
|
|
200
|
+
# 注意:部分 Unity 导出工程/多模块工程的 Gradle rootProject 目录可能不是 project_dir,
|
|
201
|
+
# 相对路径(如 signing/xxx.jks)会被解析到错误的模块目录下(如 launcher/signing/...)。
|
|
202
|
+
# 这里改为导出绝对路径,确保与 keystore 创建/拷贝目录保持一致。
|
|
203
|
+
keystore_abs = File.expand_path(File.join(project_dir, rel_plain))
|
|
204
|
+
export_jps_release_signing_env!(cfg, keystore_path_for_env: keystore_abs)
|
|
186
205
|
true
|
|
187
206
|
end
|
|
188
207
|
|
|
@@ -244,6 +263,14 @@ module Pindo
|
|
|
244
263
|
@pindo_managed_signing_paths = []
|
|
245
264
|
end
|
|
246
265
|
|
|
266
|
+
def reset_managed_gradle_backups!
|
|
267
|
+
@pindo_managed_gradle_backups = {}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def reset_managed_env_keys!
|
|
271
|
+
@pindo_managed_env_keys = []
|
|
272
|
+
end
|
|
273
|
+
|
|
247
274
|
def register_managed_signing_path!(path)
|
|
248
275
|
return if path.nil? || path.to_s.empty?
|
|
249
276
|
|
|
@@ -251,6 +278,23 @@ module Pindo
|
|
|
251
278
|
@pindo_managed_signing_paths << path
|
|
252
279
|
end
|
|
253
280
|
|
|
281
|
+
def register_managed_gradle_backup!(path, original_content)
|
|
282
|
+
return if path.nil? || path.to_s.empty?
|
|
283
|
+
return unless original_content.is_a?(String)
|
|
284
|
+
|
|
285
|
+
@pindo_managed_gradle_backups ||= {}
|
|
286
|
+
# 同一文件只备份一次:第一次写入前的内容即为“原始内容”
|
|
287
|
+
@pindo_managed_gradle_backups[path] ||= original_content
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def register_managed_env_key!(key)
|
|
291
|
+
return if key.nil? || key.to_s.empty?
|
|
292
|
+
|
|
293
|
+
@pindo_managed_env_keys ||= []
|
|
294
|
+
@pindo_managed_env_keys << key.to_s
|
|
295
|
+
@pindo_managed_env_keys.uniq!
|
|
296
|
+
end
|
|
297
|
+
|
|
254
298
|
# 将 JPS 返回的路径/口令/别名写入当前进程环境变量,供 Gradle / bundletool 子进程继承(与 build.gradle 中 RELEASE_* 名称一致)
|
|
255
299
|
def export_jps_release_signing_env!(cfg, keystore_path_for_env:)
|
|
256
300
|
return unless cfg.is_a?(Hash)
|
|
@@ -258,6 +302,7 @@ module Pindo
|
|
|
258
302
|
if keystore_path_for_env && !keystore_path_for_env.to_s.empty?
|
|
259
303
|
ENV[ENV_RELEASE_KEYSTORE_PATH] =
|
|
260
304
|
keystore_path_for_env
|
|
305
|
+
register_managed_env_key!(ENV_RELEASE_KEYSTORE_PATH)
|
|
261
306
|
end
|
|
262
307
|
|
|
263
308
|
sp = cfg["storePassword"]
|
|
@@ -270,6 +315,10 @@ module Pindo
|
|
|
270
315
|
ENV[ENV_RELEASE_KEYSTORE_PASSWORD] = sp_plain.to_s if sp_plain && !sp_plain.to_s.empty?
|
|
271
316
|
ENV[ENV_RELEASE_KEY_PASSWORD] = kp_plain.to_s if kp_plain && !kp_plain.to_s.empty?
|
|
272
317
|
ENV[ENV_RELEASE_KEY_ALIAS] = ka.to_s if ka && !ka.to_s.empty?
|
|
318
|
+
|
|
319
|
+
register_managed_env_key!(ENV_RELEASE_KEYSTORE_PASSWORD) if sp_plain && !sp_plain.to_s.empty?
|
|
320
|
+
register_managed_env_key!(ENV_RELEASE_KEY_PASSWORD) if kp_plain && !kp_plain.to_s.empty?
|
|
321
|
+
register_managed_env_key!(ENV_RELEASE_KEY_ALIAS) if ka && !ka.to_s.empty?
|
|
273
322
|
end
|
|
274
323
|
|
|
275
324
|
# Groovy:解析 storePassword/keyPassword(支持 System.getenv、可选 ?: 回退、或历史明文)
|
|
@@ -517,6 +566,7 @@ module Pindo
|
|
|
517
566
|
if content == original_content
|
|
518
567
|
puts " ✓ build.gradle 无需修改"
|
|
519
568
|
else
|
|
569
|
+
register_managed_gradle_backup!(gradle_file, original_content)
|
|
520
570
|
File.write(gradle_file, content)
|
|
521
571
|
puts " ✓ build.gradle 已更新"
|
|
522
572
|
end
|
|
@@ -571,6 +621,7 @@ module Pindo
|
|
|
571
621
|
if content == original_content
|
|
572
622
|
puts " ✓ build.gradle.kts 无需修改"
|
|
573
623
|
else
|
|
624
|
+
register_managed_gradle_backup!(gradle_file, original_content)
|
|
574
625
|
File.write(gradle_file, content)
|
|
575
626
|
puts " ✓ build.gradle.kts 已更新"
|
|
576
627
|
end
|
|
@@ -1285,34 +1336,52 @@ module Pindo
|
|
|
1285
1336
|
|
|
1286
1337
|
# =================== 写入 keystore 配置的辅助方法 ===================
|
|
1287
1338
|
|
|
1288
|
-
# 生成签名配置代码块(Groovy
|
|
1339
|
+
# 生成签名配置代码块(Groovy)
|
|
1340
|
+
# 注意:签名信息只允许从环境变量读取,严禁在 build.gradle 中写入任何明文(包括回退密码/alias/路径)。
|
|
1289
1341
|
def generate_signing_config_groovy(config_name, pindo_config)
|
|
1290
|
-
rel_fb = escape_gradle_double_quoted(signing_keystore_fallback_relative(pindo_config))
|
|
1291
|
-
alias_fb = escape_gradle_double_quoted(pindo_config["keyAlias"])
|
|
1292
|
-
|
|
1293
1342
|
<<~GROOVY
|
|
1294
1343
|
#{config_name} {
|
|
1295
|
-
def
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1344
|
+
def signingEnvVars = ["#{ENV_RELEASE_KEYSTORE_PATH}", "#{ENV_RELEASE_KEYSTORE_PASSWORD}", "#{ENV_RELEASE_KEY_ALIAS}", "#{ENV_RELEASE_KEY_PASSWORD}"]
|
|
1345
|
+
def missing = signingEnvVars.findAll { !System.getenv(it) }
|
|
1346
|
+
if (!missing.isEmpty()) {
|
|
1347
|
+
throw new GradleException("Missing required environment variables for release signing: ${missing.join(', ')}")
|
|
1348
|
+
}
|
|
1349
|
+
def keystorePath = System.getenv("#{ENV_RELEASE_KEYSTORE_PATH}")
|
|
1350
|
+
def keystoreFile = rootProject.file(keystorePath)
|
|
1351
|
+
if (!keystoreFile.exists()) {
|
|
1352
|
+
throw new GradleException("Keystore file not found: ${keystoreFile.absolutePath}")
|
|
1353
|
+
}
|
|
1354
|
+
storeFile keystoreFile
|
|
1355
|
+
storePassword System.getenv("#{ENV_RELEASE_KEYSTORE_PASSWORD}")
|
|
1356
|
+
keyAlias System.getenv("#{ENV_RELEASE_KEY_ALIAS}")
|
|
1357
|
+
keyPassword System.getenv("#{ENV_RELEASE_KEY_PASSWORD}")
|
|
1300
1358
|
}
|
|
1301
1359
|
GROOVY
|
|
1302
1360
|
end
|
|
1303
1361
|
|
|
1304
1362
|
# 生成签名配置代码块(Kotlin DSL)
|
|
1305
1363
|
def generate_signing_config_kts(config_name, pindo_config)
|
|
1306
|
-
rel_fb = escape_gradle_double_quoted(signing_keystore_fallback_relative(pindo_config))
|
|
1307
|
-
alias_fb = escape_gradle_double_quoted(pindo_config["keyAlias"])
|
|
1308
|
-
|
|
1309
1364
|
<<~KOTLIN
|
|
1310
1365
|
create("#{config_name}") {
|
|
1311
|
-
val
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1366
|
+
val signingEnvVars = listOf(
|
|
1367
|
+
"#{ENV_RELEASE_KEYSTORE_PATH}",
|
|
1368
|
+
"#{ENV_RELEASE_KEYSTORE_PASSWORD}",
|
|
1369
|
+
"#{ENV_RELEASE_KEY_ALIAS}",
|
|
1370
|
+
"#{ENV_RELEASE_KEY_PASSWORD}",
|
|
1371
|
+
)
|
|
1372
|
+
val missing = signingEnvVars.filter { System.getenv(it).isNullOrBlank() }
|
|
1373
|
+
if (missing.isNotEmpty()) {
|
|
1374
|
+
throw GradleException("Missing required environment variables for release signing: ${missing.joinToString(\", \")}")
|
|
1375
|
+
}
|
|
1376
|
+
val keystorePath = System.getenv("#{ENV_RELEASE_KEYSTORE_PATH}")
|
|
1377
|
+
val keystoreFile = rootProject.file(keystorePath)
|
|
1378
|
+
if (!keystoreFile.exists()) {
|
|
1379
|
+
throw GradleException("Keystore file not found: ${keystoreFile.absolutePath}")
|
|
1380
|
+
}
|
|
1381
|
+
storeFile = keystoreFile
|
|
1382
|
+
storePassword = System.getenv("#{ENV_RELEASE_KEYSTORE_PASSWORD}")
|
|
1383
|
+
keyAlias = System.getenv("#{ENV_RELEASE_KEY_ALIAS}")
|
|
1384
|
+
keyPassword = System.getenv("#{ENV_RELEASE_KEY_PASSWORD}")
|
|
1316
1385
|
}
|
|
1317
1386
|
KOTLIN
|
|
1318
1387
|
end
|