pindo 5.18.9 → 5.18.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3b74ddb66022968993f331b407f3a50a41d94a827e15e75f75dac1b8d5aadc1
4
- data.tar.gz: '08c5e2e855359f4369e02cb6cab841d38e3344794a7d4e8f9b0f2695683d1da2'
3
+ metadata.gz: 78cfff9c32f28c301552fdf5d7fbbe71c71b0ed6e8ec3ea178fdcb985cf07677
4
+ data.tar.gz: d96bf95c17ae486d94745466add5f521ff087bbb133ea36a24f17fcb2f2421cc
5
5
  SHA512:
6
- metadata.gz: 6c1c36ddcf83fcff151bdd21e8e3104eec862b5c72aa15303a8fe3cae4f9be0dd36f37e2cee24d293e6ace76f895c72f057a18f12485c6d4a08905bb49350e21
7
- data.tar.gz: 1f3a484ff340bd398d150960753bb541103aca2b5a8ba30e712733c029e209c9fdfcb7db6c2d80b7c1bb1e1c8202d3319600ab12d23c4cb928c3f55c2390e74c
6
+ metadata.gz: a42d4745b4e40dc76d4e984456932e61a5abf18bdd6588c26849ead270d3e2a4ca5fef45daa9098e90a6ad960ad519968e1dcb003cb0e885215154d3ea339eab
7
+ data.tar.gz: fbe00ac4256e731bd92af5950a2097ce92c908c61aa02f94111f03cd4f53fdfb45cf6f777c247d9e04c2b34c66b356c73769f4db00f72170d93bbe789494d8d8
@@ -35,6 +35,7 @@ module Pindo
35
35
  puts "处理独立的 Unity 导出工程..."
36
36
  Pindo::GradleHelper.update_build_gradle(project_dir)
37
37
  Pindo::AndroidProjectHelper.add_unity_namespace(project_dir)
38
+ Pindo::AndroidProjectHelper.ensure_unity_il2cpp_jni_merge_depends_on!(project_dir)
38
39
  Pindo::AndroidProjectHelper.modify_il2cpp_config(project_dir)
39
40
  Pindo::AndroidProjectHelper.remove_desktop_google_service(project_dir)
40
41
 
@@ -61,6 +62,7 @@ module Pindo
61
62
 
62
63
  # Unity 特有的处理
63
64
  Pindo::AndroidProjectHelper.add_unity_namespace(unity_dir)
65
+ Pindo::AndroidProjectHelper.ensure_unity_il2cpp_jni_merge_depends_on!(unity_dir)
64
66
  Pindo::AndroidProjectHelper.modify_il2cpp_config(unity_dir)
65
67
  Pindo::AndroidProjectHelper.remove_desktop_google_service(unity_dir)
66
68
  end
@@ -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}")
@@ -442,6 +442,31 @@ module Pindo
442
442
  end
443
443
  info_plist_path = File.join(project_dir, relative_plist_path)
444
444
 
445
+ # 如果文件已存在于磁盘(如 GENERATE_INFOPLIST_FILE 模式下项目自带的 Info.plist)
446
+ if File.exist?(info_plist_path)
447
+ if raw_path.nil? || raw_path.empty?
448
+ # 需要设置 INFOPLIST_FILE,让 ProcessInfoPlistFile 处理我们写入的配置
449
+ # (URL Schemes、Facebook、AdMob、版本号等)
450
+ # 对于 synchronized group,先排除 membership 避免 "Multiple commands produce" 冲突
451
+ plist_dir = File.dirname(relative_plist_path)
452
+ plist_name = File.basename(relative_plist_path)
453
+ sync_group = if plist_dir == "." || plist_dir.empty?
454
+ project_obj.main_group.find_subpath(target_name, false)
455
+ else
456
+ project_obj.main_group.find_subpath(plist_dir, false)
457
+ end
458
+ if sync_group.is_a?(Xcodeproj::Project::Object::PBXFileSystemSynchronizedRootGroup)
459
+ add_membership_exception(sync_group, target, plist_name)
460
+ end
461
+
462
+ target.build_configurations.each do |config|
463
+ config.build_settings['INFOPLIST_FILE'] = relative_plist_path
464
+ end
465
+ project_obj.save
466
+ end
467
+ return info_plist_path
468
+ end
469
+
445
470
  if is_generated_mode
446
471
  Funlog.instance.fancyinfo_warning("Target #{target_name} 使用 GENERATE_INFOPLIST_FILE 模式,创建补充 Info.plist 用于复杂配置")
447
472
  else
@@ -453,14 +478,9 @@ module Pindo
453
478
  empty_plist = {}
454
479
  Xcodeproj::Plist.write_to_path(empty_plist, info_plist_path)
455
480
 
456
- # 3.2 设置 build settings
457
- target.build_configurations.each do |config|
458
- config.build_settings['INFOPLIST_FILE'] = relative_plist_path
459
- # 保持 GENERATE_INFOPLIST_FILE = YES,Xcode 自动合并
460
- config.build_settings['GENERATE_INFOPLIST_FILE'] ||= 'YES'
461
- end
462
-
463
- # 3.3 将文件添加到 Xcode 工程引用,保持与实际相对路径一致
481
+ # 3.2 将文件添加到 Xcode 工程并设置 INFOPLIST_FILE
482
+ # 必须设置 INFOPLIST_FILE,否则写入的 URL Schemes、Facebook、AdMob 等配置
483
+ # 不会被 ProcessInfoPlistFile 处理,不会进入最终产物
464
484
  plist_dir = File.dirname(relative_plist_path)
465
485
  plist_name = File.basename(relative_plist_path)
466
486
  target_group =
@@ -470,10 +490,25 @@ module Pindo
470
490
  project_obj.main_group.find_subpath(plist_dir, true)
471
491
  end
472
492
 
473
- unless target_group.files.any? { |f| f.path == plist_name } ||
474
- project_obj.files.any? { |f| f.path == relative_plist_path }
475
- target_group.new_file(plist_name)
476
- Funlog.instance.fancyinfo_success("已将 Info.plist 添加到 Xcode 工程: #{relative_plist_path}")
493
+ if target_group.is_a?(Xcodeproj::Project::Object::PBXFileSystemSynchronizedRootGroup)
494
+ # Xcode 16+ synchronized group:文件自动同步到 Copy Bundle Resources,
495
+ # 必须先排除 membership,再设置 INFOPLIST_FILE,避免 "Multiple commands produce" 冲突
496
+ add_membership_exception(target_group, target, plist_name)
497
+ Funlog.instance.fancyinfo_success("已将 Info.plist 从 target 编译成员中排除: #{plist_name}")
498
+ elsif target_group.respond_to?(:files)
499
+ # 传统 PBXGroup:手动添加文件引用
500
+ unless target_group.files.any? { |f| f.path == plist_name } ||
501
+ project_obj.files.any? { |f| f.path == relative_plist_path }
502
+ target_group.new_file(plist_name)
503
+ Funlog.instance.fancyinfo_success("已将 Info.plist 添加到 Xcode 工程引用: #{relative_plist_path}")
504
+ end
505
+ end
506
+
507
+ # 设置 INFOPLIST_FILE,让 ProcessInfoPlistFile 处理此文件
508
+ target.build_configurations.each do |config|
509
+ config.build_settings['INFOPLIST_FILE'] = relative_plist_path
510
+ # GENERATE_INFOPLIST_FILE 模式下保持 YES,Xcode 会合并 build settings 和 plist 文件
511
+ config.build_settings['GENERATE_INFOPLIST_FILE'] ||= 'YES'
477
512
  end
478
513
 
479
514
  project_obj.save
@@ -481,6 +516,32 @@ module Pindo
481
516
  info_plist_path
482
517
  end
483
518
 
519
+ # 为 PBXFileSystemSynchronizedRootGroup 添加 membershipExceptions
520
+ # 将指定文件从 target 的编译成员中排除(Xcode 16+ 同步组机制)
521
+ # @param sync_group [PBXFileSystemSynchronizedRootGroup] 同步根组
522
+ # @param target [PBXNativeTarget] 目标 target
523
+ # @param file_name [String] 要排除的文件名(如 "Info.plist")
524
+ def add_membership_exception(sync_group, target, file_name)
525
+ # 查找该 target 已有的 exception set
526
+ existing_exception = sync_group.exceptions.find { |e|
527
+ e.is_a?(Xcodeproj::Project::Object::PBXFileSystemSynchronizedBuildFileExceptionSet) && e.target == target
528
+ }
529
+
530
+ if existing_exception
531
+ # 已有 exception set,追加文件(避免重复)
532
+ existing_exception.membership_exceptions ||= []
533
+ unless existing_exception.membership_exceptions.include?(file_name)
534
+ existing_exception.membership_exceptions << file_name
535
+ end
536
+ else
537
+ # 创建新的 PBXFileSystemSynchronizedBuildFileExceptionSet
538
+ exception_set = target.project.new(Xcodeproj::Project::Object::PBXFileSystemSynchronizedBuildFileExceptionSet)
539
+ exception_set.target = target
540
+ exception_set.membership_exceptions = [file_name]
541
+ sync_group.exceptions << exception_set
542
+ end
543
+ end
544
+
484
545
  def modify_info_plist(project_dir:nil, proj_name:nil, config_json:nil)
485
546
 
486
547
  ## Main Info.plist
@@ -707,6 +768,9 @@ module Pindo
707
768
  end
708
769
  end
709
770
 
771
+ # 注意:Xcode 26+ 的 xcodebuild 会提示 "development" 已废弃建议用 "debugging",
772
+ # 但 fastlane gym 的 export_method 白名单尚未支持 "debugging",继续使用 "development"
773
+
710
774
  # 检查 Unity 测试模板
711
775
  if build_type.eql?("app-store") || build_type.eql?("ad-hoc")
712
776
  if File.exist?(File.join(project_path, "Unity/Data/Raw/SettingsPluginFlag.txt"))
@@ -763,56 +827,34 @@ module Pindo
763
827
  values[:export_options][:iCloudContainerEnvironment] = icloud_env
764
828
  end
765
829
 
766
- # # 为所有 target 配置 provisioning profile(包括主应用和扩展)
767
- # provisioning_profiles = {}
768
- # project_obj.targets.each do |target|
769
- # # 获取 Release 配置(export 时使用的配置)
770
- # release_config = target.build_configurations.find { |config| config.name == 'Release' } || target.build_configurations.first
771
-
772
- # # 获取 target 的 Bundle ID(从 resolved_build_setting 获取实际值)
773
- # bundle_id = release_config.resolve_build_setting('PRODUCT_BUNDLE_IDENTIFIER')
774
-
775
- # # 如果 resolve_build_setting 返回 nil,尝试直接获取
776
- # if bundle_id.nil?
777
- # bundle_id = release_config.build_settings['PRODUCT_BUNDLE_IDENTIFIER']
778
- # end
779
-
780
- # puts "[DEBUG] Target: #{target.name}, Bundle ID: #{bundle_id}"
781
-
782
- # # 跳过没有 Bundle ID 的 target
783
- # next if bundle_id.nil? || bundle_id.to_s.empty? || bundle_id.to_s.include?('$(')
830
+ # 从主工程显式读取 provisioning profile 映射,避免 gym 自动扫描 workspace
831
+ # 时被其他工程(如 Unity-iPhone)中的旧 PROVISIONING_PROFILE UUID 污染
832
+ provisioning_profiles = {}
833
+ project_obj.targets.each do |target|
834
+ next unless target.respond_to?(:product_type)
784
835
 
785
- # # 获取 target provisioning profile specifier
786
- # profile_specifier = release_config.build_settings['PROVISIONING_PROFILE_SPECIFIER']
836
+ release_config = target.build_configurations.find { |config| config.name == 'Release' } || target.build_configurations.first
837
+ next if release_config.nil?
787
838
 
788
- # # 如果没有找到,尝试查找 SDK 特定的配置(如 PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*])
789
- # if profile_specifier.nil? || profile_specifier.to_s.empty?
790
- # release_config.build_settings.each do |key, value|
791
- # if key.to_s.start_with?('PROVISIONING_PROFILE_SPECIFIER')
792
- # profile_specifier = value
793
- # puts "[DEBUG] Found SDK-specific profile: #{key} = #{value}"
794
- # break
795
- # end
796
- # end
797
- # end
839
+ bundle_id = release_config.resolve_build_setting('PRODUCT_BUNDLE_IDENTIFIER')
840
+ bundle_id ||= release_config.build_settings['PRODUCT_BUNDLE_IDENTIFIER']
798
841
 
799
- # puts "[DEBUG] Profile Specifier: #{profile_specifier}"
842
+ # 跳过无效的 Bundle ID(空值、未解析变量、通配符)
843
+ next if bundle_id.nil? || bundle_id.to_s.empty?
844
+ next if bundle_id.to_s.include?('$(') || bundle_id.to_s.include?('*')
800
845
 
801
- # # 跳过没有 provisioning profile 的 target
802
- # next if profile_specifier.nil? || profile_specifier.to_s.empty?
846
+ # 优先读取 SDK 特定的配置,再回退到通用配置
847
+ profile_specifier = release_config.build_settings['PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]']
848
+ profile_specifier ||= release_config.build_settings['PROVISIONING_PROFILE_SPECIFIER']
803
849
 
804
- # # 添加到 provisioning profiles 映射
805
- # provisioning_profiles[bundle_id.to_s] = profile_specifier.to_s
806
- # puts "[DEBUG] Added: #{bundle_id} => #{profile_specifier}"
807
- # end
850
+ next if profile_specifier.nil? || profile_specifier.to_s.empty?
808
851
 
809
- # puts "[DEBUG] Total provisioning profiles: #{provisioning_profiles.inspect}"
852
+ provisioning_profiles[bundle_id.to_s] = profile_specifier.to_s
853
+ end
810
854
 
811
- # # 只有在找到 provisioning profiles 时才添加配置
812
- # if !provisioning_profiles.empty?
813
- # values[:export_options][:provisioningProfiles] = provisioning_profiles
814
- # puts "[DEBUG] Setting export_options[:provisioningProfiles] = #{provisioning_profiles.inspect}"
815
- # end
855
+ if !provisioning_profiles.empty?
856
+ values[:export_options][:provisioningProfiles] = provisioning_profiles
857
+ end
816
858
 
817
859
  values
818
860
  end
data/lib/pindo/version.rb CHANGED
@@ -6,7 +6,7 @@ require 'time'
6
6
 
7
7
  module Pindo
8
8
 
9
- VERSION = "5.18.9"
9
+ VERSION = "5.18.11"
10
10
 
11
11
  class VersionCheck
12
12
  RUBYGEMS_API = 'https://rubygems.org/api/v1/gems/pindo.json'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pindo
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.18.9
4
+ version: 5.18.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - wade
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: claide
@@ -524,7 +524,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
524
524
  - !ruby/object:Gem::Version
525
525
  version: 3.0.0
526
526
  requirements: []
527
- rubygems_version: 3.6.3
527
+ rubygems_version: 4.0.3
528
528
  specification_version: 4
529
529
  summary: easy work
530
530
  test_files: []