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 +4 -4
- 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/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78cfff9c32f28c301552fdf5d7fbbe71c71b0ed6e8ec3ea178fdcb985cf07677
|
|
4
|
+
data.tar.gz: d96bf95c17ae486d94745466add5f521ff087bbb133ea36a24f17fcb2f2421cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
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}")
|
|
@@ -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
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
#
|
|
767
|
-
#
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
786
|
-
|
|
836
|
+
release_config = target.build_configurations.find { |config| config.name == 'Release' } || target.build_configurations.first
|
|
837
|
+
next if release_config.nil?
|
|
787
838
|
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
802
|
-
|
|
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
|
-
|
|
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
|
-
|
|
852
|
+
provisioning_profiles[bundle_id.to_s] = profile_specifier.to_s
|
|
853
|
+
end
|
|
810
854
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
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.
|
|
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:
|
|
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:
|
|
527
|
+
rubygems_version: 4.0.3
|
|
528
528
|
specification_version: 4
|
|
529
529
|
summary: easy work
|
|
530
530
|
test_files: []
|