pindo 5.18.6 → 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 +4 -4
- data/lib/pindo/command/android/autobuild.rb +15 -2
- data/lib/pindo/command/unity/autobuild.rb +1 -1
- data/lib/pindo/command/utils/gitconfig.rb +268 -0
- data/lib/pindo/command/utils.rb +1 -0
- data/lib/pindo/module/android/android_build_helper.rb +111 -69
- data/lib/pindo/module/android/android_project_helper.rb +373 -0
- data/lib/pindo/module/android/gradle_helper.rb +27 -2
- data/lib/pindo/module/android/keystore_helper.rb +99 -30
- data/lib/pindo/module/android/workflow_gradle_injector.rb +82 -6
- data/lib/pindo/module/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/git/git_commit_task.rb +6 -10
- data/lib/pindo/module/task/model/unity/unity_export_task.rb +13 -0
- data/lib/pindo/module/utils/git_hook_helper.rb +284 -0
- 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/build_options.rb +0 -33
- data/lib/pindo/version.rb +1 -1
- metadata +2 -1
|
@@ -1,7 +1,120 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "pathname"
|
|
3
|
+
|
|
1
4
|
module Pindo
|
|
2
5
|
class AndroidProjectHelper
|
|
3
6
|
class << self
|
|
4
7
|
|
|
8
|
+
# 永久修复 Unity IL2CPP 工程:将 mergeDebug/mergeReleaseJniLibFolders 对 BuildIl2CppTask 的依赖
|
|
9
|
+
# 从“硬编码 buildType”改为“匹配所有 merge*JniLibFolders 变体”,避免自定义 buildType(如 workflow)
|
|
10
|
+
# 或 Unity/AGP 版本差异导致漏掉依赖关系。
|
|
11
|
+
#
|
|
12
|
+
# 仅修改 Unity 导出工程(包含 unityLibrary/build.gradle[.kts])中的 unityLibrary 模块 Gradle 文件。
|
|
13
|
+
# 幂等:已注入则不会重复写入。
|
|
14
|
+
#
|
|
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`。
|
|
45
|
+
#
|
|
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:)
|
|
52
|
+
raise ArgumentError, "unity_root_path 不能为空" if unity_root_path.to_s.empty?
|
|
53
|
+
raise ArgumentError, "export_path 不能为空" if export_path.to_s.empty?
|
|
54
|
+
raise ArgumentError, "Unity 工程目录不存在: #{unity_root_path}" unless File.directory?(unity_root_path)
|
|
55
|
+
raise ArgumentError, "导出目录不存在: #{export_path}" unless File.directory?(export_path)
|
|
56
|
+
|
|
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")
|
|
60
|
+
FileUtils.mkdir_p(libs_dir)
|
|
61
|
+
|
|
62
|
+
unless gradle_path
|
|
63
|
+
return []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
gradle_content = File.read(gradle_path, encoding: "UTF-8")
|
|
67
|
+
resolver_xml = File.join(unity_root_path, "ProjectSettings", "AndroidResolverDependencies.xml")
|
|
68
|
+
aar_index = build_android_resolver_aar_index(unity_root_path, resolver_xml)
|
|
69
|
+
|
|
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)
|
|
75
|
+
end
|
|
76
|
+
|
|
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
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
FileUtils.cp(src, dst)
|
|
96
|
+
satisfied << base
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
unless missing_after_copy.empty?
|
|
100
|
+
raise Informative, <<~MSG
|
|
101
|
+
Unity unityLibrary/libs 缺少 Gradle 已声明的 AAR,且无法在 Unity 工程内找到源文件:
|
|
102
|
+
#{missing_after_copy.sort.join(', ')}
|
|
103
|
+
Unity 工程: #{unity_root_path}
|
|
104
|
+
导出目录: #{export_path}
|
|
105
|
+
Gradle: #{gradle_path}
|
|
106
|
+
请检查: #{resolver_xml}
|
|
107
|
+
并在 Unity 中执行:External Dependency Manager → Android Resolver → Force Resolve 后重新导出。
|
|
108
|
+
MSG
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
satisfied.uniq
|
|
112
|
+
end
|
|
113
|
+
|
|
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)
|
|
117
|
+
end
|
|
5
118
|
|
|
6
119
|
def unity_android_project?(project_path)
|
|
7
120
|
# 检查 unityLibrary 模块是否存在
|
|
@@ -349,6 +462,266 @@ module Pindo
|
|
|
349
462
|
|
|
350
463
|
private
|
|
351
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
|
+
|
|
352
725
|
def extract_modules_from_settings(content)
|
|
353
726
|
modules = []
|
|
354
727
|
# 兼容 include ':app' / include(":app", ":lib")
|
|
@@ -205,7 +205,9 @@ module Pindo
|
|
|
205
205
|
end
|
|
206
206
|
|
|
207
207
|
# 生成 Wrapper
|
|
208
|
-
generate_gradle_wrapper(gradle_dir, gradle_version)
|
|
208
|
+
ok = generate_gradle_wrapper(gradle_dir, gradle_version)
|
|
209
|
+
ensure_gradlew_runnable!(gradle_dir)
|
|
210
|
+
ok
|
|
209
211
|
end
|
|
210
212
|
|
|
211
213
|
# 解析gradle版本
|
|
@@ -244,7 +246,30 @@ module Pindo
|
|
|
244
246
|
end
|
|
245
247
|
|
|
246
248
|
# 生成 Wrapper
|
|
247
|
-
generate_gradle_wrapper(gradle_dir, gradle_version)
|
|
249
|
+
ok = generate_gradle_wrapper(gradle_dir, gradle_version)
|
|
250
|
+
ensure_gradlew_runnable!(gradle_dir)
|
|
251
|
+
ok
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# 确保工程根目录下 gradlew 可被当前环境执行:补齐执行位;在 macOS 上尝试移除
|
|
255
|
+
# com.apple.quarantine(无该属性时 xattr 失败可忽略)。用于避免
|
|
256
|
+
# 「bad interpreter: Operation not permitted」类错误,无用户交互。
|
|
257
|
+
def ensure_gradlew_runnable!(gradle_dir)
|
|
258
|
+
return if gradle_dir.nil? || gradle_dir.to_s.empty?
|
|
259
|
+
|
|
260
|
+
gradlew = File.join(gradle_dir, "gradlew")
|
|
261
|
+
return unless File.file?(gradlew)
|
|
262
|
+
|
|
263
|
+
begin
|
|
264
|
+
st = File.stat(gradlew)
|
|
265
|
+
File.chmod(st.mode | 0o111, gradlew)
|
|
266
|
+
rescue StandardError
|
|
267
|
+
# 只读卷等场景下忽略
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
return unless RUBY_PLATFORM.match?(/darwin/i)
|
|
271
|
+
|
|
272
|
+
system("xattr", "-d", "com.apple.quarantine", gradlew, out: File::NULL, err: File::NULL)
|
|
248
273
|
end
|
|
249
274
|
|
|
250
275
|
# =================== 私有辅助方法 ===================
|
|
@@ -122,14 +122,34 @@ module Pindo
|
|
|
122
122
|
@pindo_managed_signing_paths = []
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# 打包结束后恢复本次被修改的 Gradle 签名配置(仅恢复被本次任务改动过的文件)
|
|
126
|
+
def restore_managed_signing_config!
|
|
127
|
+
backups = @pindo_managed_gradle_backups || {}
|
|
128
|
+
backups.each do |path, original_content|
|
|
129
|
+
next if path.nil? || path.to_s.empty?
|
|
130
|
+
next unless original_content.is_a?(String)
|
|
131
|
+
next unless File.exist?(path)
|
|
132
|
+
|
|
133
|
+
File.write(path, original_content)
|
|
134
|
+
end
|
|
135
|
+
ensure
|
|
136
|
+
@pindo_managed_gradle_backups = {}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# 清理本次写入到进程 ENV 的签名变量(避免后续任务误继承)
|
|
140
|
+
def cleanup_managed_signing_env!
|
|
141
|
+
keys = @pindo_managed_env_keys || []
|
|
142
|
+
keys.each { |k| ENV.delete(k.to_s) if k }
|
|
143
|
+
ensure
|
|
144
|
+
@pindo_managed_env_keys = []
|
|
145
|
+
end
|
|
146
|
+
|
|
125
147
|
# 将签名配置应用到 Android 工程
|
|
126
148
|
#
|
|
127
149
|
# 默认(与工程手写的 RELEASE_* 约定一致):只拉取 JPS、拷贝 jks 到项目 signing/,并设置当前进程
|
|
128
150
|
# RELEASE_KEYSTORE_PATH / RELEASE_KEYSTORE_PASSWORD / RELEASE_KEY_ALIAS / RELEASE_KEY_PASSWORD,不修改 build.gradle。
|
|
129
151
|
# RELEASE_KEYSTORE_PATH 使用 jks 的绝对路径,便于 app 子模块内 `file(System.getenv(...))` 引用。
|
|
130
152
|
#
|
|
131
|
-
# 若需恢复自动改写 signingConfigs,设置环境变量:PINDO_INJECT_ANDROID_SIGNING_GRADLE=1
|
|
132
|
-
#
|
|
133
153
|
# @param project_dir [String] 项目目录
|
|
134
154
|
# @param build_type [String] 构建类型 "debug" 或 "release"
|
|
135
155
|
# @return [Boolean] 是否成功
|
|
@@ -139,6 +159,8 @@ module Pindo
|
|
|
139
159
|
raise ArgumentError, "bundle_id 不能为空" if bundle_id.blank?
|
|
140
160
|
|
|
141
161
|
reset_managed_signing_paths!
|
|
162
|
+
reset_managed_gradle_backups!
|
|
163
|
+
reset_managed_env_keys!
|
|
142
164
|
|
|
143
165
|
main_module = Pindo::AndroidProjectHelper.get_main_module(project_dir)
|
|
144
166
|
unless main_module
|
|
@@ -170,19 +192,16 @@ module Pindo
|
|
|
170
192
|
raise "JPS keystore 未拷贝到工程 signing/(无法解析路径),Gradle 将回退到 build.gradle 中的本地 jks;请检查 JPS 证书是否下载成功"
|
|
171
193
|
end
|
|
172
194
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
inject_gradle = Pindo::Options::GlobalOptionsState.instance[:injectsigning]
|
|
176
|
-
if inject_gradle
|
|
177
|
-
if gradle_file.end_with?(".kts")
|
|
178
|
-
ensure_keystore_config_kts(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
|
|
179
|
-
else
|
|
180
|
-
ensure_keystore_config_groovy(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
|
|
181
|
-
end
|
|
182
|
-
export_jps_release_signing_env!(cfg, keystore_path_for_env: rel_plain)
|
|
195
|
+
if gradle_file.end_with?(".kts")
|
|
196
|
+
ensure_keystore_config_kts(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
|
|
183
197
|
else
|
|
184
|
-
|
|
198
|
+
ensure_keystore_config_groovy(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
|
|
185
199
|
end
|
|
200
|
+
# 注意:部分 Unity 导出工程/多模块工程的 Gradle rootProject 目录可能不是 project_dir,
|
|
201
|
+
# 相对路径(如 signing/xxx.jks)会被解析到错误的模块目录下(如 launcher/signing/...)。
|
|
202
|
+
# 这里改为导出绝对路径,确保与 keystore 创建/拷贝目录保持一致。
|
|
203
|
+
keystore_abs = File.expand_path(File.join(project_dir, rel_plain))
|
|
204
|
+
export_jps_release_signing_env!(cfg, keystore_path_for_env: keystore_abs)
|
|
186
205
|
true
|
|
187
206
|
end
|
|
188
207
|
|
|
@@ -244,6 +263,14 @@ module Pindo
|
|
|
244
263
|
@pindo_managed_signing_paths = []
|
|
245
264
|
end
|
|
246
265
|
|
|
266
|
+
def reset_managed_gradle_backups!
|
|
267
|
+
@pindo_managed_gradle_backups = {}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def reset_managed_env_keys!
|
|
271
|
+
@pindo_managed_env_keys = []
|
|
272
|
+
end
|
|
273
|
+
|
|
247
274
|
def register_managed_signing_path!(path)
|
|
248
275
|
return if path.nil? || path.to_s.empty?
|
|
249
276
|
|
|
@@ -251,6 +278,23 @@ module Pindo
|
|
|
251
278
|
@pindo_managed_signing_paths << path
|
|
252
279
|
end
|
|
253
280
|
|
|
281
|
+
def register_managed_gradle_backup!(path, original_content)
|
|
282
|
+
return if path.nil? || path.to_s.empty?
|
|
283
|
+
return unless original_content.is_a?(String)
|
|
284
|
+
|
|
285
|
+
@pindo_managed_gradle_backups ||= {}
|
|
286
|
+
# 同一文件只备份一次:第一次写入前的内容即为“原始内容”
|
|
287
|
+
@pindo_managed_gradle_backups[path] ||= original_content
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def register_managed_env_key!(key)
|
|
291
|
+
return if key.nil? || key.to_s.empty?
|
|
292
|
+
|
|
293
|
+
@pindo_managed_env_keys ||= []
|
|
294
|
+
@pindo_managed_env_keys << key.to_s
|
|
295
|
+
@pindo_managed_env_keys.uniq!
|
|
296
|
+
end
|
|
297
|
+
|
|
254
298
|
# 将 JPS 返回的路径/口令/别名写入当前进程环境变量,供 Gradle / bundletool 子进程继承(与 build.gradle 中 RELEASE_* 名称一致)
|
|
255
299
|
def export_jps_release_signing_env!(cfg, keystore_path_for_env:)
|
|
256
300
|
return unless cfg.is_a?(Hash)
|
|
@@ -258,6 +302,7 @@ module Pindo
|
|
|
258
302
|
if keystore_path_for_env && !keystore_path_for_env.to_s.empty?
|
|
259
303
|
ENV[ENV_RELEASE_KEYSTORE_PATH] =
|
|
260
304
|
keystore_path_for_env
|
|
305
|
+
register_managed_env_key!(ENV_RELEASE_KEYSTORE_PATH)
|
|
261
306
|
end
|
|
262
307
|
|
|
263
308
|
sp = cfg["storePassword"]
|
|
@@ -270,6 +315,10 @@ module Pindo
|
|
|
270
315
|
ENV[ENV_RELEASE_KEYSTORE_PASSWORD] = sp_plain.to_s if sp_plain && !sp_plain.to_s.empty?
|
|
271
316
|
ENV[ENV_RELEASE_KEY_PASSWORD] = kp_plain.to_s if kp_plain && !kp_plain.to_s.empty?
|
|
272
317
|
ENV[ENV_RELEASE_KEY_ALIAS] = ka.to_s if ka && !ka.to_s.empty?
|
|
318
|
+
|
|
319
|
+
register_managed_env_key!(ENV_RELEASE_KEYSTORE_PASSWORD) if sp_plain && !sp_plain.to_s.empty?
|
|
320
|
+
register_managed_env_key!(ENV_RELEASE_KEY_PASSWORD) if kp_plain && !kp_plain.to_s.empty?
|
|
321
|
+
register_managed_env_key!(ENV_RELEASE_KEY_ALIAS) if ka && !ka.to_s.empty?
|
|
273
322
|
end
|
|
274
323
|
|
|
275
324
|
# Groovy:解析 storePassword/keyPassword(支持 System.getenv、可选 ?: 回退、或历史明文)
|
|
@@ -517,6 +566,7 @@ module Pindo
|
|
|
517
566
|
if content == original_content
|
|
518
567
|
puts " ✓ build.gradle 无需修改"
|
|
519
568
|
else
|
|
569
|
+
register_managed_gradle_backup!(gradle_file, original_content)
|
|
520
570
|
File.write(gradle_file, content)
|
|
521
571
|
puts " ✓ build.gradle 已更新"
|
|
522
572
|
end
|
|
@@ -571,6 +621,7 @@ module Pindo
|
|
|
571
621
|
if content == original_content
|
|
572
622
|
puts " ✓ build.gradle.kts 无需修改"
|
|
573
623
|
else
|
|
624
|
+
register_managed_gradle_backup!(gradle_file, original_content)
|
|
574
625
|
File.write(gradle_file, content)
|
|
575
626
|
puts " ✓ build.gradle.kts 已更新"
|
|
576
627
|
end
|
|
@@ -1285,34 +1336,52 @@ module Pindo
|
|
|
1285
1336
|
|
|
1286
1337
|
# =================== 写入 keystore 配置的辅助方法 ===================
|
|
1287
1338
|
|
|
1288
|
-
# 生成签名配置代码块(Groovy
|
|
1339
|
+
# 生成签名配置代码块(Groovy)
|
|
1340
|
+
# 注意:签名信息只允许从环境变量读取,严禁在 build.gradle 中写入任何明文(包括回退密码/alias/路径)。
|
|
1289
1341
|
def generate_signing_config_groovy(config_name, pindo_config)
|
|
1290
|
-
rel_fb = escape_gradle_double_quoted(signing_keystore_fallback_relative(pindo_config))
|
|
1291
|
-
alias_fb = escape_gradle_double_quoted(pindo_config["keyAlias"])
|
|
1292
|
-
|
|
1293
1342
|
<<~GROOVY
|
|
1294
1343
|
#{config_name} {
|
|
1295
|
-
def
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1344
|
+
def signingEnvVars = ["#{ENV_RELEASE_KEYSTORE_PATH}", "#{ENV_RELEASE_KEYSTORE_PASSWORD}", "#{ENV_RELEASE_KEY_ALIAS}", "#{ENV_RELEASE_KEY_PASSWORD}"]
|
|
1345
|
+
def missing = signingEnvVars.findAll { !System.getenv(it) }
|
|
1346
|
+
if (!missing.isEmpty()) {
|
|
1347
|
+
throw new GradleException("Missing required environment variables for release signing: ${missing.join(', ')}")
|
|
1348
|
+
}
|
|
1349
|
+
def keystorePath = System.getenv("#{ENV_RELEASE_KEYSTORE_PATH}")
|
|
1350
|
+
def keystoreFile = rootProject.file(keystorePath)
|
|
1351
|
+
if (!keystoreFile.exists()) {
|
|
1352
|
+
throw new GradleException("Keystore file not found: ${keystoreFile.absolutePath}")
|
|
1353
|
+
}
|
|
1354
|
+
storeFile keystoreFile
|
|
1355
|
+
storePassword System.getenv("#{ENV_RELEASE_KEYSTORE_PASSWORD}")
|
|
1356
|
+
keyAlias System.getenv("#{ENV_RELEASE_KEY_ALIAS}")
|
|
1357
|
+
keyPassword System.getenv("#{ENV_RELEASE_KEY_PASSWORD}")
|
|
1300
1358
|
}
|
|
1301
1359
|
GROOVY
|
|
1302
1360
|
end
|
|
1303
1361
|
|
|
1304
1362
|
# 生成签名配置代码块(Kotlin DSL)
|
|
1305
1363
|
def generate_signing_config_kts(config_name, pindo_config)
|
|
1306
|
-
rel_fb = escape_gradle_double_quoted(signing_keystore_fallback_relative(pindo_config))
|
|
1307
|
-
alias_fb = escape_gradle_double_quoted(pindo_config["keyAlias"])
|
|
1308
|
-
|
|
1309
1364
|
<<~KOTLIN
|
|
1310
1365
|
create("#{config_name}") {
|
|
1311
|
-
val
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1366
|
+
val signingEnvVars = listOf(
|
|
1367
|
+
"#{ENV_RELEASE_KEYSTORE_PATH}",
|
|
1368
|
+
"#{ENV_RELEASE_KEYSTORE_PASSWORD}",
|
|
1369
|
+
"#{ENV_RELEASE_KEY_ALIAS}",
|
|
1370
|
+
"#{ENV_RELEASE_KEY_PASSWORD}",
|
|
1371
|
+
)
|
|
1372
|
+
val missing = signingEnvVars.filter { System.getenv(it).isNullOrBlank() }
|
|
1373
|
+
if (missing.isNotEmpty()) {
|
|
1374
|
+
throw GradleException("Missing required environment variables for release signing: ${missing.joinToString(\", \")}")
|
|
1375
|
+
}
|
|
1376
|
+
val keystorePath = System.getenv("#{ENV_RELEASE_KEYSTORE_PATH}")
|
|
1377
|
+
val keystoreFile = rootProject.file(keystorePath)
|
|
1378
|
+
if (!keystoreFile.exists()) {
|
|
1379
|
+
throw GradleException("Keystore file not found: ${keystoreFile.absolutePath}")
|
|
1380
|
+
}
|
|
1381
|
+
storeFile = keystoreFile
|
|
1382
|
+
storePassword = System.getenv("#{ENV_RELEASE_KEYSTORE_PASSWORD}")
|
|
1383
|
+
keyAlias = System.getenv("#{ENV_RELEASE_KEY_ALIAS}")
|
|
1384
|
+
keyPassword = System.getenv("#{ENV_RELEASE_KEY_PASSWORD}")
|
|
1316
1385
|
}
|
|
1317
1386
|
KOTLIN
|
|
1318
1387
|
end
|