pindo 5.18.9 → 5.18.12

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.
@@ -5,90 +5,115 @@ module Pindo
5
5
  class AndroidProjectHelper
6
6
  class << self
7
7
 
8
-
9
- # 确保导出的 Android 工程 `unityLibrary/libs` 里物理存在 Firebase Unity AAR(firebase-*-unity-*.aar)。
8
+ # 永久修复 Unity IL2CPP 工程:将 mergeDebug/mergeReleaseJniLibFolders 对 BuildIl2CppTask 的依赖
9
+ # 从“硬编码 buildType”改为“匹配所有 merge*JniLibFolders 变体”,避免自定义 buildType(如 workflow)
10
+ # 或 Unity/AGP 版本差异导致漏掉依赖关系。
10
11
  #
11
- # 某些 Unity 工程会通过 EDM4U Firebase Unity AAR 解析到 `Assets/GeneratedLocalRepo/**/m2repository`,
12
- # 并在导出工程中使用本地 maven repo 方式引用它们,导致 `unityLibrary/libs` 为空。
13
- # 当下游流程只携带“导出目录”时,这会表现为 Firebase AAR “缺失”。
12
+ # 仅修改 Unity 导出工程(包含 unityLibrary/build.gradle[.kts])中的 unityLibrary 模块 Gradle 文件。
13
+ # 幂等:已注入则不会重复写入。
14
14
  #
15
- # 本方法会从以下来源收集 firebase-*-unity-*.aar 并复制到导出工程:
16
- # - Unity 工程的 `ProjectSettings/AndroidResolverDependencies.xml`(优先)
17
- # - `Assets/GeneratedLocalRepo/**/m2repository/com/google/firebase/**/firebase-*-unity-*.aar`(兜底扫描)
15
+ # @return [Boolean] 是否发生了写入变更
16
+ def ensure_unity_il2cpp_jni_merge_depends_on!(project_path)
17
+ raise ArgumentError, "project_path 不能为空" if project_path.to_s.empty?
18
+ raise ArgumentError, "项目目录不存在: #{project_path}" unless File.directory?(project_path)
19
+
20
+ unity_library = File.join(project_path, "unityLibrary")
21
+ return false unless File.directory?(unity_library)
22
+
23
+ gradle_path = File.join(unity_library, "build.gradle")
24
+ kts_path = File.join(unity_library, "build.gradle.kts")
25
+ target_file = File.file?(kts_path) ? kts_path : (File.file?(gradle_path) ? gradle_path : nil)
26
+ return false unless target_file
27
+
28
+ original = File.read(target_file, encoding: "UTF-8")
29
+ # 仅在明显是 Unity IL2CPP 工程时介入
30
+ return false unless original.include?("BuildIl2CppTask")
31
+
32
+ dsl = target_file.end_with?(".kts") ? :kts : :groovy
33
+ updated = normalize_unity_il2cpp_jni_merge_depends_on_text(original)
34
+ updated = migrate_unity_il2cpp_jni_merge_depends_on_marker_text(updated)
35
+ updated = ensure_unity_il2cpp_jni_merge_depends_on_text(updated, dsl: dsl)
36
+
37
+ return false if updated == original
38
+
39
+ File.write(target_file, updated, encoding: "UTF-8")
40
+ true
41
+ end
42
+
43
+
44
+ # 根据导出工程 `unityLibrary/build.gradle[.kts]` 中**实际声明**的本地 AAR 依赖,校验并补齐 `unityLibrary/libs`。
18
45
  #
19
- # @return [Array<String>] 实际拷贝/确认存在的文件名列表
20
- def ensure_export_has_firebase_unity_aars!(unity_root_path:, export_path:)
46
+ # EDM4U 常将部分 AAR 解析到 `Assets/GeneratedLocalRepo/**/m2repository`,导出后 Gradle 仍按 `name + ext:aar`
47
+ #、`libs/某.aar` 或 `fileTree(dir: 'libs', include: ['*.aar', ...])` 引用;若只拷贝导出目录会缺文件。
48
+ # 声明来源:`AndroidResolverDependencies.xml` + `GeneratedLocalRepo`(`fileTree` 模式会合并二者中的 .aar 清单)。
49
+ #
50
+ # @return [Array<String>] 已在 libs 中存在或本次拷贝的 .aar 文件名
51
+ def ensure_export_unity_library_aars_from_gradle!(unity_root_path:, export_path:)
21
52
  raise ArgumentError, "unity_root_path 不能为空" if unity_root_path.to_s.empty?
22
53
  raise ArgumentError, "export_path 不能为空" if export_path.to_s.empty?
23
54
  raise ArgumentError, "Unity 工程目录不存在: #{unity_root_path}" unless File.directory?(unity_root_path)
24
55
  raise ArgumentError, "导出目录不存在: #{export_path}" unless File.directory?(export_path)
25
56
 
26
- libs_dir = File.join(export_path, "unityLibrary", "libs")
57
+ unity_library = File.join(export_path, "unityLibrary")
58
+ gradle_path = %w[build.gradle build.gradle.kts].map { |n| File.join(unity_library, n) }.find { |p| File.file?(p) }
59
+ libs_dir = File.join(unity_library, "libs")
27
60
  FileUtils.mkdir_p(libs_dir)
28
61
 
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?
62
+ unless gradle_path
63
+ return []
64
+ end
32
65
 
66
+ gradle_content = File.read(gradle_path, encoding: "UTF-8")
33
67
  resolver_xml = File.join(unity_root_path, "ProjectSettings", "AndroidResolverDependencies.xml")
68
+ aar_index = build_android_resolver_aar_index(unity_root_path, resolver_xml)
34
69
 
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 后重试。"
70
+ required_basenames =
71
+ if gradle_declares_libs_file_tree_with_aar?(gradle_content)
72
+ merged_aar_basenames_for_libs_file_tree(aar_index, unity_root_path)
73
+ else
74
+ extract_required_aar_basenames_from_unity_library_gradle(gradle_content)
51
75
  end
52
- end
53
76
 
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))
77
+ return [] if required_basenames.empty?
78
+
79
+ satisfied = []
80
+ missing_after_copy = []
81
+
82
+ required_basenames.each do |base|
83
+ dst = File.join(libs_dir, base)
84
+ if file_readable_nonbroken?(dst)
85
+ satisfied << base
86
+ next
87
+ end
88
+
89
+ src = resolve_aar_source_for_basename(base, aar_index, unity_root_path)
90
+ unless src
91
+ missing_after_copy << base
92
+ next
59
93
  end
60
- end
61
94
 
62
- candidates.uniq!
63
- existing = candidates.select { |p| File.file?(p) }
95
+ FileUtils.cp(src, dst)
96
+ satisfied << base
97
+ end
64
98
 
65
- if existing.empty?
99
+ unless missing_after_copy.empty?
66
100
  raise Informative, <<~MSG
67
- Firebase Unity AAR 依赖拷贝失败:未找到 firebase-*-unity-*.aar
101
+ Unity unityLibrary/libs 缺少 Gradle 已声明的 AAR,且无法在 Unity 工程内找到源文件:
102
+ #{missing_after_copy.sort.join(', ')}
68
103
  Unity 工程: #{unity_root_path}
69
104
  导出目录: #{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
- 然后重新导出再构建。
105
+ Gradle: #{gradle_path}
106
+ 请检查: #{resolver_xml}
107
+ 并在 Unity 中执行:External Dependency Manager → Android Resolver → Force Resolve 后重新导出。
76
108
  MSG
77
109
  end
78
110
 
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
111
+ satisfied.uniq
112
+ end
90
113
 
91
- copied.uniq
114
+ # 兼容旧调用名(实现已泛化为「按 Gradle 声明补齐 libs」)。
115
+ def ensure_export_has_firebase_unity_aars!(unity_root_path:, export_path:)
116
+ ensure_export_unity_library_aars_from_gradle!(unity_root_path: unity_root_path, export_path: export_path)
92
117
  end
93
118
 
94
119
  def unity_android_project?(project_path)
@@ -437,6 +462,266 @@ module Pindo
437
462
 
438
463
  private
439
464
 
465
+ def normalize_unity_il2cpp_jni_merge_depends_on_text(content)
466
+ return content if content.to_s.empty?
467
+
468
+ out = content.dup
469
+
470
+ # 清理旧的 hardcode 片段(只针对 BuildIl2CppTask + merge(Debug|Release)JniLibFolders 的 dependsOn)
471
+ out.gsub!(/^\s*project\(\s*['"]:unityLibrary['"]\s*\)\.merge(?:Debug|Release)JniLibFolders\.dependsOn\s+BuildIl2CppTask\s*\n?/m, "")
472
+ out.gsub!(/^\s*merge(?:Debug|Release)JniLibFolders\.dependsOn\s+BuildIl2CppTask\s*\n?/m, "")
473
+
474
+ # 清理旧的 findByName 判定(保守:仅删提到 merge(Debug|Release)JniLibFolders 的行)
475
+ out.gsub!(/^\s*if\s*\(\s*project\(\s*['"]:unityLibrary['"]\s*\)\.tasks\.findByName\(\s*['"]merge(?:Debug|Release)JniLibFolders['"]\s*\)\s*\)\s*\n?/m, "")
476
+
477
+ # 也清理一些常见的单行 if(...) 写法(保守:只删包含 mergeXJniLibFolders + BuildIl2CppTask 的行)
478
+ out.gsub!(/^\s*if\s*\([^\n]*merge(?:Debug|Release)JniLibFolders[^\n]*\)\s*[^\n]*BuildIl2CppTask[^\n]*\n?/m, "")
479
+
480
+ # 如果上面把 afterEvaluate 块内容清空了,这里移除“空壳 afterEvaluate { }”避免残留无意义块,
481
+ # 后续由 ensure_* 重新按目标写法注入。
482
+ #
483
+ # 仅移除“块内无任何非空白/非注释内容”的 afterEvaluate,避免误删业务逻辑。
484
+ out.gsub!(/^\s*afterEvaluate\s*\{\s*\n(?:(?:\s*\/\/[^\n]*\n)|(?:\s*\n))*\s*\}\s*\n?/m, "")
485
+
486
+ out
487
+ end
488
+
489
+ # 迁移旧注入块:如果历史版本把 marker + afterEvaluate 注入在文件尾部(android 块外),
490
+ # 这里先移除旧块,再按新规则(插入 android {} 内部)重新注入,确保与目标工程写法一致。
491
+ def migrate_unity_il2cpp_jni_merge_depends_on_marker_text(content)
492
+ return content if content.to_s.empty?
493
+
494
+ marker = "PINDO_UNITY_IL2CPP_JNI_MERGE_DEPENDS_ON"
495
+ idx = content.index(marker)
496
+ return content unless idx
497
+
498
+ # 从 marker 行开始,尝试定位其后的 afterEvaluate { ... } 并用花括号计数移除该段
499
+ line_start = content.rindex("\n", idx) || 0
500
+ line_start = 0 if line_start == 0
501
+ after_eval = content.index("afterEvaluate", idx)
502
+ return content unless after_eval
503
+
504
+ brace_open = content.index("{", after_eval)
505
+ return content unless brace_open
506
+
507
+ brace_close = find_matching_brace_simple(content, brace_open)
508
+ return content unless brace_close
509
+
510
+ # 删除到 afterEvaluate 块结束行尾
511
+ line_end = content.index("\n", brace_close) || brace_close
512
+ content[0...line_start] + content[(line_end + 1)..].to_s
513
+ end
514
+
515
+ def ensure_unity_il2cpp_jni_merge_depends_on_text(content, dsl:)
516
+ return content if content.to_s.empty?
517
+
518
+ marker = "PINDO_UNITY_IL2CPP_JNI_MERGE_DEPENDS_ON"
519
+ # 已经注入过(新老位置都带 marker),则认为已满足;调用方如需迁移会先 migrate_* 移除再注入
520
+ return content if content.include?(marker)
521
+
522
+ # 已存在等价实现(但没有 marker)时,不再重复注入,避免出现多个 afterEvaluate 块。
523
+ return content if unity_il2cpp_jni_merge_depends_on_already_present?(content)
524
+
525
+ snippet = unity_il2cpp_jni_merge_depends_on_snippet(marker, dsl: dsl)
526
+ inject_into_android_block_text(content, snippet)
527
+ end
528
+
529
+ def unity_il2cpp_jni_merge_depends_on_snippet(marker, dsl:)
530
+ if dsl == :kts
531
+ <<~KTS.rstrip
532
+ // #{marker}
533
+ afterEvaluate {
534
+ tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }.configureEach {
535
+ dependsOn(BuildIl2CppTask)
536
+ }
537
+ }
538
+ KTS
539
+ else
540
+ <<~GRADLE.rstrip
541
+ // #{marker}
542
+ afterEvaluate {
543
+ tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }.configureEach {
544
+ dependsOn BuildIl2CppTask
545
+ }
546
+ }
547
+ GRADLE
548
+ end
549
+ end
550
+
551
+ # 将 snippet 插入到 android { ... } 的末尾(最后一个 } 之前)。
552
+ # 如果找不到 android 块,则回退到文件末尾追加(但 Unity 导出工程一般都有 android)。
553
+ def inject_into_android_block_text(content, snippet)
554
+ android_open = content.index(/\bandroid\s*\{/)
555
+ return content.rstrip + "\n\n" + snippet + "\n" unless android_open
556
+
557
+ brace_open = content.index("{", android_open)
558
+ return content.rstrip + "\n\n" + snippet + "\n" unless brace_open
559
+
560
+ brace_close = find_matching_brace_simple(content, brace_open)
561
+ return content.rstrip + "\n\n" + snippet + "\n" unless brace_close
562
+
563
+ inner = content[(brace_open + 1)...brace_close]
564
+ # 采用 android 块的缩进风格:取 android 关键字所在行的缩进,然后 +4
565
+ line_start = content.rindex("\n", android_open) || 0
566
+ line_start = 0 if line_start == 0
567
+ base_indent = content[line_start..android_open].match(/^\s*/).to_s
568
+ snippet_indented = indent_block_text(dedent_block_text(snippet), base_indent + " ")
569
+
570
+ # 避免插入过多空行:统一只在前后各保留一个换行
571
+ updated_inner = inner.rstrip + "\n" + snippet_indented.rstrip + "\n"
572
+ content[0..brace_open] + updated_inner + content[brace_close..]
573
+ end
574
+
575
+ # 判定是否已经存在等价的「merge*JniLibFolders dependsOn BuildIl2CppTask」逻辑(不依赖 marker)。
576
+ # 这用于避免 Unity 工程本身已包含同等逻辑时重复插入。
577
+ def unity_il2cpp_jni_merge_depends_on_already_present?(content)
578
+ return false if content.to_s.empty?
579
+
580
+ # 仅在存在 BuildIl2CppTask 时才认为可能等价
581
+ return false unless content.include?("BuildIl2CppTask")
582
+
583
+ has_matching =
584
+ content.match?(/tasks\.matching\s*\{\s*it\.name\.startsWith\(\s*["']merge["']\s*\)\s*&&\s*it\.name\.endsWith\(\s*["']JniLibFolders["']\s*\)\s*\}/) ||
585
+ content.match?(/tasks\.matching\s*\{\s*it\.name\.startsWith\(\s*["']merge["']\s*\)\s*&&\s*it\.name\.endsWith\(\s*["']JniLibFolders["']\s*\)\s*\}\.configureEach/)
586
+
587
+ return false unless has_matching
588
+
589
+ # dependsOn 允许 Groovy 简写或括号写法
590
+ content.match?(/dependsOn\s*(?:\(\s*)?BuildIl2CppTask(?:\s*\))?/)
591
+ end
592
+
593
+ # 去掉 block 的公共前导缩进(保留相对缩进)
594
+ def dedent_block_text(text)
595
+ lines = text.to_s.lines
596
+ indents = lines.filter_map do |l|
597
+ next if l.strip.empty?
598
+
599
+ l[/^\s*/].to_s.length
600
+ end
601
+ return text if indents.empty?
602
+
603
+ cut = indents.min
604
+ lines.map do |l|
605
+ l.strip.empty? ? l : l.sub(/^\s{0,#{cut}}/, "")
606
+ end.join
607
+ end
608
+
609
+ def indent_block_text(text, indent)
610
+ text.to_s.lines.map do |l|
611
+ l.strip.empty? ? l : (indent + l)
612
+ end.join
613
+ end
614
+
615
+ # 简单花括号匹配(不解析字符串/注释):用于 Unity 导出 Gradle(结构相对规整)。
616
+ def find_matching_brace_simple(str, open_brace_index)
617
+ depth = 0
618
+ i = open_brace_index
619
+ while i < str.length
620
+ ch = str.getbyte(i)
621
+ if ch == 123 # {
622
+ depth += 1
623
+ elsif ch == 125 # }
624
+ depth -= 1
625
+ return i if depth == 0
626
+ end
627
+ i += 1
628
+ end
629
+ nil
630
+ end
631
+
632
+ def file_readable_nonbroken?(path)
633
+ return false unless path && File.exist?(path)
634
+ return false unless File.file?(path)
635
+
636
+ return File.exist?(path) if File.symlink?(path)
637
+
638
+ true
639
+ end
640
+
641
+ # implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 等:按 Resolver + GeneratedLocalRepo 合并 .aar 清单
642
+ def gradle_declares_libs_file_tree_with_aar?(content)
643
+ return false unless content.include?("fileTree")
644
+
645
+ libs_dir_arg = %q{['"]\.?/?libs['"]}
646
+ content.scan(/fileTree\s*\(\s*dir\s*:\s*#{libs_dir_arg}\s*,\s*include\s*:\s*(\[[^\]]+\])/im) do |m|
647
+ return true if gradle_file_tree_include_lists_aar?(m[0])
648
+ end
649
+ content.scan(/fileTree\s*\(\s*include\s*:\s*(\[[^\]]+\])\s*,\s*dir\s*:\s*#{libs_dir_arg}\s*\)/im) do |m|
650
+ return true if gradle_file_tree_include_lists_aar?(m[0])
651
+ end
652
+
653
+ # Kotlin: fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar")))
654
+ if content.match?(/fileTree\s*\(\s*mapOf/im) && content.match?(/"dir"\s+to\s+"libs"/m)
655
+ return true if content.match?(/(?:listOf|setOf)\s*\([^)]*["'][^"']*\.aar["']/m)
656
+ end
657
+
658
+ false
659
+ end
660
+
661
+ def gradle_file_tree_include_lists_aar?(include_bracket_text)
662
+ include_bracket_text.match?(/['"][^'"]*\*[^'"]*\.aar['"]/)
663
+ end
664
+
665
+ def merged_aar_basenames_for_libs_file_tree(aar_index, unity_root_path)
666
+ bases = aar_index.keys.dup
667
+ generated = File.join(unity_root_path, "Assets", "GeneratedLocalRepo")
668
+ if File.directory?(generated)
669
+ Dir.glob(File.join(generated, "**", "*.aar")).each do |p|
670
+ bases << File.basename(p) if File.file?(p)
671
+ end
672
+ end
673
+ bases.uniq
674
+ end
675
+
676
+ def extract_required_aar_basenames_from_unity_library_gradle(content)
677
+ names = []
678
+ dep_types = %w[implementation api compileOnly runtimeOnly debugImplementation releaseImplementation]
679
+ dep_alt = dep_types.join("|")
680
+
681
+ content.scan(/(?:#{dep_alt})\s*\(\s*name\s*:\s*['"]([^'"]+)['"]\s*,\s*ext\s*:\s*['"]aar['"]/im) { names << "#{Regexp.last_match(1)}.aar" }
682
+ content.scan(/(?:#{dep_alt})\s*\(\s*ext\s*:\s*['"]aar['"]\s*,\s*name\s*:\s*['"]([^'"]+)['"]/im) { names << "#{Regexp.last_match(1)}.aar" }
683
+ content.scan(/(?:#{dep_alt})\s*\(\s*name\s*=\s*["']([^'"]+)["']\s*,\s*ext\s*=\s*["']aar["']/im) { names << "#{Regexp.last_match(1)}.aar" }
684
+ content.scan(/(?:#{dep_alt})\s*\(\s*ext\s*=\s*["']aar["']\s*,\s*name\s*=\s*["']([^'"]+)["']/im) { names << "#{Regexp.last_match(1)}.aar" }
685
+
686
+ content.scan(/['"](?:\.\/)?libs\/([^'"]+\.aar)['"]/i) { names << Regexp.last_match(1) }
687
+
688
+ names.uniq
689
+ end
690
+
691
+ def build_android_resolver_aar_index(unity_root_path, resolver_xml)
692
+ index = {}
693
+ return index unless File.file?(resolver_xml)
694
+
695
+ require "rexml/document"
696
+ doc = REXML::Document.new(File.read(resolver_xml, encoding: "UTF-8"))
697
+ doc.elements.each("dependencies/files/file") do |e|
698
+ rel = e.text.to_s.strip
699
+ next if rel.empty?
700
+ next unless rel.end_with?(".aar")
701
+
702
+ abs = Pathname.new(rel).absolute? ? rel : File.join(unity_root_path, rel)
703
+ base = File.basename(rel)
704
+ (index[base] ||= []) << abs
705
+ end
706
+ index
707
+ rescue StandardError => e
708
+ raise Informative, "解析 AndroidResolverDependencies.xml 失败: #{e.message}\n请在 Unity 中执行 EDM4U Force Resolve 后重试。"
709
+ end
710
+
711
+ def resolve_aar_source_for_basename(basename, aar_index, unity_root_path)
712
+ (aar_index[basename] || []).each do |abs|
713
+ return abs if File.file?(abs)
714
+ end
715
+
716
+ generated = File.join(unity_root_path, "Assets", "GeneratedLocalRepo")
717
+ if File.directory?(generated)
718
+ hit = Dir.glob(File.join(generated, "**", basename)).find { |p| File.file?(p) }
719
+ return hit if hit
720
+ end
721
+
722
+ nil
723
+ end
724
+
440
725
  def extract_modules_from_settings(content)
441
726
  modules = []
442
727
  # 兼容 include ':app' / include(":app", ":lib")
@@ -160,9 +160,69 @@ module Pindo
160
160
  provisioning_info_array: provisioning_info_array
161
161
  )
162
162
 
163
+ # 清理 workspace 中其他工程(如 Unity-iPhone)的旧 PROVISIONING_PROFILE 设置
164
+ # gym 会扫描 workspace 下所有 project,旧的 UUID 会覆盖正确的 specifier
165
+ clean_stale_provisioning_profiles_in_workspace(project_dir: project_dir, provisioning_info_array: provisioning_info_array)
166
+
163
167
  Funlog.instance.fancyinfo_success("Xcode 工程配置完成!")
164
168
  end
165
169
 
170
+ # 清理 workspace 中其他工程的旧 PROVISIONING_PROFILE (UUID) 设置
171
+ # gym 扫描 workspace 下所有 project 时,旧的 PROVISIONING_PROFILE UUID 会覆盖
172
+ # 主工程中正确的 PROVISIONING_PROFILE_SPECIFIER,导致导出失败
173
+ def clean_stale_provisioning_profiles_in_workspace(project_dir:, provisioning_info_array:)
174
+ # 查找 workspace
175
+ workspace_file = Dir.glob(File.join(project_dir, "*.xcworkspace")).first
176
+ return unless workspace_file && File.exist?(workspace_file)
177
+
178
+ main_xcodeproj = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by { |f| File.mtime(f) }
179
+ return unless main_xcodeproj
180
+
181
+ main_proj_name = File.basename(main_xcodeproj)
182
+
183
+ # 解析 workspace 中的所有 project
184
+ workspace_data_file = File.join(workspace_file, "contents.xcworkspacedata")
185
+ return unless File.exist?(workspace_data_file)
186
+
187
+ require 'rexml/document'
188
+ doc = REXML::Document.new(File.read(workspace_data_file))
189
+ doc.elements.each('Workspace/FileRef') do |file_ref|
190
+ location = file_ref.attributes['location']
191
+ next unless location
192
+ relative_path = location.sub(/^group:/, '')
193
+ next unless relative_path.end_with?('.xcodeproj')
194
+ # 跳过主工程和 Pods 工程
195
+ next if relative_path == main_proj_name || relative_path.start_with?('Pods/')
196
+
197
+ full_path = File.join(project_dir, relative_path)
198
+ next unless File.exist?(full_path)
199
+
200
+ begin
201
+ sub_project = Xcodeproj::Project.open(full_path)
202
+ project_modified = false
203
+
204
+ sub_project.targets.each do |target|
205
+ target.build_configurations.each do |config|
206
+ # 清理旧的 PROVISIONING_PROFILE (UUID 格式的旧字段)
207
+ old_profile = config.build_settings['PROVISIONING_PROFILE']
208
+ if old_profile && !old_profile.empty?
209
+ config.build_settings.delete('PROVISIONING_PROFILE')
210
+ project_modified = true
211
+ end
212
+ end
213
+ end
214
+
215
+ if project_modified
216
+ sub_project.save
217
+ puts " ✓ 已清理 #{relative_path} 中的旧 PROVISIONING_PROFILE 设置" if ENV['PINDO_DEBUG'] == '1' || ENV['PINDO_DEBUG'] == 'true'
218
+ end
219
+ rescue => e
220
+ # 不中断主流程
221
+ puts " ⚠ 清理 #{relative_path} 时出错: #{e.message}" if ENV['PINDO_DEBUG'] == '1' || ENV['PINDO_DEBUG'] == 'true'
222
+ end
223
+ end
224
+ end
225
+
166
226
  # ========================================
167
227
  # 共享配置:Target 映射
168
228
  # ========================================
@@ -355,6 +415,14 @@ module Pindo
355
415
  unless provisioning_info["bundle_id"].include?("*")
356
416
  config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = provisioning_info["bundle_id"]
357
417
  end
418
+
419
+ if ENV['PINDO_DEBUG'] == '1' || ENV['PINDO_DEBUG'] == 'true'
420
+ puts "[DEBUG] configure_target_build_settings: target=#{target.name}, config=#{config.name}"
421
+ puts "[DEBUG] PROVISIONING_PROFILE_SPECIFIER = #{config.build_settings['PROVISIONING_PROFILE_SPECIFIER']}"
422
+ puts "[DEBUG] PRODUCT_BUNDLE_IDENTIFIER = #{config.build_settings['PRODUCT_BUNDLE_IDENTIFIER']}"
423
+ puts "[DEBUG] profile_name(来源) = #{provisioning_info['profile_name']}"
424
+ puts "[DEBUG] bundle_id(来源) = #{provisioning_info['bundle_id']}"
425
+ end
358
426
  end
359
427
  end
360
428
 
@@ -218,6 +218,13 @@ module Pindo
218
218
  data = result['data'] || {}
219
219
  profiles = data['appleProfiles'] || []
220
220
 
221
+ if debug_mode?
222
+ Funlog.instance.info("JPS API 返回 #{profiles.size} 个描述文件:")
223
+ profiles.each_with_index do |p, i|
224
+ Funlog.instance.info(" [#{i}] name=#{p['name']}, bundleIdIdentifier=#{p['bundleIdIdentifier']}, profileUrl=#{p['profileUrl'] ? '(有)' : '(无)'}")
225
+ end
226
+ end
227
+
221
228
  if profiles.empty?
222
229
  raise Informative, "未找到匹配的描述文件 (apple_dev_account: #{apple_id}, bundle_ids=#{bundle_ids.join(', ')}, platform: #{platform}, type: #{profile_type})"
223
230
  end
@@ -299,9 +306,26 @@ module Pindo
299
306
  # 重新解析已安装的描述文件
300
307
  parsed_data = Provisioninghelper.parse(dest_path)
301
308
 
309
+ if debug_mode?
310
+ Funlog.instance.info("描述文件解析结果:")
311
+ Funlog.instance.info(" Name: #{parsed_data['Name']}")
312
+ Funlog.instance.info(" UUID: #{parsed_data['UUID']}")
313
+ Funlog.instance.info(" AppIDName: #{parsed_data['AppIDName']}")
314
+ Funlog.instance.info(" TeamIdentifier: #{parsed_data['TeamIdentifier']}")
315
+ Funlog.instance.info(" TeamName: #{parsed_data['TeamName']}")
316
+ entitlements = parsed_data['Entitlements'] || {}
317
+ Funlog.instance.info(" application-identifier: #{entitlements['application-identifier']}")
318
+ Funlog.instance.info(" ExpirationDate: #{parsed_data['ExpirationDate']}")
319
+ end
320
+
302
321
  # 从 bundle_id_map 中查找对应的 type
303
322
  type = bundle_id_map.key(actual_bundle_id) || 'bundle_id'
304
323
 
324
+ if debug_mode?
325
+ Funlog.instance.info("bundle_id 映射: actual_bundle_id=#{actual_bundle_id}, type=#{type}")
326
+ Funlog.instance.info("bundle_id_map: #{bundle_id_map.inspect}")
327
+ end
328
+
305
329
  # 构建 provisioning_info 对象
306
330
  provisioning_info = {
307
331
  'type' => type,
@@ -317,6 +341,15 @@ module Pindo
317
341
  provisioning_info['signing_identity'] = cert_info["Common Name"]
318
342
  end
319
343
 
344
+ if debug_mode?
345
+ Funlog.instance.info("provisioning_info 最终结果:")
346
+ Funlog.instance.info(" type: #{provisioning_info['type']}")
347
+ Funlog.instance.info(" bundle_id: #{provisioning_info['bundle_id']}")
348
+ Funlog.instance.info(" profile_name: #{provisioning_info['profile_name']}")
349
+ Funlog.instance.info(" signing_identity: #{provisioning_info['signing_identity']}")
350
+ Funlog.instance.info(" team_id: #{provisioning_info['team_id']}")
351
+ end
352
+
320
353
  provisioning_info_array << provisioning_info
321
354
 
322
355
  Funlog.instance.fancyinfo_success("处理完成: #{actual_bundle_id}") if debug_mode?
@@ -166,18 +166,17 @@ module Pindo
166
166
 
167
167
  result = execute_unity_build(platform)
168
168
 
169
- # 确保导出产物中 `unityLibrary/libs` 物理存在 firebase-*-unity-*.aar,
170
- # 以便下游仅携带导出目录时仍能正常集成 Firebase。
169
+ # 按导出 unityLibrary/build.gradle[.kts] 声明的本地 AAR,补齐 unityLibrary/libs(与 Resolver / GeneratedLocalRepo 对齐)。
171
170
  begin
172
- copied = Pindo::AndroidProjectHelper.ensure_export_has_firebase_unity_aars!(
171
+ copied = Pindo::AndroidProjectHelper.ensure_export_unity_library_aars_from_gradle!(
173
172
  unity_root_path: @unity_root_path,
174
173
  export_path: @export_path
175
174
  )
176
- puts "✓ 已同步 Firebase Unity AAR 到导出工程 unityLibrary/libs: #{copied.join(', ')}" if copied && !copied.empty?
175
+ puts "✓ 已按 Gradle 声明同步 AAR 到导出工程 unityLibrary/libs: #{copied.join(', ')}" if copied && !copied.empty?
177
176
  rescue Informative
178
177
  raise
179
178
  rescue StandardError => e
180
- raise Informative, "同步 Firebase Unity AAR 失败: #{e.message}"
179
+ raise Informative, "同步 unityLibrary/libs AAR 失败: #{e.message}"
181
180
  end
182
181
 
183
182
  {
@@ -162,9 +162,12 @@ module Pindo
162
162
  Pindo::GitHandler.git!(%W(-C #{git_root_dir} commit -m #{commit_message}))
163
163
  Funlog.instance.fancyinfo_success("已自动提交 .gitignore 更改")
164
164
 
165
- # 推送到远程
166
- Pindo::GitHandler.git!(%W(-C #{git_root_dir} push origin #{current_branch}))
167
- Funlog.instance.fancyinfo_success("已自动推送 .gitignore 到远程仓库")
165
+ # 推送到远程(仅在 remote origin 存在时)
166
+ has_remote = Pindo::GitHandler.git!(%W(-C #{git_root_dir} remote)).strip
167
+ if has_remote.split("\n").include?("origin")
168
+ Pindo::GitHandler.git!(%W(-C #{git_root_dir} push origin #{current_branch}))
169
+ Funlog.instance.fancyinfo_success("已自动推送 .gitignore 到远程仓库")
170
+ end
168
171
  end
169
172
  rescue => e
170
173
  Funlog.instance.fancyinfo_error("Git 操作失败: #{e.message}")