pindo 5.2.4 → 5.3.7

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/aeshelper.rb +23 -2
  3. data/lib/pindo/base/pindocontext.rb +259 -0
  4. data/lib/pindo/client/pgyer_feishu_oauth_cli.rb +343 -80
  5. data/lib/pindo/client/pgyerclient.rb +30 -20
  6. data/lib/pindo/command/android/autobuild.rb +52 -22
  7. data/lib/pindo/command/android/build.rb +27 -16
  8. data/lib/pindo/command/android/debug.rb +25 -15
  9. data/lib/pindo/command/dev/debug.rb +2 -51
  10. data/lib/pindo/command/dev/feishu.rb +19 -2
  11. data/lib/pindo/command/ios/adhoc.rb +2 -1
  12. data/lib/pindo/command/ios/autobuild.rb +35 -8
  13. data/lib/pindo/command/ios/debug.rb +2 -132
  14. data/lib/pindo/command/lib/lint.rb +24 -1
  15. data/lib/pindo/command/setup.rb +24 -4
  16. data/lib/pindo/command/unity/apk.rb +15 -0
  17. data/lib/pindo/command/unity/ipa.rb +16 -0
  18. data/lib/pindo/command.rb +13 -2
  19. data/lib/pindo/module/android/android_build_config_helper.rb +425 -0
  20. data/lib/pindo/module/android/apk_helper.rb +23 -25
  21. data/lib/pindo/module/android/base_helper.rb +572 -0
  22. data/lib/pindo/module/android/build_helper.rb +8 -318
  23. data/lib/pindo/module/android/gp_compliance_helper.rb +668 -0
  24. data/lib/pindo/module/android/gradle_helper.rb +746 -3
  25. data/lib/pindo/module/appselect.rb +18 -5
  26. data/lib/pindo/module/build/buildhelper.rb +120 -29
  27. data/lib/pindo/module/build/unityhelper.rb +675 -18
  28. data/lib/pindo/module/build/versionhelper.rb +146 -0
  29. data/lib/pindo/module/cert/certhelper.rb +33 -2
  30. data/lib/pindo/module/cert/xcodecerthelper.rb +3 -1
  31. data/lib/pindo/module/pgyer/pgyerhelper.rb +114 -31
  32. data/lib/pindo/module/xcode/xcodebuildconfig.rb +232 -0
  33. data/lib/pindo/module/xcode/xcodebuildhelper.rb +0 -1
  34. data/lib/pindo/version.rb +356 -86
  35. data/lib/pindo.rb +72 -3
  36. metadata +5 -1
@@ -0,0 +1,146 @@
1
+ require 'singleton'
2
+ require_relative '../../base/githelper'
3
+ require_relative '../../base/funlog'
4
+
5
+ module Pindo
6
+ class VersionHelper
7
+ include Singleton
8
+ include Pindo::Githelper
9
+
10
+ class << self
11
+ def share_instance
12
+ instance
13
+ end
14
+ end
15
+
16
+ # 从Git tag获取版本号
17
+ # @param project_dir [String] 项目目录路径
18
+ # @return [String] 版本号(例如:1.2.0),如果没有tag则返回0.0.1
19
+ def get_version_from_tag(project_dir: nil)
20
+ raise ArgumentError, "项目目录不能为空" if project_dir.nil?
21
+
22
+ # 获取git根目录
23
+ git_root = git_root_directory(local_repo_dir: project_dir)
24
+ return "0.0.1" unless git_root
25
+
26
+ # 尝试不同的tag前缀
27
+ latest_tag = nil
28
+ ["v", "release", ""].each do |prefix|
29
+ latest_tag = get_latest_version_tag(project_dir: git_root, tag_prefix: prefix)
30
+ break if latest_tag
31
+ end
32
+
33
+ return "0.0.1" if latest_tag.nil?
34
+
35
+ # 从tag中提取版本号
36
+ version = latest_tag.gsub(/^(v|release[\s_-]*)/, '')
37
+
38
+ # 确保版本号格式正确(x.y.z)
39
+ if version =~ /^\d+\.\d+\.\d+$/
40
+ version
41
+ elsif version =~ /^\d+\.\d+$/
42
+ "#{version}.0"
43
+ elsif version =~ /^\d+$/
44
+ "#{version}.0.0"
45
+ else
46
+ "0.0.1"
47
+ end
48
+ end
49
+
50
+ # 获取Build号(从commit hash转换而来)
51
+ # @param project_dir [String] 项目目录路径
52
+ # @return [Integer] Build号(6位16进制转10进制)
53
+ def get_build_number_from_commit(project_dir: nil)
54
+ raise ArgumentError, "项目目录不能为空" if project_dir.nil?
55
+
56
+ # 获取git根目录
57
+ git_root = git_root_directory(local_repo_dir: project_dir)
58
+ return 1 unless git_root
59
+
60
+ begin
61
+ # 获取当前HEAD的commit hash前6位
62
+ commit_hash = git!(%W(-C #{git_root} rev-parse HEAD)).strip[0..5]
63
+
64
+ # 将16进制转换为10进制
65
+ build_number = commit_hash.to_i(16)
66
+
67
+ # 确保在Android versionCode安全范围内(最大值为2^31-1)
68
+ max_value = 2**31 - 1
69
+ build_number = build_number % max_value if build_number > max_value
70
+
71
+ # 确保build_number不为0
72
+ build_number = 1 if build_number == 0
73
+
74
+ build_number
75
+ rescue StandardError => e
76
+ Funlog.instance.fancyinfo_error("获取commit hash失败: #{e.message}")
77
+ # 如果获取失败,返回基于时间戳的默认值
78
+ Time.now.to_i % 1000000 + 1
79
+ end
80
+ end
81
+
82
+ # 从Build号反推commit hash前缀
83
+ # @param build_number [Integer] Build号
84
+ # @return [String] 6位16进制commit hash前缀
85
+ def get_commit_hash_from_build_number(build_number: nil)
86
+ raise ArgumentError, "Build号不能为空" if build_number.nil?
87
+
88
+ # 将10进制转换为6位16进制字符串
89
+ "%06x" % build_number.to_i
90
+ end
91
+
92
+ # 获取版本信息摘要
93
+ # @param project_dir [String] 项目目录路径
94
+ # @return [Hash] 包含版本号、Build号、commit hash等信息
95
+ def get_version_info(project_dir: nil)
96
+ raise ArgumentError, "项目目录不能为空" if project_dir.nil?
97
+
98
+ # 检查是否为Git仓库
99
+ if !is_git_directory?(local_repo_dir: project_dir)
100
+ return {
101
+ is_git_repo: false,
102
+ version: nil,
103
+ build_number: nil,
104
+ message: "项目不在Git仓库中,保持原有版本号"
105
+ }
106
+ end
107
+
108
+ git_root = git_root_directory(local_repo_dir: project_dir)
109
+ version = get_version_from_tag(project_dir: project_dir)
110
+ build_number = get_build_number_from_commit(project_dir: project_dir)
111
+ commit_hash_prefix = get_commit_hash_from_build_number(build_number: build_number)
112
+
113
+ begin
114
+ full_commit_hash = git!(%W(-C #{git_root} rev-parse HEAD)).strip
115
+ current_branch = git!(%W(-C #{git_root} rev-parse --abbrev-ref HEAD)).strip
116
+ latest_tag = get_latest_version_tag(project_dir: git_root) || "无"
117
+ rescue StandardError => e
118
+ full_commit_hash = ""
119
+ current_branch = ""
120
+ latest_tag = "无"
121
+ end
122
+
123
+ {
124
+ is_git_repo: true,
125
+ version: version,
126
+ build_number: build_number,
127
+ commit_hash_prefix: commit_hash_prefix,
128
+ full_commit_hash: full_commit_hash,
129
+ current_branch: current_branch,
130
+ latest_tag: latest_tag,
131
+ version_string: "#{version} (#{build_number})"
132
+ }
133
+ end
134
+
135
+
136
+ # 验证Build号是否在有效范围内
137
+ # @param build_number [Integer] Build号
138
+ # @return [Boolean] 是否有效
139
+ def valid_build_number?(build_number)
140
+ return false if build_number.nil?
141
+
142
+ # Android versionCode的有效范围是1到2^31-1
143
+ build_number >= 1 && build_number <= 2**31 - 1
144
+ end
145
+ end
146
+ end
@@ -10,6 +10,30 @@ require 'pindo/module/cert/provisioninghelper'
10
10
  module Pindo
11
11
 
12
12
  module CertHelper
13
+ # 密码缓存,避免重复获取相同URL的密码
14
+ @@password_cache = {}
15
+
16
+ # 获取密码的辅助方法,使用缓存避免重复获取
17
+ def self.get_cached_password(cert_url)
18
+ unless @@password_cache[cert_url]
19
+ puts "\e[33m[DEBUG] 密码缓存中未找到,从Keychain获取: #{cert_url}\e[0m" if ENV['PINDO_DEBUG']
20
+ @@password_cache[cert_url] = AESHelper.fetch_password(keychain_name: cert_url)
21
+ puts "\e[32m[DEBUG] 密码已缓存: #{cert_url}\e[0m" if ENV['PINDO_DEBUG']
22
+ else
23
+ puts "\e[32m[DEBUG] 从密码缓存获取: #{cert_url}\e[0m" if ENV['PINDO_DEBUG']
24
+ end
25
+ @@password_cache[cert_url]
26
+ end
27
+
28
+ # 清除密码缓存
29
+ def self.clear_password_cache
30
+ @@password_cache.clear
31
+ end
32
+
33
+ # 清除特定URL的密码缓存
34
+ def self.clear_password_cache_for_url(cert_url)
35
+ @@password_cache.delete(cert_url)
36
+ end
13
37
 
14
38
  def get_cert_info(cer_certificate)
15
39
  # can receive a certificate path or the file data
@@ -87,18 +111,22 @@ module Pindo
87
111
  else
88
112
  output_dir = Dir.mktmpdir
89
113
 
90
- decrypt_password = AESHelper.fetch_password(keychain_name:cert_url)
114
+ decrypt_password = CertHelper.get_cached_password(cert_url)
91
115
  Funlog.instance.fancyinfo_start("正在安装证书...")
92
116
 
93
117
  cert_path = AESHelper.decrypt_specific_file(src_file: certs.first, password:decrypt_password, output_dir: output_dir)
94
118
  if cert_path.nil? || cert_path.empty? || !File.exist?(cert_path)
95
119
  AESHelper.delete_password(keychain_name:cert_url)
120
+ # 清除内存中的密码缓存,避免重复使用错误密码
121
+ @@password_cache.delete(cert_url)
96
122
  raise Informative, "证书解析失败,密码错误!"
97
123
  end
98
124
 
99
125
  key_path = AESHelper.decrypt_specific_file(src_file: keys.first, password:decrypt_password, output_dir: output_dir)
100
126
  if key_path.nil? || key_path.empty? || !File.exist?(key_path)
101
127
  AESHelper.delete_password(keychain_name:cert_url)
128
+ # 清除内存中的密码缓存,避免重复使用错误密码
129
+ @@password_cache.delete(cert_url)
102
130
  raise Informative, "证书解析失败,密码错误!"
103
131
  end
104
132
 
@@ -168,6 +196,10 @@ module Pindo
168
196
 
169
197
  un_exist_files = []
170
198
  provisioning_info_array = []
199
+
200
+ # 在循环外获取密码,避免重复添加到Keychain
201
+ decrypt_password = CertHelper.get_cached_password(cert_url)
202
+
171
203
  bundle_id_map.each do |type, bundle_id_temp|
172
204
  profile_filename = File.join(certs_dir, "profiles", cert_sub_dir, [provision_start_name.to_s, bundle_id_temp].join('_') + provision_extension_name)
173
205
  unless File.exist?(profile_filename)
@@ -175,7 +207,6 @@ module Pindo
175
207
  next
176
208
  end
177
209
  # puts "正在安装 #{bundle_id_temp}..."
178
- decrypt_password = AESHelper.fetch_password(keychain_name:cert_url)
179
210
  output_dir = Dir.mktmpdir
180
211
  file_decrypt = AESHelper.decrypt_specific_file(src_file: profile_filename, password:decrypt_password, output_dir: output_dir)
181
212
  destpath = Provisioninghelper.install(file_decrypt)
@@ -333,11 +333,13 @@ module Pindo
333
333
 
334
334
 
335
335
  keys = Dir[File.join(certs_dir, "certs", cert_sub_dir, "*.p12")]
336
- decrypt_password = AESHelper.fetch_password(keychain_name:cert_git_url)
336
+ decrypt_password = CertHelper.get_cached_password(cert_git_url)
337
337
  output_dir = Dir.mktmpdir
338
338
  key_path = AESHelper.decrypt_specific_file(src_file: keys.first, password:decrypt_password, output_dir: output_dir)
339
339
  if key_path.nil? || key_path.empty? || !File.exist?(key_path)
340
340
  AESHelper.delete_password(keychain_name:cert_git_url)
341
+ # 清除内存中的密码缓存,避免重复使用错误密码
342
+ CertHelper.clear_password_cache_for_url(cert_git_url)
341
343
  raise Informative, "证书解析失败,密码错误!"
342
344
  end
343
345
 
@@ -105,19 +105,38 @@ module Pindo
105
105
  proj_name_array.uniq
106
106
  proj_name_array << "自定义输入Pyger上的App代号"
107
107
 
108
- if proj_name_array.size > 1
109
- cli = HighLine.new
110
- upload_proj_name = cli.choose do |menu|
111
- menu.prompt = "请选择Pgyer上的App代号:"
112
- menu.choices(*proj_name_array)
113
- end
114
- if upload_proj_name.include?("自定义输入")
108
+ # 检查缓存的 App Key
109
+ require_relative '../../base/pindocontext'
110
+ context = Pindo::PindoContext.instance
111
+ cached_app_key = context.get_selection(Pindo::PindoContext::SelectionKey::APP_KEY)
112
+
113
+ if cached_app_key && proj_name_array.include?(cached_app_key)
114
+ puts "\n使用之前选择的App代号: #{cached_app_key}"
115
+ upload_proj_name = cached_app_key
116
+ # 直接使用缓存的选择,跳过后续选择逻辑
117
+ end
118
+
119
+ # 只有在没有缓存或缓存无效时才显示选择菜单
120
+ if upload_proj_name.nil? || upload_proj_name.empty?
121
+ if proj_name_array.size > 1
122
+ cli = HighLine.new
123
+ upload_proj_name = cli.choose do |menu|
124
+ menu.prompt = "请选择Pgyer上的App代号:"
125
+ menu.choices(*proj_name_array)
126
+ end
127
+ if upload_proj_name.include?("自定义输入")
128
+ upload_proj_name = ask('请输入Pyger上的App代号(大小写空格忽略):') || nil
129
+ upload_proj_name = upload_proj_name.strip if upload_proj_name
130
+ end
131
+ else
115
132
  upload_proj_name = ask('请输入Pyger上的App代号(大小写空格忽略):') || nil
116
- upload_proj_name = upload_proj_name.strip
133
+ upload_proj_name = upload_proj_name.strip if upload_proj_name
134
+ end
135
+
136
+ # 保存选择到缓存(排除自定义输入选项)
137
+ if upload_proj_name && !upload_proj_name.include?("自定义输入")
138
+ context.set_selection(Pindo::PindoContext::SelectionKey::APP_KEY, upload_proj_name)
117
139
  end
118
- else
119
- upload_proj_name = ask('请输入Pyger上的App代号(大小写空格忽略):') || nil
120
- upload_proj_name = upload_proj_name.strip
121
140
  end
122
141
 
123
142
 
@@ -125,7 +144,7 @@ module Pindo
125
144
 
126
145
  if app_info_obj.nil?
127
146
  upload_proj_name = ask('没有找到结果,请重新输入Pyger上的App代号(大小写空格忽略):') || nil
128
- upload_proj_name = upload_proj_name.strip
147
+ upload_proj_name = upload_proj_name.strip if upload_proj_name
129
148
  app_info_obj = PgyerHelper.share_instace.find_app_info_with_obj_list(proj_name:upload_proj_name)
130
149
  end
131
150
 
@@ -168,7 +187,6 @@ module Pindo
168
187
  Dir.chdir(current_dir)
169
188
  end
170
189
  end
171
- puts "ipa_file_upload: ++++1"
172
190
  if !ipa_file_upload.nil? &&
173
191
  File.extname(ipa_file_upload).eql?(".html")
174
192
 
@@ -200,7 +218,6 @@ module Pindo
200
218
  Dir.chdir(current_project_dir)
201
219
  ipa_file_upload = web_res_zip_fullname
202
220
  end
203
- puts "ipa_file_upload: ++++2"
204
221
  unless !ipa_file_upload.nil? && File.exist?(ipa_file_upload)
205
222
  return
206
223
  end
@@ -209,7 +226,8 @@ module Pindo
209
226
  ipa_file_upload=File.join(args_ipa_file_dir, File.basename(ipa_file_upload))
210
227
  current_project_dir = Dir.pwd
211
228
  description = get_description_from_git(current_project_dir:current_project_dir)
212
- description = " " if description.nil? || description.empty?
229
+ # get_description_from_git 现在会在失败时抛出异常,成功时返回有效的描述
230
+ # 所以不需要再检查 nil 或空值
213
231
 
214
232
 
215
233
  addtach_file = nil
@@ -235,9 +253,6 @@ module Pindo
235
253
  Dir.chdir current_project_dir
236
254
  end
237
255
 
238
- if description.nil? || description.length <= 0
239
- description = " "
240
- end
241
256
  puts
242
257
  puts "上传项目: #{app_info_obj["appName"]}"
243
258
  print "上传备注: "
@@ -556,26 +571,94 @@ module Pindo
556
571
 
557
572
  def get_description_from_git(current_project_dir:nil)
558
573
  description = nil
574
+
575
+ # 检查是否是 git 仓库
559
576
  if !current_project_dir.nil? && is_git_directory?(local_repo_dir: current_project_dir)
560
577
  current_git_root_path = git_root_directory(local_repo_dir: current_project_dir)
561
578
 
562
- # dev 打包情况的备注
563
- cliff_toml_path = File.join(current_git_root_path, "cliff.toml")
564
- if File.exist?(cliff_toml_path)
565
- begin
566
- `git-cliff --version`
567
- git_cliff_installed = $?.success?
568
- if git_cliff_installed
569
- temp_dir = Dir.pwd
570
- Dir.chdir(current_git_root_path)
571
- `git-cliff -c #{cliff_toml_path} --latest -o -`
572
- description = `git-cliff -c #{cliff_toml_path} --latest -o -`.strip
573
- Dir.chdir(temp_dir)
579
+ # 检查 git-cliff 是否已安装
580
+ `git-cliff --version 2>&1`
581
+ git_cliff_installed = $?.success?
582
+
583
+ if git_cliff_installed
584
+ temp_dir = Dir.pwd
585
+ Dir.chdir(current_git_root_path)
586
+
587
+ # 检查 HEAD 是否有 tag
588
+ head_tag = `git describe --exact-match --tags HEAD 2>/dev/null`.strip
589
+ has_tag_at_head = $?.success? && !head_tag.empty?
590
+
591
+ # 根据 HEAD 是否有 tag 决定使用哪种参数
592
+ cliff_args = if has_tag_at_head
593
+ # HEAD 有 tag,输出最新 tag 的更改(相对于前一个 tag)
594
+ puts "HEAD 存在 tag: #{head_tag},使用 --latest 参数" if ENV['DEBUG']
595
+ "--latest"
596
+ else
597
+ # HEAD 没有 tag,输出未发布的更改(从最新 tag 到 HEAD)
598
+ puts "HEAD 不存在 tag,使用 --unreleased 参数" if ENV['DEBUG']
599
+ "--unreleased"
600
+ end
601
+
602
+ # 尝试使用不同的配置文件策略
603
+ cliff_config_cmd = nil
604
+
605
+ # 策略1: 使用项目根目录的 cliff.toml
606
+ project_cliff_toml = File.join(current_git_root_path, "cliff.toml")
607
+ if File.exist?(project_cliff_toml)
608
+ puts "使用项目配置文件: #{project_cliff_toml}" if ENV['DEBUG']
609
+ cliff_config_cmd = "git-cliff -c \"#{project_cliff_toml}\" #{cliff_args} -o -"
610
+ else
611
+ # 策略2: 使用 Pindo 默认配置文件(使用与 check_and_install_cliff 相同的方法)
612
+ pindo_common_dir = clone_pindo_common_config_repo(force_delete:false)
613
+ pindo_cliff_toml = File.join(pindo_common_dir, 'cliff.toml')
614
+ if File.exist?(pindo_cliff_toml)
615
+ puts "使用 Pindo 默认配置文件: #{pindo_cliff_toml}" if ENV['DEBUG']
616
+ cliff_config_cmd = "git-cliff -c \"#{pindo_cliff_toml}\" #{cliff_args} -o -"
574
617
  end
575
- rescue StandardError => e
576
618
  end
619
+
620
+ # 执行 git-cliff 命令
621
+ if cliff_config_cmd
622
+ # 分别捕获 stdout 和 stderr,只使用 stdout 作为描述
623
+ require 'open3'
624
+ stdout, stderr, status = Open3.capture3(cliff_config_cmd)
625
+
626
+ if status.success?
627
+ description = stdout.strip
628
+ # 如果有版本更新提示,输出到终端但不包含在描述中
629
+ if stderr && !stderr.empty?
630
+ puts stderr if stderr.include?("new version") || ENV['DEBUG']
631
+ end
632
+ puts "git-cliff 输出成功" if ENV['DEBUG']
633
+ else
634
+ puts "\n\e[31m错误: git-cliff 执行失败\e[0m"
635
+ error_msg = stderr && !stderr.empty? ? stderr : stdout
636
+ puts "\e[31m错误信息: #{error_msg}\e[0m"
637
+ puts "\e[33m请检查 git-cliff 配置或联系管理员\e[0m"
638
+ raise Informative, "git-cliff 执行失败,无法生成版本描述"
639
+ end
640
+ else
641
+ # 没有找到任何配置文件
642
+ puts "\n\e[31m错误: 未找到 cliff.toml 配置文件\e[0m"
643
+ puts "\e[33m请确保:\e[0m"
644
+ puts "\e[36m 1. 项目根目录存在 cliff.toml 文件\e[0m"
645
+ puts "\e[36m 2. 或 Pindo 工具目录存在默认配置文件\e[0m"
646
+ raise Informative, "缺少 cliff.toml 配置文件,无法生成版本描述"
647
+ end
648
+
649
+ Dir.chdir(temp_dir)
650
+ else
651
+ # git-cliff 未安装,提示用户并退出
652
+ puts "\n\e[31m错误: git-cliff 未安装\e[0m"
653
+ puts "\e[33m请先安装 git-cliff 工具:\e[0m"
654
+ puts "\e[36m macOS: brew install git-cliff\e[0m"
655
+ puts "\e[36m 或访问: https://github.com/orhun/git-cliff\e[0m"
656
+ raise Informative, "git-cliff 未安装,无法生成版本描述"
577
657
  end
658
+ else
659
+ puts "当前目录不是 git 仓库" if ENV['DEBUG']
578
660
  end
661
+
579
662
  return description
580
663
  end
581
664
 
@@ -1,11 +1,243 @@
1
1
  require 'fileutils'
2
2
  require 'xcodeproj'
3
3
  require 'json'
4
+ require_relative '../../base/funlog'
4
5
 
5
6
  module Pindo
6
7
 
7
8
  class XcodeBuildConfig
8
9
 
10
+ # 添加URL Schemes到iOS工程的Info.plist
11
+ # @param project_dir [String] iOS项目目录路径
12
+ # @param scheme_name [String] 要添加的scheme名称(可选)
13
+ # @return [Boolean] 是否成功添加
14
+ def self.add_url_schemes(project_dir: nil, scheme_name: nil)
15
+ raise ArgumentError, "项目目录不能为空" if project_dir.nil?
16
+
17
+ project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
18
+ return false if project_fullname.nil?
19
+
20
+ info_plist_path = nil
21
+ bundleid_scheme_name = nil
22
+
23
+ project_obj = Xcodeproj::Project.open(project_fullname)
24
+ project_obj.targets.each do |target|
25
+ if target.product_type.to_s.eql?("com.apple.product-type.application")
26
+ temp_info_file = target.build_configurations.first.build_settings['INFOPLIST_FILE']
27
+ if temp_info_file && !temp_info_file.empty?
28
+ info_plist_path = File.join(project_dir, temp_info_file)
29
+ end
30
+ bundleid_scheme_name = target.build_configurations.first.build_settings['PRODUCT_BUNDLE_IDENTIFIER']
31
+ break # 找到第一个application target即可
32
+ end
33
+ end
34
+
35
+ return false unless info_plist_path && File.exist?(info_plist_path)
36
+
37
+ info_plist_dict = Xcodeproj::Plist.read_from_path(info_plist_path)
38
+ info_plist_dict["CFBundleURLTypes"] ||= []
39
+ schemes_added = []
40
+
41
+ # 添加基于项目名的scheme
42
+ if scheme_name && !scheme_name.empty?
43
+ scheme_value = scheme_name.to_s.gsub(/[^a-zA-Z0-9]/, '').downcase
44
+ if add_single_scheme(info_plist_dict, scheme_value)
45
+ schemes_added << scheme_value
46
+ end
47
+ end
48
+
49
+ # 添加基于Bundle ID的scheme
50
+ if bundleid_scheme_name && !bundleid_scheme_name.empty?
51
+ bundleid_scheme_value = bundleid_scheme_name.gsub(/[^a-zA-Z0-9]/, '').downcase
52
+ if add_single_scheme(info_plist_dict, bundleid_scheme_value)
53
+ schemes_added << bundleid_scheme_value
54
+ end
55
+ end
56
+
57
+ # 保存修改
58
+ if schemes_added.any?
59
+ Xcodeproj::Plist.write_to_path(info_plist_dict, info_plist_path)
60
+ schemes_added.each { |s| puts " ✓ 已添加URL Scheme: #{s}" }
61
+ end
62
+
63
+ return true
64
+ end
65
+
66
+ # 添加单个scheme到plist字典中
67
+ # @param plist_dict [Hash] Info.plist字典
68
+ # @param scheme_value [String] scheme值
69
+ # @return [Boolean] 是否添加了新scheme
70
+ def self.add_single_scheme(plist_dict, scheme_value)
71
+ return false if scheme_value.nil? || scheme_value.empty?
72
+
73
+ # 检查是否已存在
74
+ return false if plist_dict["CFBundleURLTypes"].any? { |item| item["CFBundleURLName"] == scheme_value }
75
+
76
+ # 创建新的URL Type
77
+ url_type = {
78
+ "CFBundleTypeRole" => "Editor",
79
+ "CFBundleURLName" => scheme_value,
80
+ "CFBundleURLSchemes" => [scheme_value]
81
+ }
82
+
83
+ plist_dict["CFBundleURLTypes"] << url_type
84
+ return true
85
+ end
86
+
87
+ # 更新iOS工程版本号
88
+ # @param project_dir [String] iOS项目目录路径
89
+ # @param version [String] 版本号
90
+ # @param build_number [Integer] Build号
91
+ # @return [Boolean] 是否成功更新
92
+ def self.update_ios_project_version(project_dir: nil, version: nil, build_number: nil)
93
+ raise ArgumentError, "项目目录不能为空" if project_dir.nil?
94
+ raise ArgumentError, "版本号不能为空" if version.nil?
95
+ raise ArgumentError, "Build号不能为空" if build_number.nil?
96
+
97
+ Funlog.instance.fancyinfo_start("正在更新iOS工程版本信息...")
98
+
99
+ begin
100
+ # 查找.xcodeproj文件
101
+ xcodeproj_path = find_xcodeproj(project_dir)
102
+
103
+ if xcodeproj_path.nil?
104
+ Funlog.instance.fancyinfo_error("未找到Xcode项目文件")
105
+ return false
106
+ end
107
+
108
+ # 打开Xcode项目
109
+ project = Xcodeproj::Project.open(xcodeproj_path)
110
+
111
+ # 更新所有application类型target的版本号
112
+ updated_targets = []
113
+ project.targets.each do |target|
114
+ # 只处理application类型的target
115
+ if target.product_type.to_s.eql?("com.apple.product-type.application")
116
+ target.build_configurations.each do |config|
117
+ # 更新版本号
118
+ config.build_settings['MARKETING_VERSION'] = version
119
+ config.build_settings['CURRENT_PROJECT_VERSION'] = build_number.to_s
120
+
121
+ # 兼容旧的设置方式,通过Info.plist更新
122
+ if config.build_settings['INFOPLIST_FILE']
123
+ info_plist_path = File.join(project_dir, config.build_settings['INFOPLIST_FILE'])
124
+ if File.exist?(info_plist_path)
125
+ update_info_plist(info_plist_path, version, build_number.to_s)
126
+ end
127
+ end
128
+ end
129
+ updated_targets << target.name
130
+ end
131
+ end
132
+
133
+ # 保存项目
134
+ project.save
135
+
136
+ if updated_targets.empty?
137
+ Funlog.instance.fancyinfo_error("未找到需要更新的application target")
138
+ return false
139
+ else
140
+ Funlog.instance.fancyinfo_success("iOS版本更新完成!")
141
+ puts " ✓ 版本号已更新: #{version}"
142
+ puts " ✓ Build号已更新: #{build_number}"
143
+ puts " ✓ 更新的Target: #{updated_targets.join(', ')}"
144
+ return true
145
+ end
146
+
147
+ rescue StandardError => e
148
+ Funlog.instance.fancyinfo_error("更新iOS版本失败: #{e.message}")
149
+ return false
150
+ end
151
+ end
152
+
153
+ # 更新Info.plist文件
154
+ # @param plist_path [String] Info.plist文件路径
155
+ # @param version [String] 版本号
156
+ # @param build_number [String] Build号
157
+ private_class_method def self.update_info_plist(plist_path, version, build_number)
158
+ return unless File.exist?(plist_path)
159
+
160
+ # 使用PlistBuddy更新plist
161
+ system("/usr/libexec/PlistBuddy -c 'Set :CFBundleShortVersionString #{version}' '#{plist_path}' 2>/dev/null")
162
+ system("/usr/libexec/PlistBuddy -c 'Set :CFBundleVersion #{build_number}' '#{plist_path}' 2>/dev/null")
163
+ end
164
+
165
+ # 查找Xcode项目文件
166
+ # @param project_dir [String] 项目目录
167
+ # @return [String, nil] xcodeproj路径
168
+ private_class_method def self.find_xcodeproj(project_dir)
169
+ # 先查找当前目录
170
+ xcodeproj_path = Dir.glob(File.join(project_dir, "*.xcodeproj")).first
171
+
172
+ # Unity导出的iOS工程特殊处理
173
+ if xcodeproj_path.nil? && File.exist?(File.join(project_dir, "Unity", "Unity-iPhone.xcodeproj"))
174
+ xcodeproj_path = File.join(project_dir, "Unity", "Unity-iPhone.xcodeproj")
175
+ end
176
+
177
+ xcodeproj_path
178
+ end
179
+
180
+ # 更新entitlements配置
181
+ # 根据entitlements文件内容,同步更新config.json中的配置
182
+ # 如果entitlements中没有icloud或app group,则从config.json中移除对应配置
183
+ # @param project_dir [String] 项目目录路径
184
+ # @param config_file [String] config.json文件路径
185
+ # @return [Boolean] 是否成功更新
186
+ def self.update_entitlements_config(project_dir: nil, config_file: nil)
187
+ raise ArgumentError, "项目目录不能为空" if project_dir.nil?
188
+ raise ArgumentError, "配置文件路径不能为空" if config_file.nil?
189
+
190
+ project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
191
+ return false if project_fullname.nil?
192
+
193
+ entitlements_plist_path = nil
194
+ project_obj = Xcodeproj::Project.open(project_fullname)
195
+
196
+ project_obj.targets.each do |target|
197
+ if target.product_type.to_s.eql?("com.apple.product-type.application")
198
+ temp_entitlements_file = target.build_configurations.first.build_settings['CODE_SIGN_ENTITLEMENTS']
199
+ if temp_entitlements_file && !temp_entitlements_file.empty?
200
+ entitlements_plist_path = File.join(project_dir, temp_entitlements_file)
201
+ end
202
+ break # 找到第一个application target即可
203
+ end
204
+ end
205
+
206
+ # 处理entitlements配置
207
+ if entitlements_plist_path && File.exist?(entitlements_plist_path)
208
+ config_json = nil
209
+ if File.exist?(config_file)
210
+ config_json = JSON.parse(File.read(config_file))
211
+ end
212
+
213
+ return false if config_json.nil?
214
+
215
+ entitlements_plist_dict = Xcodeproj::Plist.read_from_path(entitlements_plist_path)
216
+
217
+ # 如果entitlements中没有icloud配置,从config.json中移除
218
+ if entitlements_plist_dict["com.apple.developer.icloud-container-identifiers"].nil?
219
+ if config_json["app_info"] && config_json["app_info"]['app_icloud_id']
220
+ config_json["app_info"].delete('app_icloud_id')
221
+ end
222
+ end
223
+
224
+ # 如果entitlements中没有app group配置,从config.json中移除
225
+ if entitlements_plist_dict["com.apple.security.application-groups"].nil?
226
+ if config_json["app_info"] && config_json["app_info"]['app_group_id']
227
+ config_json["app_info"].delete('app_group_id')
228
+ end
229
+ end
230
+
231
+ # 保存更新后的config.json
232
+ File.open(config_file, "w") do |f|
233
+ f.write(JSON.pretty_generate(config_json))
234
+ end
235
+
236
+ return true
237
+ end
238
+
239
+ return false
240
+ end
9
241
 
10
242
  end
11
243