pindo 5.17.4 → 5.18.3

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/git_handler.rb +120 -38
  3. data/lib/pindo/command/android/autobuild.rb +92 -31
  4. data/lib/pindo/command/appstore/adhocbuild.rb +1 -1
  5. data/lib/pindo/command/appstore/autobuild.rb +1 -1
  6. data/lib/pindo/command/appstore/autoresign.rb +1 -1
  7. data/lib/pindo/command/appstore/updateid.rb +229 -0
  8. data/lib/pindo/command/appstore.rb +1 -0
  9. data/lib/pindo/command/ios/autobuild.rb +70 -33
  10. data/lib/pindo/command/ios/podpush.rb +1 -1
  11. data/lib/pindo/command/unity/autobuild.rb +38 -18
  12. data/lib/pindo/command/utils/allcopyconfig.rb +144 -0
  13. data/lib/pindo/command/utils/copyconfig.rb +207 -0
  14. data/lib/pindo/command/utils/icon.rb +2 -2
  15. data/lib/pindo/command/utils/renewbundleid.rb +199 -0
  16. data/lib/pindo/command/utils/renewcert.rb +56 -54
  17. data/lib/pindo/command/utils.rb +3 -0
  18. data/lib/pindo/command/web/autobuild.rb +10 -8
  19. data/lib/pindo/config/build_info_manager.rb +1 -3
  20. data/lib/pindo/module/android/android_build_helper.rb +198 -33
  21. data/lib/pindo/module/android/android_config_helper.rb +305 -88
  22. data/lib/pindo/module/android/android_project_helper.rb +124 -14
  23. data/lib/pindo/module/android/android_res_helper.rb +349 -51
  24. data/lib/pindo/module/android/keystore_helper.rb +611 -295
  25. data/lib/pindo/module/android/workflow_gradle_injector.rb +702 -0
  26. data/lib/pindo/module/appselect.rb +4 -4
  27. data/lib/pindo/module/appstore/bundleid_helper.rb +204 -14
  28. data/lib/pindo/module/build/build_helper.rb +76 -10
  29. data/lib/pindo/module/build/git_repo_helper.rb +4 -4
  30. data/lib/pindo/module/cert/mode/base_cert_operator.rb +12 -6
  31. data/lib/pindo/module/pgyer/pgyerhelper.rb +124 -39
  32. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +64 -6
  33. data/lib/pindo/module/task/model/git/git_commit_task.rb +70 -54
  34. data/lib/pindo/module/task/model/git/git_tag_task.rb +13 -9
  35. data/lib/pindo/module/task/model/jps/jps_upload_task.rb +110 -3
  36. data/lib/pindo/module/task/model/unity/unity_export_task.rb +2 -1
  37. data/lib/pindo/module/task/model/unity/unity_update_task.rb +2 -1
  38. data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +2 -1
  39. data/lib/pindo/module/task/model/unity_task.rb +2 -1
  40. data/lib/pindo/module/unity/unity_helper.rb +13 -10
  41. data/lib/pindo/module/unity/unity_proc_helper.rb +27 -2
  42. data/lib/pindo/module/xcode/applovin_xcode_helper.rb +6 -2
  43. data/lib/pindo/module/xcode/res/xcode_res_constant.rb +72 -0
  44. data/lib/pindo/module/xcode/res/xcode_res_handler.rb +3 -3
  45. data/lib/pindo/module/xcode/xcode_build_config.rb +46 -17
  46. data/lib/pindo/module/xcode/xcode_build_helper.rb +186 -25
  47. data/lib/pindo/module/xcode/xcode_project_helper.rb +1 -1
  48. data/lib/pindo/module/xcode/xcode_res_helper.rb +32 -16
  49. data/lib/pindo/options/groups/build_options.rb +5 -5
  50. data/lib/pindo/options/groups/git_options.rb +7 -5
  51. data/lib/pindo/options/groups/unity_options.rb +11 -0
  52. data/lib/pindo/options/helpers/bundleid_selector.rb +25 -0
  53. data/lib/pindo/options/helpers/git_constants.rb +7 -6
  54. data/lib/pindo/version.rb +3 -3
  55. metadata +12 -7
@@ -1,5 +1,6 @@
1
1
  require 'fileutils'
2
2
  require 'open-uri'
3
+ require 'json'
3
4
  require_relative '../../base/funlog'
4
5
  require_relative 'android_project_helper'
5
6
  require_relative '../../base/executable'
@@ -431,6 +432,105 @@ module Pindo
431
432
  end
432
433
  end
433
434
 
435
+ # 从 Android 导出工程路径推断 Unity 工程根目录(GoodPlatform/BaseAndroid 布局)
436
+ # @param export_project_dir [String] 例如 GoodPlatform/BaseAndroid
437
+ # @return [String, nil] Unity 根目录绝对路径,无法推断时返回 nil
438
+ def self.infer_unity_project_root_from_export_dir(export_project_dir)
439
+ return nil if export_project_dir.nil? || !File.directory?(export_project_dir)
440
+
441
+ abs = File.expand_path(export_project_dir)
442
+ base = File.basename(abs)
443
+ parent = File.basename(File.dirname(abs))
444
+ if base == 'BaseAndroid' && parent == 'GoodPlatform'
445
+ return File.expand_path('../..', abs)
446
+ end
447
+
448
+ dir = abs
449
+ 8.times do
450
+ if File.directory?(File.join(dir, 'Assets')) &&
451
+ File.directory?(File.join(dir, 'ProjectSettings'))
452
+ return dir
453
+ end
454
+ parent_dir = File.dirname(dir)
455
+ break if parent_dir == dir
456
+
457
+ dir = parent_dir
458
+ end
459
+ nil
460
+ end
461
+
462
+ # Unity 工程中 Firebase Android 配置(Editor 与 Gradle 均可能读取)
463
+ # @param unity_root [String] Unity 工程根目录
464
+ # @return [String]
465
+ def self.unity_assets_firebase_google_services_path(unity_root)
466
+ File.join(unity_root, 'Assets', 'Scripts', 'Firebase', 'google-services.json')
467
+ end
468
+
469
+ # Unity Editor 侧 Firebase 可能读取的桌面配置(与 Assets/Firebase 下 json 宜保持一致)
470
+ # @param unity_root [String] Unity 工程根目录
471
+ # @return [String]
472
+ def self.unity_streaming_assets_google_services_desktop_path(unity_root)
473
+ File.join(unity_root, 'Assets', 'StreamingAssets', 'google-services-desktop.json')
474
+ end
475
+
476
+ # @param clients [Array]
477
+ # @param package_name [String]
478
+ # @return [Boolean]
479
+ def self.google_services_has_android_package?(clients, package_name)
480
+ return false unless clients.is_a?(Array)
481
+
482
+ clients.any? do |c|
483
+ next false unless c.is_a?(Hash)
484
+ ci = c['client_info']
485
+ next false unless ci.is_a?(Hash)
486
+ aci = ci['android_client_info']
487
+ aci.is_a?(Hash) && aci['package_name'] == package_name
488
+ end
489
+ end
490
+
491
+ # 取首个含 android_client_info.package_name 的 client,作为追加新包名时的模板
492
+ # @param clients [Array]
493
+ # @return [Hash, nil]
494
+ def self.google_services_first_android_client(clients)
495
+ return nil unless clients.is_a?(Array)
496
+
497
+ clients.each do |c|
498
+ next unless c.is_a?(Hash)
499
+ ci = c['client_info']
500
+ next unless ci.is_a?(Hash)
501
+ aci = ci['android_client_info']
502
+ if aci.is_a?(Hash) && aci['package_name'].is_a?(String) && !aci['package_name'].empty?
503
+ return c
504
+ end
505
+ end
506
+ nil
507
+ end
508
+
509
+ # 不修改已有 client;若尚无目标 package_name,则深拷贝模板 client 并追加
510
+ # @param json [Hash]
511
+ # @param package_name [String]
512
+ # @return [Boolean] 是否追加了新 client
513
+ def self.google_services_merge_android_package!(json, package_name)
514
+ return false unless json.is_a?(Hash)
515
+
516
+ clients = json['client']
517
+ return false unless clients.is_a?(Array) && !clients.empty?
518
+
519
+ return false if google_services_has_android_package?(clients, package_name)
520
+
521
+ template = google_services_first_android_client(clients)
522
+ unless template
523
+ return false
524
+ end
525
+
526
+ new_client = JSON.parse(JSON.generate(template))
527
+ new_client['client_info'] ||= {}
528
+ new_client['client_info']['android_client_info'] ||= {}
529
+ new_client['client_info']['android_client_info']['package_name'] = package_name
530
+ clients << new_client
531
+ true
532
+ end
533
+
434
534
  # 从配置仓库拷贝 google-services.json 到项目的 launcher 目录
435
535
  # @param config_repo_dir [String] 配置仓库的路径
436
536
  # @param project_dir [String] Android项目目录路径
@@ -493,9 +593,94 @@ module Pindo
493
593
  puts " ⚠ 请确认文件位置是否正确"
494
594
  end
495
595
 
596
+ # Unity Editor(Firebase.Editor)也会读取 Assets/Scripts/Firebase/google-services.json
597
+ unity_root = infer_unity_project_root_from_export_dir(project_dir)
598
+ if unity_root && File.directory?(File.join(unity_root, 'Assets'))
599
+ assets_firebase_dir = File.join(unity_root, 'Assets', 'Scripts', 'Firebase')
600
+ FileUtils.mkdir_p(assets_firebase_dir)
601
+ unity_gs = File.join(assets_firebase_dir, 'google-services.json')
602
+ FileUtils.cp(source_file, unity_gs)
603
+ puts " ✓ google-services.json 已拷贝到: #{unity_gs}"
604
+ end
605
+
496
606
  return true
497
607
  end
498
608
 
609
+ # 与 update_google_services_package_name 相同的候选路径(不一定存在)
610
+ def self.google_services_json_candidate_paths(project_dir)
611
+ candidates = [
612
+ File.join(project_dir, 'unityLibrary', 'launcher', 'google-services.json'),
613
+ File.join(project_dir, 'unityLibrary', 'google-services.json'),
614
+ File.join(project_dir, 'launcher', 'google-services.json'),
615
+ File.join(project_dir, 'app', 'google-services.json'),
616
+ File.join(project_dir, 'google-services.json')
617
+ ]
618
+ unity_root = infer_unity_project_root_from_export_dir(project_dir)
619
+ if unity_root
620
+ candidates << unity_assets_firebase_google_services_path(unity_root)
621
+ candidates << unity_streaming_assets_google_services_desktop_path(unity_root)
622
+ end
623
+ candidates.uniq
624
+ end
625
+
626
+ # @param project_dir [String]
627
+ # @return [Array<String>] 项目内已存在的 google-services.json 绝对路径
628
+ def self.existing_google_services_json_paths(project_dir)
629
+ raise ArgumentError, "项目目录不能为空" if project_dir.nil?
630
+ raise ArgumentError, "项目路径无效: #{project_dir}" unless File.directory?(project_dir)
631
+
632
+ google_services_json_candidate_paths(project_dir).select { |p| File.exist?(p) }
633
+ end
634
+
635
+ # 在 google-services.json 的 client 数组中确保存在目标 Android package_name:不改写已有 client,
636
+ # 仅当尚不存在时深拷贝模板 client 并追加,避免切换 bundle id / applicationId 时 Firebase Unity SDK 反复弹窗。
637
+ # @param project_dir [String] Android项目目录路径
638
+ # @param package_name [String] 目标 applicationId(例如: com.heroneverdie101.fancyapptest)
639
+ # @return [Boolean] 是否至少在一个文件中新追加了 client(已存在目标包名则视为无需写入)
640
+ def self.update_google_services_package_name(project_dir: nil, package_name: nil)
641
+ raise ArgumentError, "项目目录不能为空" if project_dir.nil?
642
+ raise ArgumentError, "Package Name不能为空" if package_name.nil? || package_name.empty?
643
+ raise ArgumentError, "项目路径无效: #{project_dir}" unless File.directory?(project_dir)
644
+
645
+ target_files = existing_google_services_json_paths(project_dir)
646
+ if target_files.empty?
647
+ Funlog.instance.fancyinfo_warning("未找到 google-services.json,无法合并 package_name: #{package_name}")
648
+ return false
649
+ end
650
+
651
+ updated_any = false
652
+
653
+ target_files.each do |target_file|
654
+ begin
655
+ raw = File.read(target_file)
656
+ json = JSON.parse(raw)
657
+
658
+ unless json.is_a?(Hash) && json['client'].is_a?(Array) && !json['client'].empty?
659
+ Funlog.instance.fancyinfo_warning("google-services.json 缺少有效 client 数组,跳过: #{target_file}")
660
+ next
661
+ end
662
+
663
+ unless google_services_first_android_client(json['client'])
664
+ Funlog.instance.fancyinfo_warning("google-services.json 中无可用 Android client 模板,跳过: #{target_file}")
665
+ next
666
+ end
667
+
668
+ merged = google_services_merge_android_package!(json, package_name)
669
+ if merged
670
+ File.write(target_file, JSON.pretty_generate(json) + "\n")
671
+ puts " ✓ 已合并 #{File.basename(target_file)}:新增 client package_name #{package_name}(保留原有条目)"
672
+ updated_any = true
673
+ end
674
+ rescue JSON::ParserError
675
+ Funlog.instance.fancyinfo_warning("google-services.json 不是合法 JSON,跳过更新: #{target_file}")
676
+ rescue StandardError => e
677
+ Funlog.instance.fancyinfo_warning("更新 google-services.json 失败: #{e.message} (#{target_file})")
678
+ end
679
+ end
680
+
681
+ updated_any
682
+ end
683
+
499
684
  # 从配置仓库应用配置(拷贝 config.json)
500
685
  # @param config_repo_dir [String] 配置仓库的路径
501
686
  # @param project_dir [String] Android项目目录路径
@@ -661,13 +846,9 @@ module Pindo
661
846
  return true
662
847
  end
663
848
 
664
- # 尝试使用DOM添加
665
- success = add_scheme_with_dom(manifest_path, main_activity, android_prefix, scheme_name)
666
-
667
- unless success
668
- # DOM方法失败,尝试文本替换
669
- success = add_scheme_with_text_replace(manifest_path, scheme_name)
670
- end
849
+ # 为了避免 Nokogiri 重新序列化导致整体格式变化,优先使用文本方式局部插入。
850
+ activity_name = main_activity["name"] || main_activity["android:name"] || main_activity["#{android_prefix}:name"]
851
+ success = add_scheme_with_text_replace(manifest_path, scheme_name, activity_name: activity_name)
671
852
 
672
853
  unless success
673
854
  Funlog.instance.fancyinfo_error("无法添加scheme: #{scheme_name}")
@@ -772,33 +953,17 @@ module Pindo
772
953
  [scheme_exists, existing_scheme]
773
954
  end
774
955
 
775
- # 使用DOM操作添加scheme
776
- def self.add_scheme_with_dom(manifest_path, activity, android_prefix, scheme_name)
956
+ # 使用文本替换添加scheme
957
+ def self.add_scheme_with_text_replace(manifest_path, scheme_name, activity_name: nil)
777
958
  begin
778
- doc = Nokogiri::XML(File.read(manifest_path))
779
-
780
- # 创建intent-filter
781
- intent_filter = doc.create_element('intent-filter')
782
-
783
- # 添加子元素
784
- intent_filter.add_child(create_element(doc, 'action', "#{android_prefix}:name", 'android.intent.action.VIEW'))
785
- intent_filter.add_child(create_element(doc, 'category', "#{android_prefix}:name", 'android.intent.category.DEFAULT'))
786
- intent_filter.add_child(create_element(doc, 'category', "#{android_prefix}:name", 'android.intent.category.BROWSABLE'))
787
- intent_filter.add_child(create_element(doc, 'data', "#{android_prefix}:scheme", scheme_name))
788
-
789
- # 添加空白和缩进
790
- activity.add_child(doc.create_text_node("\n "))
791
- activity.add_child(intent_filter)
792
- activity.add_child(doc.create_text_node("\n "))
959
+ # 读取原始内容
960
+ xml_content = File.read(manifest_path)
793
961
 
794
- # 保存修改
795
- xml_content = doc.to_xml(indent: 2, encoding: 'UTF-8')
962
+ modified_xml = insert_scheme_intent_filter_before_activity_close(xml_content, scheme_name, activity_name: activity_name)
963
+ return false unless modified_xml
796
964
 
797
- # 验证修改是否成功
798
- if xml_content.include?("android:scheme=\"#{scheme_name}\"")
799
- File.write(manifest_path, xml_content)
800
- return true
801
- end
965
+ File.write(manifest_path, modified_xml)
966
+ return true
802
967
 
803
968
  return false
804
969
  rescue => e
@@ -806,75 +971,127 @@ module Pindo
806
971
  end
807
972
  end
808
973
 
809
- # 创建XML元素并设置属性
810
- def self.create_element(doc, name, attr_name, attr_value)
811
- element = doc.create_element(name)
812
- element[attr_name] = attr_value
813
- element
814
- end
815
-
816
- # 使用文本替换添加scheme
817
- def self.add_scheme_with_text_replace(manifest_path, scheme_name)
974
+ def self.update_existing_scheme(manifest_path, activity, android_prefix, existing_scheme, scheme_name)
818
975
  begin
819
- # 读取原始内容
820
976
  xml_content = File.read(manifest_path)
977
+ modified = update_scheme_value_in_place(xml_content, existing_scheme, scheme_name)
978
+ return false unless modified
821
979
 
822
- # 定义要添加的intent-filter
823
- scheme_intent_filter = %Q{
824
- <intent-filter>
825
- <action android:name="android.intent.action.VIEW"/>
826
- <category android:name="android.intent.category.DEFAULT"/>
827
- <category android:name="android.intent.category.BROWSABLE"/>
828
- <data android:scheme="#{scheme_name}"/>
829
- </intent-filter>}
830
-
831
- # 在</activity>前添加intent-filter
832
- if xml_content.match(/<\/activity>/)
833
- modified_xml = xml_content.gsub(/<\/activity>/) do |match|
834
- "#{scheme_intent_filter}\n #{match}"
835
- end
836
-
837
- # 保存修改
838
- File.write(manifest_path, modified_xml)
839
- return true
840
- end
841
-
842
- return false
980
+ File.write(manifest_path, modified)
981
+ true
843
982
  rescue => e
844
983
  return false
845
984
  end
846
985
  end
847
986
 
848
- def self.update_existing_scheme(manifest_path, activity, android_prefix, existing_scheme, scheme_name)
849
- begin
850
- doc = Nokogiri::XML(File.read(manifest_path))
851
-
852
- # 查找所有intent-filter
853
- intent_filters = doc.xpath("//intent-filter")
854
-
855
- intent_filters.each do |intent_filter|
856
- # 检查intent-filter中的scheme
857
- intent_filter.xpath("data[@#{android_prefix}:scheme]").each do |data|
858
- current_scheme = data["#{android_prefix}:scheme"]
859
- if current_scheme == existing_scheme
860
- # 找到intent-filter,更新scheme
861
- data["#{android_prefix}:scheme"] = scheme_name
862
- end
863
- end
864
- end
987
+ def self.update_scheme_value_in_place(xml_content, existing_scheme, scheme_name)
988
+ return nil if xml_content.to_s.empty?
989
+ return nil if existing_scheme.to_s.empty? || scheme_name.to_s.empty?
865
990
 
866
- # 保存修改
867
- xml_content = doc.to_xml(indent: 2, encoding: 'UTF-8')
991
+ # 尽量局部替换 data 的 android:scheme / scheme 属性值,避免重排整个 XML。
992
+ patterns = [
993
+ /(?<attr>\bandroid:scheme)\s*=\s*(?<q>["'])#{Regexp.escape(existing_scheme)}\k<q>/,
994
+ /(?<attr>\bscheme)\s*=\s*(?<q>["'])#{Regexp.escape(existing_scheme)}\k<q>/
995
+ ]
868
996
 
869
- # 验证修改是否成功
870
- if xml_content.include?("android:scheme=\"#{scheme_name}\"")
871
- File.write(manifest_path, xml_content)
872
- return true
997
+ patterns.each do |pat|
998
+ next unless xml_content.match?(pat)
999
+ return xml_content.gsub(pat) { |m| %(#{$~[:attr]}="#{scheme_name}") }
1000
+ end
1001
+
1002
+ nil
1003
+ end
1004
+
1005
+ def self.insert_scheme_intent_filter_before_activity_close(xml_content, scheme_name, activity_name: nil)
1006
+ return nil if xml_content.to_s.empty?
1007
+
1008
+ newline = xml_content.include?("\r\n") ? "\r\n" : "\n"
1009
+
1010
+ activity_block = nil
1011
+ if activity_name && !activity_name.to_s.empty?
1012
+ name = Regexp.escape(activity_name.to_s)
1013
+ # 锁定到目标 activity(兼容 android:name / name)
1014
+ activity_block = xml_content.match(
1015
+ /(?<open><activity\b[^>]*(?:\bandroid:name|\bname)\s*=\s*["']#{name}["'][^>]*>)(?<inner>.*?)(?<close><\/activity>)/m
1016
+ )
1017
+ end
1018
+
1019
+ # 找不到目标 activity 时,再退化为第一个 </activity>(保持向后兼容)
1020
+ if activity_block
1021
+ open_tag = activity_block[:open]
1022
+ inner = activity_block[:inner]
1023
+ # close 缩进必须与原文件一致(避免 </activity> 顶格)
1024
+ close_line_indent =
1025
+ activity_block[0].match(/^(?<indent>[ \t]*)<\/activity>\s*$/m)&.[](:indent) ||
1026
+ activity_block[0].match(/^(?<indent>[ \t]*)<activity\b/m)&.[](:indent) ||
1027
+ ""
1028
+ close_line = "#{close_line_indent}</activity>"
1029
+
1030
+ # 复用该 activity 内已存在 intent-filter 的缩进(取最短的,避免被历史插歪的块污染)
1031
+ intent_filter_indents = inner.scan(/^(?<indent>[ \t]*)<intent-filter\b/m).flatten
1032
+ child_indent = intent_filter_indents.reject(&:empty?).min_by(&:length)
1033
+
1034
+ # 如果没有 intent-filter,则尝试复用其它子元素缩进
1035
+ child_indent ||= inner.scan(/^(?<indent>[ \t]*)<(?:action|category|data)\b/m).flatten.reject(&:empty?).min_by(&:length)
1036
+
1037
+ # 如果 activity 内部完全没有子元素,则用 close_tag 行缩进推导
1038
+ unless child_indent
1039
+ # 默认与现有 AndroidManifest 常见风格对齐:activity 内缩进步长通常为 4
1040
+ child_indent = close_line_indent + " "
873
1041
  end
874
1042
 
875
- return false
876
- rescue => e
877
- return false
1043
+ # 推导 intent-filter 内部子元素的缩进步长(2/4/tab 都兼容)
1044
+ existing_grandchild = inner.match(/^(?<indent>[ \t]*)<(?:action|category|data)\b/m)&.[](:indent)
1045
+ indent_step = nil
1046
+ if existing_grandchild && existing_grandchild.start_with?(child_indent)
1047
+ step = existing_grandchild.delete_prefix(child_indent)
1048
+ indent_step = step unless step.empty?
1049
+ end
1050
+ indent_step ||= " "
1051
+ grandchild_indent = child_indent + indent_step
1052
+
1053
+ block = [
1054
+ "#{child_indent}<intent-filter>",
1055
+ "#{grandchild_indent}<action android:name=\"android.intent.action.VIEW\" />",
1056
+ "#{grandchild_indent}<category android:name=\"android.intent.category.DEFAULT\" />",
1057
+ "#{grandchild_indent}<category android:name=\"android.intent.category.BROWSABLE\" />",
1058
+ "#{grandchild_indent}<data android:scheme=\"#{scheme_name}\" />",
1059
+ "#{child_indent}</intent-filter>",
1060
+ ].join(newline)
1061
+
1062
+ # 规范化:块与块之间留 1 个空行;保持 </activity> 缩进对齐
1063
+ normalized_inner = inner.sub(/(?:#{Regexp.escape(newline)}[ \t]*)*\z/, "")
1064
+ replaced = [
1065
+ open_tag,
1066
+ normalized_inner,
1067
+ newline,
1068
+ newline,
1069
+ block,
1070
+ newline,
1071
+ close_line
1072
+ ].join
1073
+ return xml_content.sub(activity_block[0], replaced)
1074
+ end
1075
+
1076
+ m = xml_content.match(/^(?<indent>[ \t]*)<\/activity>\s*$/m)
1077
+ return nil unless m
1078
+
1079
+ close_indent = m[:indent] || ""
1080
+ # 默认与现有 AndroidManifest 常见风格对齐:activity 内缩进步长通常为 4
1081
+ child_indent = close_indent + " "
1082
+ grandchild_indent = child_indent + " "
1083
+
1084
+ block = [
1085
+ "#{child_indent}<intent-filter>",
1086
+ "#{grandchild_indent}<action android:name=\"android.intent.action.VIEW\" />",
1087
+ "#{grandchild_indent}<category android:name=\"android.intent.category.DEFAULT\" />",
1088
+ "#{grandchild_indent}<category android:name=\"android.intent.category.BROWSABLE\" />",
1089
+ "#{grandchild_indent}<data android:scheme=\"#{scheme_name}\" />",
1090
+ "#{child_indent}</intent-filter>",
1091
+ ].join(newline)
1092
+
1093
+ xml_content.sub(/^(?<indent>[ \t]*)<\/activity>\s*$/m) do |match|
1094
+ "#{newline}#{block}#{newline}#{newline}#{match}"
878
1095
  end
879
1096
  end
880
1097
 
@@ -228,30 +228,49 @@ module Pindo
228
228
  end
229
229
 
230
230
  content = File.read(settings_gradle_path)
231
- # 兼容 include ':app' 和 include(":app")
232
- modules = content.scan(/include\s*\(?\s*['\"]?([:\w\-]+)['\"]?\s*\)?/).flatten
231
+ modules = extract_modules_from_settings(content)
232
+ project_dir_map = extract_module_project_dirs(content)
233
233
 
234
234
  main_module = modules.find do |m|
235
235
  module_name = m.split(':').last
236
- gradle_path = File.join(project_path, module_name, "build.gradle")
237
- gradle_kts_path = File.join(project_path, module_name, "build.gradle.kts")
238
-
239
- # 优先使用 build.gradle.kts,如果不存在则使用 build.gradle
240
- if File.exist?(gradle_kts_path)
241
- gradle_path = gradle_kts_path
236
+ module_rel_path = project_dir_map[m] || module_name
237
+ module_dir = File.join(project_path, module_rel_path)
238
+ gradle_path = File.join(module_dir, "build.gradle")
239
+ gradle_kts_path = File.join(module_dir, "build.gradle.kts")
240
+
241
+ gradle_file = if File.exist?(gradle_kts_path)
242
+ gradle_kts_path
243
+ elsif File.exist?(gradle_path)
244
+ gradle_path
242
245
  end
243
246
 
244
- if File.exist?(gradle_path)
245
- gradle_content = File.read(gradle_path)
246
- gradle_content.include?("apply plugin: 'com.android.application") ||
247
- gradle_content.include?("id 'com.android.application") ||
248
- (gradle_content.include?("plugins {") && gradle_content.include?("com.android.application"))
247
+ next false unless gradle_file
248
+
249
+ gradle_content = File.read(gradle_file)
250
+ android_application_module?(gradle_content)
251
+ end
252
+
253
+ # 兜底:一些工程未声明标准 application 插件,优先尝试常见主模块名
254
+ if main_module.nil?
255
+ %w[app launcher application].each do |candidate|
256
+ candidate_module = modules.find { |m| m.split(':').last == candidate }
257
+ next unless candidate_module
258
+
259
+ module_rel_path = project_dir_map[candidate_module] || candidate
260
+ module_dir = File.join(project_path, module_rel_path)
261
+ has_gradle = File.exist?(File.join(module_dir, "build.gradle")) || File.exist?(File.join(module_dir, "build.gradle.kts"))
262
+ has_manifest = File.exist?(File.join(module_dir, "src/main/AndroidManifest.xml"))
263
+ if has_gradle && has_manifest
264
+ main_module = candidate_module
265
+ break
266
+ end
249
267
  end
250
268
  end
251
269
  return nil unless main_module
252
270
 
253
271
  module_name = main_module.split(':').last
254
- File.join(project_path, module_name)
272
+ module_rel_path = project_dir_map[main_module] || module_name
273
+ File.join(project_path, module_rel_path)
255
274
  end
256
275
 
257
276
 
@@ -273,7 +292,98 @@ module Pindo
273
292
  android_dir
274
293
  end
275
294
 
295
+ # 在 Unity 作为 lib 的工程中,将 Unity 根目录下 gradle.properties
296
+ # 中的关键配置同步到主工程的 gradle.properties 中
297
+ # 当前仅同步以下键:
298
+ # - android.aapt2FromMavenOverride
299
+ # - org.gradle.java.home
300
+ def sync_gradle_properties_from_unity_to_main(project_path)
301
+ return unless unity_as_lib_android_project?(project_path)
302
+
303
+ unity_gradle_properties = File.join(project_path, "Unity", "gradle.properties")
304
+ return unless File.exist?(unity_gradle_properties)
305
+
306
+ keys_to_sync = [
307
+ "android.aapt2FromMavenOverride",
308
+ "org.gradle.java.home",
309
+ ]
310
+
311
+ unity_values = {}
312
+ File.read(unity_gradle_properties).each_line do |line|
313
+ stripped = line.strip
314
+ next if stripped.empty? || stripped.start_with?("#", "!")
315
+
316
+ keys_to_sync.each do |key|
317
+ # 兼容前后有空格的 "key = value" 写法
318
+ if stripped =~ /^#{Regexp.escape(key)}\s*=\s*(.+)$/
319
+ unity_values[key] = Regexp.last_match(1).strip
320
+ end
321
+ end
322
+ end
323
+
324
+ return if unity_values.empty?
325
+
326
+ main_gradle_properties = File.join(project_path, "gradle.properties")
327
+ main_content = File.exist?(main_gradle_properties) ? File.read(main_gradle_properties) : ""
328
+ original_content = main_content.dup
329
+
330
+ keys_to_sync.each do |key|
331
+ value = unity_values[key]
332
+ next unless value
333
+
334
+ key_regex = /^#{Regexp.escape(key)}\s*=.*$/
335
+ if main_content =~ key_regex
336
+ # 替换已存在的配置行
337
+ main_content = main_content.gsub(key_regex, "#{key}=#{value}")
338
+ else
339
+ # 追加新的配置行
340
+ main_content << "\n" unless main_content.empty? || main_content.end_with?("\n")
341
+ main_content << "#{key}=#{value}\n"
342
+ end
343
+ end
344
+
345
+ return if main_content == original_content
346
+
347
+ File.write(main_gradle_properties, main_content)
348
+ end
349
+
276
350
  private
351
+
352
+ def extract_modules_from_settings(content)
353
+ modules = []
354
+ # 兼容 include ':app' / include(":app", ":lib")
355
+ content.scan(/^\s*include(?:\s*\(|\s+)(.+)$/).each do |match|
356
+ include_args = match[0]
357
+ include_args.scan(/['"](:[^'"]+)['"]/).each do |module_match|
358
+ modules << module_match[0]
359
+ end
360
+ end
361
+ modules.uniq
362
+ end
363
+
364
+ def extract_module_project_dirs(content)
365
+ module_dirs = {}
366
+
367
+ # Groovy: project(':app').projectDir = new File('android/app')
368
+ content.scan(/project\(\s*['"](:[^'"]+)['"]\s*\)\.projectDir\s*=\s*new\s+File\(\s*['"]([^'"]+)['"]\s*\)/).each do |module_name, rel_path|
369
+ module_dirs[module_name] = rel_path
370
+ end
371
+
372
+ # Kotlin DSL: project(":app").projectDir = file("android/app")
373
+ content.scan(/project\(\s*['"](:[^'"]+)['"]\s*\)\.projectDir\s*=\s*file\(\s*['"]([^'"]+)['"]\s*\)/).each do |module_name, rel_path|
374
+ module_dirs[module_name] = rel_path
375
+ end
376
+
377
+ module_dirs
378
+ end
379
+
380
+ def android_application_module?(gradle_content)
381
+ # 标准声明方式
382
+ return true if gradle_content.include?("com.android.application")
383
+
384
+ # Version Catalog alias 声明(如 alias(libs.plugins.android.application))
385
+ gradle_content.match?(/alias\s*\(\s*libs\.plugins\.[A-Za-z0-9_.-]*android[A-Za-z0-9_.-]*application[A-Za-z0-9_.-]*\s*\)/)
386
+ end
277
387
  end
278
388
  end
279
389
  end