pindo 5.21.3 → 5.21.5
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/git_handler.rb +21 -2
- data/lib/pindo/base/xcodeproj_compat.rb +42 -0
- data/lib/pindo/module/cert/keychain_helper.rb +36 -81
- data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +34 -4
- data/lib/pindo/module/xcode/res/xcode_res_handler.rb +87 -26
- data/lib/pindo/module/xcode/scene_lifecycle_guard.rb +16 -5
- data/lib/pindo/module/xcode/xcode_build_config.rb +47 -3
- data/lib/pindo/version.rb +1 -1
- data/lib/pindo.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4ccbec1218f28dfa10b000e89759db3e8167c0ea598637a0f8436a557ab14bc
|
|
4
|
+
data.tar.gz: 737550673bf62315c4ab30652496a82bcda55001991af18c2c5da1b019b9cf83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52e831191efa0d0092fdd2333658ece13ff82f004255d5dfe55b5f75a295b5d46356fc56956323b8b526bd6dfdda5fa10b15e5e144e971fb70bd629bb5268e57
|
|
7
|
+
data.tar.gz: 6fa3ad96ffe7951bcec719e8c9a3ffad2540e807c4d451ba6a3dc16fbe59e85a2520f874d72ae589b72ebd47828df71c0482dfcedbdb310e58d6c99f49e24efb
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
|
+
require 'open3'
|
|
2
3
|
require 'pindo/base/executable'
|
|
3
4
|
require 'pindo/base/informative'
|
|
4
5
|
require 'etc'
|
|
@@ -556,6 +557,16 @@ module Pindo
|
|
|
556
557
|
|
|
557
558
|
|
|
558
559
|
|
|
560
|
+
# 仓库是否已有提交(git init 后尚未首次提交的仓库没有 HEAD)
|
|
561
|
+
# @param git_root [String] Git 根目录
|
|
562
|
+
# @return [Boolean]
|
|
563
|
+
def head_commit_exists?(git_root:)
|
|
564
|
+
_stdout, _stderr, status = Open3.capture3('git', '-C', git_root, 'rev-parse', '--verify', '--quiet', 'HEAD')
|
|
565
|
+
status.success?
|
|
566
|
+
rescue SystemCallError
|
|
567
|
+
false
|
|
568
|
+
end
|
|
569
|
+
|
|
559
570
|
# 获取未提交的文件列表
|
|
560
571
|
# @param project_dir [String] 项目目录路径
|
|
561
572
|
# @return [Array<String>, nil] 返回未提交的文件列表,如果没有则返回 nil
|
|
@@ -970,6 +981,15 @@ module Pindo
|
|
|
970
981
|
# @param default_process_type [Symbol, nil] 非交互模式下的默认处理方式,默认为 nil
|
|
971
982
|
# @return [Symbol] UncommittedFilesProcessType 枚举常量
|
|
972
983
|
def get_uncommitted_files_process_type(project_dir:, interactive: true, default_process_type: nil)
|
|
984
|
+
# 0. 空仓库没有 HEAD,后续的暂存区重置、版本计算、分支同步和打 tag 都无法进行
|
|
985
|
+
git_root = git_root_directory(local_repo_dir: project_dir)
|
|
986
|
+
if git_root && !head_commit_exists?(git_root: git_root)
|
|
987
|
+
raise Informative, "当前 Git 仓库还没有任何提交,无法计算版本号和打 tag:#{git_root}\n" \
|
|
988
|
+
"请先完成首次提交后重新执行:\n" \
|
|
989
|
+
" git -C \"#{git_root}\" add -A\n" \
|
|
990
|
+
" git -C \"#{git_root}\" commit -m \"chore: 初始化工程\""
|
|
991
|
+
end
|
|
992
|
+
|
|
973
993
|
# 1. 检查是否有未提交的文件
|
|
974
994
|
uncommitted_files = get_uncommitted_files(project_dir: project_dir)
|
|
975
995
|
|
|
@@ -986,8 +1006,7 @@ module Pindo
|
|
|
986
1006
|
return default_process_type
|
|
987
1007
|
end
|
|
988
1008
|
|
|
989
|
-
# 4.
|
|
990
|
-
git_root = git_root_directory(local_repo_dir: project_dir)
|
|
1009
|
+
# 4. 获取当前分支名
|
|
991
1010
|
unless git_root
|
|
992
1011
|
Funlog.instance.fancyinfo_warning("当前目录不是Git仓库")
|
|
993
1012
|
return Pindo::UncommittedFilesProcessType::SKIP_WITH_TAG
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'xcodeproj'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
# Xcode 26.4+/27 工程格式兼容补丁(xcodeproj 1.28.1 尚未支持,见 CocoaPods/Xcodeproj#1017)
|
|
5
|
+
#
|
|
6
|
+
# CocoaPods 与 fastlane gym 在 pindo 进程内运行,共用同一份补丁。
|
|
7
|
+
module XcodeprojCompat
|
|
8
|
+
# 新格式把 shellScript 存为按行拆分的数组,合并回字符串
|
|
9
|
+
module ShellScriptArray
|
|
10
|
+
def shell_script=(value)
|
|
11
|
+
super(value.is_a?(Array) ? value.join("\n") : value)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# 保存时保留 xcodeproj 不认识的字段(如 Copy Files 阶段的 dstSubfolder),
|
|
16
|
+
# 否则这些字段会被丢弃,导致扩展等产物的嵌入位置丢失
|
|
17
|
+
module UnknownAttributes
|
|
18
|
+
def configure_with_plist(objects_by_uuid_plist)
|
|
19
|
+
object_plist = objects_by_uuid_plist[uuid]
|
|
20
|
+
known_keys = self.class.attributes.map(&:plist_name) << 'isa'
|
|
21
|
+
@pindo_unknown_attributes = object_plist.reject { |key, _| known_keys.include?(key) }
|
|
22
|
+
|
|
23
|
+
# 暂时移除未知字段,避免 xcodeproj 对已保留的字段输出误导性警告
|
|
24
|
+
@pindo_unknown_attributes.each_key { |key| object_plist.delete(key) }
|
|
25
|
+
super
|
|
26
|
+
ensure
|
|
27
|
+
object_plist&.merge!(@pindo_unknown_attributes || {})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def to_hash_as(method = :to_hash)
|
|
33
|
+
plist = super
|
|
34
|
+
(@pindo_unknown_attributes || {}).each { |key, value| plist[key] = value unless plist.key?(key) }
|
|
35
|
+
plist
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Xcodeproj::Project::Object::PBXShellScriptBuildPhase.prepend(Pindo::XcodeprojCompat::ShellScriptArray)
|
|
42
|
+
Xcodeproj::Project::Object::AbstractObject.prepend(Pindo::XcodeprojCompat::UnknownAttributes)
|
|
@@ -34,7 +34,6 @@ module Pindo
|
|
|
34
34
|
if thrd.value.success?
|
|
35
35
|
# puts "安装成功!"
|
|
36
36
|
unless skip_set_partition_list
|
|
37
|
-
keychain_password ||= resolve_keychain_password(keychain_path)
|
|
38
37
|
set_partition_list(path, keychain_path, keychain_password: keychain_password, output: output, strict: strict)
|
|
39
38
|
end
|
|
40
39
|
else
|
|
@@ -59,100 +58,56 @@ module Pindo
|
|
|
59
58
|
return result
|
|
60
59
|
end
|
|
61
60
|
|
|
61
|
+
# 与 Match 一致:配置密码优先,否则从钥匙串读取或询问并缓存。
|
|
62
62
|
def set_partition_list(path, keychain_path, keychain_password: nil, output: false, strict: false)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# puts command
|
|
81
|
-
# UI.command(sensitive_command) if output
|
|
82
|
-
Open3.popen3(command) do |stdin, stdout, stderr, thrd|
|
|
83
|
-
unless thrd.value.success?
|
|
84
|
-
err = stderr.read.to_s.strip
|
|
85
|
-
raise Informative, "钥匙串访问配置失败,请检查 macOS 钥匙串密码", cause: nil if strict
|
|
86
|
-
|
|
87
|
-
# Inform user when no/wrong password was used as its needed to prevent UI permission popup from Xcode when signing
|
|
88
|
-
if err.include?("SecKeychainItemSetAccessWithPassword")
|
|
89
|
-
keychain_name = File.basename(keychain_path, ".*")
|
|
90
|
-
# puts "password #{keychain_password}"
|
|
91
|
-
# puts "keychain #{keychain_name}"
|
|
92
|
-
# puts "security #{server_name(keychain_name)}"
|
|
93
|
-
Security::InternetPassword.delete(server: server_name(keychain_name))
|
|
94
|
-
|
|
95
|
-
# UI.important("")
|
|
96
|
-
# UI.important("Could not configure imported keychain item (certificate) to prevent UI permission popup when code signing\n" \
|
|
97
|
-
# "Check if you supplied the correct `keychain_password` for keychain: `#{keychain_path}`\n" \
|
|
98
|
-
# "#{err}")
|
|
99
|
-
# UI.important("")
|
|
100
|
-
# UI.important("Please look at the following docs to see how to set a keychain password:")
|
|
101
|
-
# UI.important(" - https://docs.fastlane.tools/actions/sync_code_signing")
|
|
102
|
-
# UI.important(" - https://docs.fastlane.tools/actions/get_certificates")
|
|
103
|
-
# puts "证书密码错误!"
|
|
104
|
-
else
|
|
105
|
-
puts err
|
|
106
|
-
end
|
|
63
|
+
warning = '未配置免弹窗签名权限;后续签名时 macOS 可能弹出系统对话框,请按提示授权。'
|
|
64
|
+
return unless help_backticks('security -h | grep set-key-partition-list', print: false).length > 0
|
|
65
|
+
keychain_password = resolve_keychain_password(keychain_path) if keychain_password.to_s.empty?
|
|
66
|
+
|
|
67
|
+
command = "security set-key-partition-list -S apple-tool:,apple:,codesign: -s"
|
|
68
|
+
command << " -k #{keychain_password.shellescape} #{keychain_path.shellescape} 1> /dev/null"
|
|
69
|
+
Open3.popen3(command) do |stdin, stdout, stderr, thread|
|
|
70
|
+
# 消费错误输出但不打印,避免泄露敏感信息。
|
|
71
|
+
error = stderr.read
|
|
72
|
+
next if thread.value.success?
|
|
73
|
+
|
|
74
|
+
if error.include?('SecKeychainItemSetAccessWithPassword')
|
|
75
|
+
# Match 会清除被拒绝的缓存;捕获输出以安静处理条目不存在的情况。
|
|
76
|
+
_output, _error, status = Open3.capture3('security', 'delete-internet-password', '-s',
|
|
77
|
+
server_name(keychain_path), '-a', '')
|
|
78
|
+
unless status.success? || status.exitstatus == 44 # errSecItemNotFound
|
|
79
|
+
FastlaneCore::UI.important('未能清除失效的钥匙串密码缓存,请检查钥匙串访问权限;下次可能仍读取旧密码。')
|
|
107
80
|
end
|
|
108
81
|
end
|
|
109
|
-
|
|
110
|
-
# Hiding after Open3 finishes
|
|
111
|
-
# Helper.hide_loading_indicator
|
|
112
|
-
|
|
82
|
+
FastlaneCore::UI.important(warning)
|
|
113
83
|
end
|
|
114
84
|
end
|
|
115
85
|
|
|
116
|
-
# https://github.com/fastlane/fastlane/issues/14196
|
|
117
|
-
# Keychain password is needed to set the partition list to
|
|
118
|
-
# prevent Xcode from prompting dialog for keychain password when signing
|
|
119
|
-
# 1. Uses keychain password from login keychain if found
|
|
120
|
-
# 2. Prompts user for keychain password and stores it in login keychain for user later
|
|
121
86
|
def resolve_keychain_password(keychain_path)
|
|
122
|
-
|
|
123
|
-
server = server_name(keychain_name)
|
|
124
|
-
|
|
125
|
-
# Attempt to find password in keychain for keychain
|
|
87
|
+
server = server_name(keychain_path)
|
|
126
88
|
item = Security::InternetPassword.find(server: server)
|
|
127
|
-
if item
|
|
128
|
-
keychain_password = item.password
|
|
129
|
-
# UI.important("Using keychain password from keychain item #{server} in #{keychain_path}")
|
|
130
|
-
end
|
|
89
|
+
return item.password if item && !item.password.to_s.empty?
|
|
131
90
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# UI.important("This passphrase will be stored in your local keychain with the name #{server} and used in future runs")
|
|
136
|
-
# UI.important("This prompt can be avoided by specifying the 'keychain_password' option or 'MATCH_KEYCHAIN_PASSWORD' environment variable")
|
|
137
|
-
# 收起 spinner,否则密码提示会被动画行覆盖,终端看起来像卡死
|
|
138
|
-
keychain_password = Funlog.with_paused_spinner do
|
|
139
|
-
FastlaneCore::Helper.ask_password(message: "Password for #{keychain_name} keychain: ", confirm: true, confirmation_message: "Type password for #{keychain_name} keychain again: ")
|
|
140
|
-
end
|
|
141
|
-
Security::InternetPassword.add(server, "", keychain_password)
|
|
142
|
-
# else
|
|
143
|
-
# UI.important("Keychain password for #{keychain_path} was not specified and not found in your keychain. Specify the 'keychain_password' option to prevent the UI permission popup when code signing")
|
|
144
|
-
# keychain_password = ""
|
|
145
|
-
# end
|
|
91
|
+
unless $stdin.tty? && FastlaneCore::UI.interactive?
|
|
92
|
+
FastlaneCore::UI.important('未配置或缓存 macOS 钥匙串密码;签名时可能需要系统授权。')
|
|
93
|
+
return ''
|
|
146
94
|
end
|
|
147
95
|
|
|
148
|
-
|
|
96
|
+
name = File.basename(keychain_path, '.*')
|
|
97
|
+
password = Funlog.with_paused_spinner do
|
|
98
|
+
FastlaneCore::Helper.ask_password(
|
|
99
|
+
message: "请输入 macOS #{name} 钥匙串密码(非证书导出密码,将保存到 #{server}):",
|
|
100
|
+
confirm: true, confirmation_message: "请再次输入 #{name} 钥匙串密码:")
|
|
101
|
+
end
|
|
102
|
+
unless Security::InternetPassword.add(server, '', password)
|
|
103
|
+
FastlaneCore::UI.important('未能缓存钥匙串密码,下次运行可能需要重新输入。')
|
|
104
|
+
end
|
|
105
|
+
password
|
|
149
106
|
end
|
|
150
107
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
["pindo", "keychain", keychain_name].join("_")
|
|
108
|
+
def server_name(keychain_path)
|
|
109
|
+
['pindo', 'keychain', File.basename(keychain_path, '.*')].join('_')
|
|
154
110
|
end
|
|
155
|
-
|
|
156
111
|
private :server_name
|
|
157
112
|
end
|
|
158
113
|
end
|
|
@@ -142,7 +142,8 @@ module Pindo
|
|
|
142
142
|
project_dir: @project_path,
|
|
143
143
|
workflow_packname: workflow_packname,
|
|
144
144
|
project_id: project_id,
|
|
145
|
-
bundle_id: @bundle_id
|
|
145
|
+
bundle_id: @bundle_id,
|
|
146
|
+
bundle_id_prefix: Pindo::XcodeBuildConfig.bundle_id_prefix_from(config_bundle_id)
|
|
146
147
|
)
|
|
147
148
|
|
|
148
149
|
# 下载并替换 Icon,优先使用 packageProjectIcon,为空则回退到 projectIcon
|
|
@@ -159,7 +160,7 @@ module Pindo
|
|
|
159
160
|
)
|
|
160
161
|
|
|
161
162
|
unless icon_replace_success
|
|
162
|
-
raise Informative, "Icon
|
|
163
|
+
raise Informative, "Icon 下载或替换失败,具体原因见上方日志(Icon URL: #{icon_url})"
|
|
163
164
|
end
|
|
164
165
|
else
|
|
165
166
|
Funlog.warning("未配置 Icon URL,跳过 Icon 替换")
|
|
@@ -196,8 +197,8 @@ module Pindo
|
|
|
196
197
|
commit_short = commit_hash ? commit_hash[0..7] : "unknown"
|
|
197
198
|
Funlog.instance.fancyinfo_success("自己计算版本号: #{build_version}, Build: #{build_number}, Commit: #{commit_short}")
|
|
198
199
|
else
|
|
199
|
-
# 不是 Git 仓库且没有从 GitCommitTask
|
|
200
|
-
|
|
200
|
+
# 不是 Git 仓库且没有从 GitCommitTask 获取到版本号:沿用工程版本号,但补齐为 JPS 要求的 major.minor.patch
|
|
201
|
+
normalize_project_version
|
|
201
202
|
return
|
|
202
203
|
end
|
|
203
204
|
end
|
|
@@ -210,6 +211,35 @@ module Pindo
|
|
|
210
211
|
)
|
|
211
212
|
end
|
|
212
213
|
|
|
214
|
+
# 非 Git 仓库时沿用工程原有版本号,仅补齐格式(如 Xcode 模板默认的 1.0 => 1.0.0),否则 JPS 拒收
|
|
215
|
+
def normalize_project_version
|
|
216
|
+
version, build_number = Pindo::XcodeBuildConfig.current_app_version(project_dir: @project_path)
|
|
217
|
+
normalized = Pindo::XcodeBuildConfig.normalize_marketing_version(version)
|
|
218
|
+
|
|
219
|
+
if normalized.nil?
|
|
220
|
+
Funlog.warning("非Git仓库,保持工程原有版本号 #{version.inspect};JPS 要求 major.minor.patch 格式,上传可能失败")
|
|
221
|
+
return
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if normalized == version.to_s.strip
|
|
225
|
+
Funlog.warning("非Git仓库且未从 GitCommitTask 获取到版本号,保持工程原有版本号 #{normalized}")
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
Funlog.warning("非Git仓库,工程版本号 #{version} 补齐为 #{normalized}")
|
|
230
|
+
Pindo::XcodeBuildConfig.update_ios_project_version(
|
|
231
|
+
project_dir: @project_path,
|
|
232
|
+
version: normalized,
|
|
233
|
+
build_number: build_number.to_s.strip.empty? ? '1' : build_number
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# 当前环境配置仓库中的主 Bundle ID(如 workhard 为 com.eyesofstorms.*),与证书使用同一来源
|
|
238
|
+
def config_bundle_id
|
|
239
|
+
require 'pindo/config/ios_config_parser'
|
|
240
|
+
Pindo::IosConfigParser.instance.bundle_id
|
|
241
|
+
end
|
|
242
|
+
|
|
213
243
|
# 检查配置
|
|
214
244
|
def pull_config
|
|
215
245
|
# 检查 IosConfigParser 单例中是否已加载配置
|
|
@@ -12,38 +12,69 @@ module Pindo
|
|
|
12
12
|
@project_obj = Xcodeproj::Project.open(proj_fullname)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
DEFAULT_APP_ICON_NAME = "AppIcon".freeze
|
|
16
|
+
APP_ICON_NAME_SETTING = "ASSETCATALOG_COMPILER_APPICON_NAME".freeze
|
|
17
|
+
|
|
18
|
+
# 查找工程的 App 图标集目录(<图标名>.appiconset),找不到返回 nil(不抛异常)
|
|
16
19
|
def find_xcodeproj_icon_path()
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
icon_dir_name = "#{app_icon_name}.appiconset"
|
|
21
|
+
|
|
22
|
+
# 优先在主 target 自己的 asset catalog 中查找
|
|
23
|
+
icon_path = main_target_asset_catalogs
|
|
24
|
+
.map { |catalog| File.join(catalog, icon_dir_name) }
|
|
25
|
+
.find { |path| File.directory?(path) }
|
|
26
|
+
return icon_path if icon_path
|
|
27
|
+
|
|
28
|
+
icon_path_array = Dir.glob(File.join(@project_obj.project_dir, "**", icon_dir_name))
|
|
29
|
+
if icon_path_array.size > 1
|
|
30
|
+
icon_path = icon_path_array.find{ |filename| File.directory?(filename) && filename.include?("Assets.xcassets/#{icon_dir_name}")}
|
|
31
|
+
elsif icon_path_array.size == 1
|
|
32
|
+
icon_path = icon_path_array.first
|
|
33
|
+
end
|
|
19
34
|
|
|
20
|
-
|
|
35
|
+
return nil if icon_path.nil? || icon_path.empty? || !File.exist?(icon_path)
|
|
36
|
+
icon_path
|
|
37
|
+
end
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
if icon_path.nil? || icon_path.empty? || !File.exist?(icon_path)
|
|
34
|
-
icon_path_array = Dir.glob(File.join(project_dir, "**", "AppIcon.appiconset"))
|
|
35
|
-
if icon_path_array.size > 1
|
|
36
|
-
icon_path = icon_path_array.find{ |filename| File.directory?(filename) && filename.include?("Assets.xcassets/AppIcon.appiconset")}
|
|
37
|
-
elsif icon_path_array.size == 1
|
|
38
|
-
icon_path = icon_path_array.first
|
|
39
|
-
end
|
|
39
|
+
# 查找 App 图标集目录;工程没有图标集时(如 Xcode 新模板工程),
|
|
40
|
+
# 在主 target 的 asset catalog 中创建,并设置 ASSETCATALOG_COMPILER_APPICON_NAME
|
|
41
|
+
def ensure_app_icon_path()
|
|
42
|
+
icon_path = find_xcodeproj_icon_path()
|
|
43
|
+
return icon_path if icon_path
|
|
44
|
+
|
|
45
|
+
catalog = main_target_asset_catalogs.first
|
|
46
|
+
if catalog.nil?
|
|
47
|
+
raise Informative, "没有找到Xcode icon 目录:主 target 中既没有 #{app_icon_name}.appiconset,也没有可存放图标的 .xcassets"
|
|
40
48
|
end
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
icon_path = File.join(catalog, "#{app_icon_name}.appiconset")
|
|
51
|
+
FileUtils.mkdir_p(icon_path)
|
|
52
|
+
assign_app_icon_name
|
|
53
|
+
puts " ✓ 工程中没有 App 图标集,已创建: #{icon_path}"
|
|
54
|
+
icon_path
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 主 App target 配置的图标名,未配置时为 AppIcon
|
|
58
|
+
def app_icon_name()
|
|
59
|
+
names = main_app_target&.build_configurations&.map { |config| config.build_settings[APP_ICON_NAME_SETTING] } || []
|
|
60
|
+
name = names.compact.map(&:to_s).find { |value| !value.strip.empty? }
|
|
61
|
+
name || DEFAULT_APP_ICON_NAME
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 主 App target 使用的 asset catalog(含文件夹同步方式管理的资源)
|
|
65
|
+
def main_target_asset_catalogs()
|
|
66
|
+
target = main_app_target
|
|
67
|
+
return [] if target.nil?
|
|
68
|
+
|
|
69
|
+
referenced = target.resources_build_phase.files_references
|
|
70
|
+
.select { |file| file.respond_to?(:real_path) && file.real_path.to_s.end_with?(".xcassets") }
|
|
71
|
+
.map { |file| file.real_path.to_s }
|
|
72
|
+
|
|
73
|
+
synchronized = synchronized_group_paths(target).flat_map do |group_path|
|
|
74
|
+
Dir.glob(File.join(group_path, "**", "*.xcassets")).sort
|
|
44
75
|
end
|
|
45
76
|
|
|
46
|
-
|
|
77
|
+
(referenced + synchronized).uniq.select { |path| File.directory?(path) }
|
|
47
78
|
end
|
|
48
79
|
|
|
49
80
|
# 工程是否包含 AppIcon 目录(macOS 命令行工具等无 icon 工程返回 false)
|
|
@@ -68,7 +99,7 @@ module Pindo
|
|
|
68
99
|
end
|
|
69
100
|
|
|
70
101
|
def install_icon_res(new_icon_dir:nil)
|
|
71
|
-
icon_path =
|
|
102
|
+
icon_path = ensure_app_icon_path()
|
|
72
103
|
begin
|
|
73
104
|
FileUtils.rm_rf(icon_path)
|
|
74
105
|
FileUtils.mkdir_p(icon_path)
|
|
@@ -265,6 +296,36 @@ module Pindo
|
|
|
265
296
|
|
|
266
297
|
end
|
|
267
298
|
|
|
299
|
+
private
|
|
300
|
+
|
|
301
|
+
def main_app_target()
|
|
302
|
+
@project_obj.targets.find do |target|
|
|
303
|
+
target.respond_to?(:product_type) && target.product_type.to_s.include?(Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application])
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# 文件夹同步(PBXFileSystemSynchronizedRootGroup)方式管理的 target 目录
|
|
308
|
+
def synchronized_group_paths(target)
|
|
309
|
+
return [] unless target.respond_to?(:file_system_synchronized_groups)
|
|
310
|
+
|
|
311
|
+
target.file_system_synchronized_groups
|
|
312
|
+
.map { |group| Xcodeproj::Project::Object::GroupableHelper.real_path(group).to_s }
|
|
313
|
+
.select { |path| File.directory?(path) }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# 未配置图标名的 target 补上 ASSETCATALOG_COMPILER_APPICON_NAME,使图标集参与编译
|
|
317
|
+
def assign_app_icon_name()
|
|
318
|
+
target = main_app_target
|
|
319
|
+
return if target.nil?
|
|
320
|
+
|
|
321
|
+
name = app_icon_name
|
|
322
|
+
missing = target.build_configurations.select { |config| config.build_settings[APP_ICON_NAME_SETTING].to_s.strip.empty? }
|
|
323
|
+
return if missing.empty?
|
|
324
|
+
|
|
325
|
+
missing.each { |config| config.build_settings[APP_ICON_NAME_SETTING] = name }
|
|
326
|
+
@project_obj.save
|
|
327
|
+
end
|
|
328
|
+
|
|
268
329
|
end
|
|
269
330
|
|
|
270
331
|
end
|
|
@@ -21,6 +21,11 @@ module Pindo
|
|
|
21
21
|
/func\s+application\s*\([^)]*configurationForConnecting\b/
|
|
22
22
|
).freeze
|
|
23
23
|
|
|
24
|
+
# SwiftUI App 生命周期(@main 入口遵循 App 协议)本身基于 UIScene
|
|
25
|
+
SWIFTUI_APP_PATTERN = /^[ \t]*@main\b[^{]*?\b(?:struct|class|enum)\s+\w+\s*:[^{]*\bApp\b/.freeze
|
|
26
|
+
|
|
27
|
+
SCENE_ADOPTION_SOURCE_PATTERN = Regexp.union(SCENE_CONFIGURATION_PATTERN, SWIFTUI_APP_PATTERN).freeze
|
|
28
|
+
|
|
24
29
|
SOURCE_EXTENSIONS = %w[.m .mm .swift].freeze
|
|
25
30
|
|
|
26
31
|
EXCLUDED_DIRECTORY_NAMES = %w[.git Pods build DerivedData].freeze
|
|
@@ -47,7 +52,7 @@ module Pindo
|
|
|
47
52
|
)
|
|
48
53
|
return if scene_manifest_declared?(info_plist_path)
|
|
49
54
|
return if scene_manifest_generated?(release_config)
|
|
50
|
-
return if
|
|
55
|
+
return if sources_adopt_scene_lifecycle?(project_dir)
|
|
51
56
|
|
|
52
57
|
raise Informative, adoption_error_message(sdk_version, info_plist_path)
|
|
53
58
|
end
|
|
@@ -91,13 +96,19 @@ module Pindo
|
|
|
91
96
|
# Xcode 只在 GENERATE_INFOPLIST_FILE = YES 时采用 INFOPLIST_KEY_* 设置
|
|
92
97
|
def scene_manifest_generated?(release_config)
|
|
93
98
|
settings = release_config.build_settings
|
|
94
|
-
settings
|
|
95
|
-
settings
|
|
99
|
+
iphoneos_setting(settings, 'GENERATE_INFOPLIST_FILE').to_s.casecmp?('YES') &&
|
|
100
|
+
iphoneos_setting(settings, SCENE_MANIFEST_GENERATION_SETTING).to_s.casecmp?('YES')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# iOS 真机构建实际生效的值:[sdk=iphoneos*] 条件设置优先于无条件设置(多平台模板只写条件设置)
|
|
104
|
+
def iphoneos_setting(settings, key)
|
|
105
|
+
_name, conditional = settings.find { |name, _value| name.start_with?("#{key}[sdk=iphoneos") }
|
|
106
|
+
conditional.nil? ? settings[key] : conditional
|
|
96
107
|
end
|
|
97
108
|
|
|
98
|
-
def
|
|
109
|
+
def sources_adopt_scene_lifecycle?(project_dir)
|
|
99
110
|
each_source_file(project_dir).any? do |path|
|
|
100
|
-
File.binread(path).match?(
|
|
111
|
+
File.binread(path).match?(SCENE_ADOPTION_SOURCE_PATTERN)
|
|
101
112
|
end
|
|
102
113
|
end
|
|
103
114
|
|
|
@@ -11,6 +11,18 @@ module Pindo
|
|
|
11
11
|
|
|
12
12
|
class XcodeBuildConfig
|
|
13
13
|
|
|
14
|
+
# 未提供任何 Bundle ID 线索时,工作流推导 Bundle ID 使用的前缀
|
|
15
|
+
DEFAULT_BUNDLE_ID_PREFIX = "com.heroneverdie101".freeze
|
|
16
|
+
|
|
17
|
+
# 从 Bundle ID 提取前缀:通配 ID 去掉 ".*",具体 ID 去掉最后一段
|
|
18
|
+
# @param bundle_id [String, nil] 如 "com.example.*" 或 "com.example.app"
|
|
19
|
+
# @return [String, nil] 如 "com.example";无法提取时返回 nil
|
|
20
|
+
def self.bundle_id_prefix_from(bundle_id)
|
|
21
|
+
value = bundle_id.to_s.strip
|
|
22
|
+
prefix = value.end_with?(".*") ? value.chomp(".*") : value.rpartition(".").first
|
|
23
|
+
prefix.empty? || prefix.include?("*") ? nil : prefix
|
|
24
|
+
end
|
|
25
|
+
|
|
14
26
|
# 使用package_name一次性更新Display Name、Bundle ID和URL Schemes(基于package_name)
|
|
15
27
|
# 优化版本:只读写一次plist文件,提高性能
|
|
16
28
|
# 注意:此函数只添加基于package_name的URL Scheme,基于实际Bundle ID的URL Scheme
|
|
@@ -19,8 +31,9 @@ module Pindo
|
|
|
19
31
|
# @param workflow_packname [String] 工作流的名称(如:"Test Demo")
|
|
20
32
|
# @param project_id [String] JPS项目ID(可选,用于添加快捷操作)
|
|
21
33
|
# @param bundle_id [String] 显式指定的 Bundle ID(可选,来自 --bundleid)
|
|
34
|
+
# @param bundle_id_prefix [String] 工作流推导 Bundle ID 的前缀(可选,如 "com.example")
|
|
22
35
|
# @return [Boolean] 是否成功更新
|
|
23
|
-
def self.update_project_with_workflow(project_dir: nil, workflow_packname: nil, project_id: nil, bundle_id: nil)
|
|
36
|
+
def self.update_project_with_workflow(project_dir: nil, workflow_packname: nil, project_id: nil, bundle_id: nil, bundle_id_prefix: nil)
|
|
24
37
|
raise ArgumentError, "项目目录不能为空" if project_dir.nil?
|
|
25
38
|
raise ArgumentError, "Workflow Package Name不能为空" if workflow_packname.nil?
|
|
26
39
|
|
|
@@ -30,10 +43,14 @@ module Pindo
|
|
|
30
43
|
|
|
31
44
|
# Bundle ID 优先使用显式传入的值(--bundleid),未传时按工作流名推导。
|
|
32
45
|
# 通配符(如 com.example.*)不能作为工程的 PRODUCT_BUNDLE_IDENTIFIER,
|
|
33
|
-
# 与 BaseCertOperator#configure_target_build_settings
|
|
46
|
+
# 与 BaseCertOperator#configure_target_build_settings 的处理保持一致,退回推导值,
|
|
47
|
+
# 推导值的前缀依次取 bundle_id_prefix、通配 ID 的前缀、默认前缀。
|
|
34
48
|
explicit_bundle_id = bundle_id.to_s.strip
|
|
35
49
|
use_explicit_bundle_id = !explicit_bundle_id.empty? && !explicit_bundle_id.include?("*")
|
|
36
|
-
|
|
50
|
+
derived_prefix = bundle_id_prefix.to_s.strip
|
|
51
|
+
derived_prefix = bundle_id_prefix_from(explicit_bundle_id).to_s if derived_prefix.empty?
|
|
52
|
+
derived_prefix = DEFAULT_BUNDLE_ID_PREFIX if derived_prefix.empty?
|
|
53
|
+
final_bundle_id = use_explicit_bundle_id ? explicit_bundle_id : "#{derived_prefix}.#{package_scheme}"
|
|
37
54
|
|
|
38
55
|
project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
|
|
39
56
|
return false if project_fullname.nil?
|
|
@@ -315,6 +332,33 @@ module Pindo
|
|
|
315
332
|
# @param version [String] 版本号
|
|
316
333
|
# @param build_number [Integer] Build号
|
|
317
334
|
# @return [Boolean] 是否成功更新
|
|
335
|
+
# 把纯数字版本补齐为 major.minor.patch(JPS 要求的格式),如 "1.0" => "1.0.0"
|
|
336
|
+
# @param version [String, nil] 工程中的版本号
|
|
337
|
+
# @return [String, nil] 补齐后的版本号;含变量、后缀或超过三段时返回 nil
|
|
338
|
+
def self.normalize_marketing_version(version)
|
|
339
|
+
value = version.to_s.strip
|
|
340
|
+
return nil unless value.match?(/\A\d+(\.\d+){0,2}\z/)
|
|
341
|
+
|
|
342
|
+
segments = value.split(".")
|
|
343
|
+
(segments + ["0"] * (3 - segments.size)).join(".")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# 读取主 App target 当前的版本号与 Build 号(Release 配置优先)
|
|
347
|
+
# @param project_dir [String] 项目目录
|
|
348
|
+
# @return [Array(String, String)] [MARKETING_VERSION, CURRENT_PROJECT_VERSION],取不到时为 nil
|
|
349
|
+
def self.current_app_version(project_dir:)
|
|
350
|
+
xcodeproj_path = find_xcodeproj(project_dir)
|
|
351
|
+
return [nil, nil] if xcodeproj_path.nil?
|
|
352
|
+
|
|
353
|
+
target = Xcodeproj::Project.open(xcodeproj_path).targets.find do |t|
|
|
354
|
+
t.respond_to?(:product_type) && t.product_type.to_s.eql?("com.apple.product-type.application")
|
|
355
|
+
end
|
|
356
|
+
return [nil, nil] if target.nil?
|
|
357
|
+
|
|
358
|
+
config = target.build_configurations.find { |c| c.name == "Release" } || target.build_configurations.first
|
|
359
|
+
[config.build_settings['MARKETING_VERSION'], config.build_settings['CURRENT_PROJECT_VERSION']]
|
|
360
|
+
end
|
|
361
|
+
|
|
318
362
|
def self.update_ios_project_version(project_dir: nil, version: nil, build_number: nil)
|
|
319
363
|
raise ArgumentError, "项目目录不能为空" if project_dir.nil?
|
|
320
364
|
raise ArgumentError, "版本号不能为空" if version.nil?
|
data/lib/pindo/version.rb
CHANGED
data/lib/pindo.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pindo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.21.
|
|
4
|
+
version: 5.21.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- wade
|
|
@@ -272,6 +272,7 @@ files:
|
|
|
272
272
|
- lib/pindo/base/plaininformative.rb
|
|
273
273
|
- lib/pindo/base/plistbuddyexecutable.rb
|
|
274
274
|
- lib/pindo/base/standarderror.rb
|
|
275
|
+
- lib/pindo/base/xcodeproj_compat.rb
|
|
275
276
|
- lib/pindo/client/applovinclient.rb
|
|
276
277
|
- lib/pindo/client/aws3sclient.rb
|
|
277
278
|
- lib/pindo/client/bossclient.rb
|