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.
- checksums.yaml +4 -4
- data/lib/pindo/command/android/install.rb +277 -0
- data/lib/pindo/command/android.rb +1 -0
- data/lib/pindo/command/ios/install.rb +241 -0
- data/lib/pindo/command/ios.rb +1 -0
- data/lib/pindo/module/android/android_build_helper.rb +2 -0
- data/lib/pindo/module/android/android_project_helper.rb +344 -59
- data/lib/pindo/module/cert/mode/base_cert_operator.rb +68 -0
- data/lib/pindo/module/cert/mode/custom_https_cert_operator.rb +33 -0
- data/lib/pindo/module/task/model/unity/unity_export_task.rb +4 -5
- data/lib/pindo/module/utils/git_repo_helper.rb +6 -3
- data/lib/pindo/module/xcode/xcode_build_helper.rb +97 -55
- data/lib/pindo/options/groups/tool_options.rb +18 -0
- data/lib/pindo/version.rb +1 -1
- metadata +5 -3
|
@@ -5,90 +5,115 @@ module Pindo
|
|
|
5
5
|
class AndroidProjectHelper
|
|
6
6
|
class << self
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
#
|
|
8
|
+
# 永久修复 Unity IL2CPP 工程:将 mergeDebug/mergeReleaseJniLibFolders 对 BuildIl2CppTask 的依赖
|
|
9
|
+
# 从“硬编码 buildType”改为“匹配所有 merge*JniLibFolders 变体”,避免自定义 buildType(如 workflow)
|
|
10
|
+
# 或 Unity/AGP 版本差异导致漏掉依赖关系。
|
|
10
11
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# 当下游流程只携带“导出目录”时,这会表现为 Firebase AAR “缺失”。
|
|
12
|
+
# 仅修改 Unity 导出工程(包含 unityLibrary/build.gradle[.kts])中的 unityLibrary 模块 Gradle 文件。
|
|
13
|
+
# 幂等:已注入则不会重复写入。
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
#
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
95
|
+
FileUtils.cp(src, dst)
|
|
96
|
+
satisfied << base
|
|
97
|
+
end
|
|
64
98
|
|
|
65
|
-
|
|
99
|
+
unless missing_after_copy.empty?
|
|
66
100
|
raise Informative, <<~MSG
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
170
|
-
# 以便下游仅携带导出目录时仍能正常集成 Firebase。
|
|
169
|
+
# 按导出 unityLibrary/build.gradle[.kts] 声明的本地 AAR,补齐 unityLibrary/libs(与 Resolver / GeneratedLocalRepo 对齐)。
|
|
171
170
|
begin
|
|
172
|
-
copied = Pindo::AndroidProjectHelper.
|
|
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 "✓
|
|
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, "同步
|
|
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}
|
|
167
|
-
|
|
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}")
|