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.
- checksums.yaml +4 -4
- data/lib/pindo/base/aeshelper.rb +23 -2
- data/lib/pindo/base/pindocontext.rb +259 -0
- data/lib/pindo/client/pgyer_feishu_oauth_cli.rb +343 -80
- data/lib/pindo/client/pgyerclient.rb +30 -20
- data/lib/pindo/command/android/autobuild.rb +52 -22
- data/lib/pindo/command/android/build.rb +27 -16
- data/lib/pindo/command/android/debug.rb +25 -15
- data/lib/pindo/command/dev/debug.rb +2 -51
- data/lib/pindo/command/dev/feishu.rb +19 -2
- data/lib/pindo/command/ios/adhoc.rb +2 -1
- data/lib/pindo/command/ios/autobuild.rb +35 -8
- data/lib/pindo/command/ios/debug.rb +2 -132
- data/lib/pindo/command/lib/lint.rb +24 -1
- data/lib/pindo/command/setup.rb +24 -4
- data/lib/pindo/command/unity/apk.rb +15 -0
- data/lib/pindo/command/unity/ipa.rb +16 -0
- data/lib/pindo/command.rb +13 -2
- data/lib/pindo/module/android/android_build_config_helper.rb +425 -0
- data/lib/pindo/module/android/apk_helper.rb +23 -25
- data/lib/pindo/module/android/base_helper.rb +572 -0
- data/lib/pindo/module/android/build_helper.rb +8 -318
- data/lib/pindo/module/android/gp_compliance_helper.rb +668 -0
- data/lib/pindo/module/android/gradle_helper.rb +746 -3
- data/lib/pindo/module/appselect.rb +18 -5
- data/lib/pindo/module/build/buildhelper.rb +120 -29
- data/lib/pindo/module/build/unityhelper.rb +675 -18
- data/lib/pindo/module/build/versionhelper.rb +146 -0
- data/lib/pindo/module/cert/certhelper.rb +33 -2
- data/lib/pindo/module/cert/xcodecerthelper.rb +3 -1
- data/lib/pindo/module/pgyer/pgyerhelper.rb +114 -31
- data/lib/pindo/module/xcode/xcodebuildconfig.rb +232 -0
- data/lib/pindo/module/xcode/xcodebuildhelper.rb +0 -1
- data/lib/pindo/version.rb +356 -86
- data/lib/pindo.rb +72 -3
- 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 =
|
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 =
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
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
|
-
#
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
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
|
|