pindo 5.18.6 → 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.
@@ -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
- abs_keystore = File.expand_path(File.join(project_dir, rel_plain))
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
- export_jps_release_signing_env!(cfg, keystore_path_for_env: abs_keystore)
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)——与工程约定:RELEASE_* + ?: 本地回退(明文仅回退串,来自 JPS 的敏感值走本次进程 ENV
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 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"
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 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"
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
@@ -346,13 +346,78 @@ module Pindo
346
346
  content = File.read(settings_path)
347
347
  includes = []
348
348
 
349
- content.scan(/include\s+(.*)$/).each do |m|
350
- line = m.first.to_s
351
- line.scan(/["'](:[^"']+)["']/).each do |mm|
352
- includes << mm.first
349
+ normalize_mod = lambda do |raw_mod|
350
+ m = raw_mod.to_s.strip
351
+ return nil if m.empty?
352
+ return nil unless m.start_with?(":") || m.include?(":")
353
+
354
+ m.start_with?(":") ? m : ":#{m}"
355
+ end
356
+
357
+ extract_from_args = lambda do |args|
358
+ args.to_s.scan(/["']([^"']+)["']/).each do |mm|
359
+ mod = normalize_mod.call(mm.first)
360
+ includes << mod if mod
353
361
  end
354
362
  end
355
363
 
364
+ # 兼容 settings.gradle / settings.gradle.kts 的多种 include 写法:
365
+ # - Groovy: include ':launcher', ':unityLibrary'
366
+ # - Groovy: include(':launcher', ':unityLibrary')
367
+ # - KTS: include(":launcher", ":unityLibrary")
368
+ # - 以及跨行/缩进的场景(include(...) 可能换行)
369
+ #
370
+ # 注意:KTS 常见多行写法:
371
+ # include(
372
+ # ":launcher",
373
+ # ":unityLibrary"
374
+ # )
375
+ #
376
+ # 这里先全局提取 include(...) 块,再逐行兜底提取 include 语句。
377
+ content.scan(/\binclude\s*\((.*?)\)/m).each do |m|
378
+ extract_from_args.call(m.first.to_s)
379
+ end
380
+
381
+ # 逐行兜底:适配
382
+ # - include ':a', ':b'
383
+ # - include(':a', ':b')
384
+ # - include ':a',
385
+ # ':b'
386
+ # - include ":a",
387
+ # ":b"
388
+ lines = content.lines
389
+ i = 0
390
+ while i < lines.length
391
+ raw = lines[i].to_s
392
+ line = raw.strip
393
+ i += 1
394
+
395
+ next if line.empty?
396
+ next if line.start_with?("//", "#")
397
+ next unless line.match?(/\binclude\b/)
398
+
399
+ # 捕获 include 后的参数区:既支持 `include xxx` 也支持 `include(xxx)`
400
+ args = line.sub(/\A.*?\binclude\b\s*/, "")
401
+ args = args.sub(/\A\(\s*/, "")
402
+
403
+ # 如果这一行以逗号结尾或括号未闭合,继续吞掉后续的“续行参数”
404
+ open_parens = args.count("(") - args.count(")")
405
+ needs_more = args.rstrip.end_with?(",") || open_parens.positive?
406
+ while needs_more && i < lines.length
407
+ nxt = lines[i].to_s
408
+ i += 1
409
+ nxt_stripped = nxt.strip
410
+ break if nxt_stripped.empty? || nxt_stripped.start_with?("//", "#")
411
+
412
+ args << " " << nxt_stripped
413
+ open_parens = args.count("(") - args.count(")")
414
+ needs_more = args.rstrip.end_with?(",") || open_parens.positive?
415
+ end
416
+
417
+ args = args.sub(/\s*\)\s*;?\s*\z/, "")
418
+ extract_from_args.call(args)
419
+ end
420
+
356
421
  includes.uniq
357
422
  end
358
423
 
@@ -377,6 +442,8 @@ module Pindo
377
442
  return true if content.include?("com.android.application")
378
443
  return true if content.match?(/id\s*\(?\s*["']com\.android\.application["']\s*\)?/)
379
444
  return true if content.match?(/alias\(\s*libs\.plugins\.android\.application\s*\)/)
445
+ return true if content.match?(/apply\s+plugin:\s*["']com\.android\.application["']/)
446
+ return true if content.match?(/apply\s*\(\s*plugin\s*=\s*["']com\.android\.application["']\s*\)/)
380
447
 
381
448
  false
382
449
  end
@@ -385,18 +452,27 @@ module Pindo
385
452
  content = File.read(gradle_path)
386
453
  content.include?("com.android.asset-pack") ||
387
454
  content.match?(/id\s*\(?\s*["']com\.android\.asset-pack["']\s*\)?/) ||
388
- content.match?(/alias\(\s*libs\.plugins\.android\.asset\.pack\s*\)/)
455
+ content.match?(/alias\(\s*libs\.plugins\.android\.asset\.pack\s*\)/) ||
456
+ content.match?(/apply\s+plugin:\s*["']com\.android\.asset-pack["']/) ||
457
+ content.match?(/apply\s*\(\s*plugin\s*=\s*["']com\.android\.asset-pack["']\s*\)/)
389
458
  end
390
459
 
391
460
  def android_module?(gradle_path)
392
461
  content = File.read(gradle_path)
393
462
  return false if android_asset_pack_module?(gradle_path)
394
463
 
464
+ # 兼容多种 Gradle/AGP 插件写法:
465
+ # - plugins { id 'com.android.application' } / id("com.android.library")
466
+ # - apply plugin: 'com.android.application'
467
+ # - Unity/旧工程常见:apply plugin: 'android-library'
395
468
  content.include?("com.android.application") ||
396
469
  content.include?("com.android.library") ||
397
470
  content.include?("com.android.dynamic-feature") ||
398
471
  content.match?(/com\.android\.(application|library|dynamic-feature)/) ||
399
- content.match?(/alias\(\s*libs\.plugins\.android\.(application|library|dynamic\.feature)\s*\)/)
472
+ content.match?(/alias\(\s*libs\.plugins\.android\.(application|library|dynamic\.feature)\s*\)/) ||
473
+ content.match?(/apply\s+plugin:\s*["']android-library["']/) ||
474
+ content.match?(/apply\s+plugin:\s*["']com\.android\.(application|library|dynamic-feature)["']/) ||
475
+ content.match?(/apply\s*\(\s*plugin\s*=\s*["']com\.android\.(application|library|dynamic-feature)["']\s*\)/)
400
476
  end
401
477
 
402
478
  def gradle_dsl(gradle_path)
@@ -1,7 +1,6 @@
1
1
  require 'pindo/module/task/model/git_task'
2
2
  require 'pindo/base/git_handler'
3
3
  require 'pindo/module/utils/git_repo_helper'
4
- require 'pindo/module/utils/git_hook_helper'
5
4
  require 'pindo/options/helpers/git_constants'
6
5
 
7
6
  module Pindo
@@ -87,10 +86,7 @@ module Pindo
87
86
  # 2. 检查并修复 .gitignore(可能创建新提交,但 fixed_version 已经确定)
88
87
  git_repo_helper.check_gitignore(root_dir)
89
88
 
90
- # 3. 检查并安装 commit 变更统计 hook(主仓库 + 子模块)
91
- Pindo::GitHookHelper.share_instance.install_commit_stats_hook(root_dir)
92
-
93
- # 4. 检查并处理未提交的文件(使用 GitHandler)
89
+ # 3. 检查并处理未提交的文件(使用 GitHandler)
94
90
  # process_type 已经在初始化时确定(由外部传入或默认为 skip)
95
91
  begin
96
92
  Pindo::GitHandler.process_need_add_files(
@@ -124,7 +120,7 @@ module Pindo
124
120
  }
125
121
  end
126
122
 
127
- # 5. Stash 工作目录的残留修改(为分支同步做准备)
123
+ # 4. Stash 工作目录的残留修改(为分支同步做准备)
128
124
  coding_branch = nil
129
125
  stash_saved = false
130
126
  stash_name = "pindo_stash_#{Time.now.strftime('%Y%m%d%H%M%S')}_#{rand(1000)}"
@@ -136,13 +132,13 @@ module Pindo
136
132
  end
137
133
 
138
134
  begin
139
- # 6. 获取当前分支
135
+ # 5. 获取当前分支
140
136
  coding_branch = get_current_branch_name
141
137
 
142
- # 7. 检查并推送本地提交到远程(确保本地和远程同步)
138
+ # 6. 检查并推送本地提交到远程(确保本地和远程同步)
143
139
  Pindo::GitHandler.check_unpushed_commits(project_dir: root_dir, branch: coding_branch)
144
140
 
145
- # 8. 分支同步:将 coding_branch 和 release_branch 双向合并(确保两分支在同一节点)
141
+ # 7. 分支同步:将 coding_branch 和 release_branch 双向合并(确保两分支在同一节点)
146
142
  if coding_branch != @release_branch
147
143
  Funlog.instance.fancyinfo_start("开始同步 #{coding_branch} 和 #{@release_branch} 分支")
148
144
  Pindo::GitHandler.merge_branches(
@@ -154,7 +150,7 @@ module Pindo
154
150
  Funlog.instance.fancyinfo_success("分支同步完成,#{coding_branch} 和 #{@release_branch} 现在在同一节点")
155
151
  end
156
152
 
157
- # 9. 计算 build_version
153
+ # 8. 计算 build_version
158
154
  # 优先级 1: 如果指定了 fixed_version(外部传入或从 HEAD tag 获取),使用它
159
155
  if @fixed_version && !@fixed_version.empty?
160
156
  @build_version = @fixed_version
@@ -166,6 +166,20 @@ module Pindo
166
166
 
167
167
  result = execute_unity_build(platform)
168
168
 
169
+ # 确保导出产物中 `unityLibrary/libs` 物理存在 firebase-*-unity-*.aar,
170
+ # 以便下游仅携带导出目录时仍能正常集成 Firebase。
171
+ begin
172
+ copied = Pindo::AndroidProjectHelper.ensure_export_has_firebase_unity_aars!(
173
+ unity_root_path: @unity_root_path,
174
+ export_path: @export_path
175
+ )
176
+ puts "✓ 已同步 Firebase Unity AAR 到导出工程 unityLibrary/libs: #{copied.join(', ')}" if copied && !copied.empty?
177
+ rescue Informative
178
+ raise
179
+ rescue StandardError => e
180
+ raise Informative, "同步 Firebase Unity AAR 失败: #{e.message}"
181
+ end
182
+
169
183
  {
170
184
  success: true,
171
185
  platform: 'android',