pindo 5.21.0 → 5.21.2
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 +5 -1
- data/lib/pindo/base/funlog.rb +66 -5
- data/lib/pindo/module/cert/keychain_helper.rb +16 -9
- data/lib/pindo/module/cert/mode/custom_https_cert_operator.rb +102 -60
- data/lib/pindo/module/cert/mode/match_git_cert_operator.rb +3 -1
- data/lib/pindo/module/cert/p12_material.rb +68 -0
- data/lib/pindo/module/xcode/scene_lifecycle_guard.rb +129 -0
- data/lib/pindo/module/xcode/xcode_build_helper.rb +14 -0
- data/lib/pindo/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fc2d66a2010bba4e00fe508252a291bac2efda4623ded2135bf707b6f93ade0
|
|
4
|
+
data.tar.gz: cfca3acb943122590ffeba33d95249c2d73143b7e51ff87e74ef5342e44dd67f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 652bf861a3896a3763e38ff0f7142128dace9d221350db4a353b1f6ad22fd19dc5661ac926825438cf5dae7fdf287221b3888b8b84d6dccf6d54be024bac759b
|
|
7
|
+
data.tar.gz: bfd8a3891dc78f37fa1bccec6a67eb9dd7f6f8a4f4cac86d95788d0d64b63e938dd6385579ac3d0bc50141629bc236f2bf4a16d69f597b7036f8a1b497c03cb4
|
data/lib/pindo/base/aeshelper.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
require 'openssl'
|
|
3
3
|
require 'match'
|
|
4
4
|
require 'pindo/base/informative'
|
|
5
|
+
require 'pindo/base/funlog'
|
|
5
6
|
|
|
6
7
|
module Pindo
|
|
7
8
|
|
|
@@ -62,7 +63,10 @@ module Pindo
|
|
|
62
63
|
|
|
63
64
|
unless password
|
|
64
65
|
puts "\e[33m[DEBUG] Keychain中未找到密码,需要用户输入: #{server_name}\e[0m" if ENV['PINDO_DEBUG']
|
|
65
|
-
|
|
66
|
+
# 收起 spinner,否则密码提示会被动画行覆盖,终端看起来像卡死
|
|
67
|
+
password = Funlog.with_paused_spinner do
|
|
68
|
+
FastlaneCore::Helper.ask_password(message: "请输入证书仓库的加密密码: ", confirm: true)
|
|
69
|
+
end
|
|
66
70
|
puts "\e[33m[DEBUG] 用户输入密码成功,等待验证后保存到Keychain\e[0m" if ENV['PINDO_DEBUG']
|
|
67
71
|
else
|
|
68
72
|
puts "\e[32m[DEBUG] 从Keychain获取密码成功: #{server_name}\e[0m" if ENV['PINDO_DEBUG']
|
data/lib/pindo/base/funlog.rb
CHANGED
|
@@ -36,32 +36,59 @@ module Pindo
|
|
|
36
36
|
@spinner_log
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# 临时让出终端,执行需要用户交互输入的代码块
|
|
40
|
+
#
|
|
41
|
+
# spinner 在后台线程中持续重绘同一行,会覆盖掉 fastlane、HighLine 等
|
|
42
|
+
# 交互提示(如 match passphrase、Apple ID 2FA、证书仓库密码),
|
|
43
|
+
# 导致终端看起来像卡死。执行交互代码前必须先收起 spinner。
|
|
44
|
+
#
|
|
45
|
+
# @yield 需要交互输入的代码块
|
|
46
|
+
# @return [Object] 代码块的返回值
|
|
47
|
+
def with_paused_spinner
|
|
48
|
+
return yield if @spinner_log.nil?
|
|
49
|
+
|
|
50
|
+
title = @spinner_title
|
|
51
|
+
@spinner_log.stop
|
|
52
|
+
@spinner_log = nil
|
|
39
53
|
|
|
54
|
+
begin
|
|
55
|
+
yield
|
|
56
|
+
ensure
|
|
57
|
+
fancyinfo_start(title) unless title.nil? || title.empty?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
40
60
|
|
|
41
61
|
def fancyinfo_start(*args)
|
|
42
62
|
message = Funlog.ensure_utf8(args.join(" "))
|
|
43
|
-
|
|
63
|
+
@spinner_title = message
|
|
64
|
+
|
|
44
65
|
# 检查是否在任务上下文中
|
|
45
66
|
if Funlog.in_task_context?
|
|
46
67
|
# 并发模式:路由到输出管理器(只写日志文件)
|
|
47
68
|
Funlog.current_output_sink.log_info(Funlog.current_task_id, message)
|
|
69
|
+
elsif Funlog.spinner_disabled?
|
|
70
|
+
# 禁用动画:只输出一次静态行,不占用终端
|
|
71
|
+
puts " #{message}"
|
|
48
72
|
else
|
|
49
73
|
# 串行模式:使用 spinner
|
|
50
74
|
spinner_log_handle.update(title:message)
|
|
51
75
|
spinner_log_handle.auto_spin
|
|
52
76
|
end
|
|
53
|
-
|
|
77
|
+
|
|
54
78
|
@spinner_log
|
|
55
79
|
end
|
|
56
80
|
|
|
57
81
|
|
|
58
82
|
def fancyinfo_update(*args)
|
|
59
83
|
message = Funlog.ensure_utf8(args.join(" "))
|
|
84
|
+
@spinner_title = message
|
|
60
85
|
|
|
61
86
|
# 检查是否在任务上下文中
|
|
62
87
|
if Funlog.in_task_context?
|
|
63
88
|
# 并发模式:不更新终端,只写日志
|
|
64
89
|
Funlog.current_output_sink.log_info(Funlog.current_task_id, message)
|
|
90
|
+
elsif Funlog.spinner_disabled?
|
|
91
|
+
puts " #{message}"
|
|
65
92
|
else
|
|
66
93
|
# 串行模式:使用 spinner
|
|
67
94
|
spinner_log_handle.update(title:message)
|
|
@@ -73,25 +100,31 @@ module Pindo
|
|
|
73
100
|
|
|
74
101
|
def fancyinfo_success(*args)
|
|
75
102
|
message = Funlog.ensure_utf8(args.join(" "))
|
|
103
|
+
return finish_without_spinner("\e[32m ✓ #{message}\e[0m") if Funlog.spinner_disabled?
|
|
104
|
+
|
|
76
105
|
spinner_log_handle.update(title:message)
|
|
77
106
|
spinner_log_handle.success
|
|
78
|
-
|
|
107
|
+
reset_spinner_state
|
|
79
108
|
end
|
|
80
109
|
|
|
81
110
|
def fancyinfo_error(*args)
|
|
82
111
|
message = Funlog.ensure_utf8(args.join(" "))
|
|
112
|
+
return finish_without_spinner("\e[31m ✗ #{message}\e[0m") if Funlog.spinner_disabled?
|
|
113
|
+
|
|
83
114
|
spinner_log_handle.update(title:message)
|
|
84
115
|
spinner_log_handle.error
|
|
85
|
-
|
|
116
|
+
reset_spinner_state
|
|
86
117
|
end
|
|
87
118
|
|
|
88
119
|
# 输出警告信息(使用spinner,黄色警告标记)
|
|
89
120
|
# @param args [Array] 要输出的消息
|
|
90
121
|
def fancyinfo_warning(*args)
|
|
91
122
|
message = Funlog.ensure_utf8(args.join(" "))
|
|
123
|
+
return finish_without_spinner("\e[33m ⚠ #{message}\e[0m") if Funlog.spinner_disabled?
|
|
124
|
+
|
|
92
125
|
spinner_log_handle.update(title:message)
|
|
93
126
|
spinner_log_handle.stop("\e[33m⚠\e[0m")
|
|
94
|
-
|
|
127
|
+
reset_spinner_state
|
|
95
128
|
end
|
|
96
129
|
|
|
97
130
|
# 输出静态成功信息(不使用spinner,对应 fancyinfo_success)
|
|
@@ -139,6 +172,21 @@ module Pindo
|
|
|
139
172
|
end
|
|
140
173
|
end
|
|
141
174
|
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
# 禁用动画时输出静态收尾行
|
|
178
|
+
# @param line [String] 已经带好颜色和标记的整行文本
|
|
179
|
+
def finish_without_spinner(line)
|
|
180
|
+
puts line
|
|
181
|
+
reset_spinner_state
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# 清空 spinner 状态,等待下一次 fancyinfo_start 重新创建
|
|
185
|
+
def reset_spinner_state
|
|
186
|
+
@spinner_log = nil
|
|
187
|
+
@spinner_title = nil
|
|
188
|
+
end
|
|
189
|
+
|
|
142
190
|
#-------------------------------------------------------------------------#
|
|
143
191
|
#-------------------------------------------------------------------------#
|
|
144
192
|
# @!group Singleton
|
|
@@ -185,6 +233,19 @@ module Pindo
|
|
|
185
233
|
!current_output_sink.nil? && !current_task_id.nil?
|
|
186
234
|
end
|
|
187
235
|
|
|
236
|
+
# 检查是否禁用了 spinner 动画
|
|
237
|
+
# @return [Boolean] PINDO_NO_SPINNER=1 时禁用
|
|
238
|
+
def spinner_disabled?
|
|
239
|
+
ENV['PINDO_NO_SPINNER'] == '1'
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# 临时让出终端执行交互输入,详见实例方法说明
|
|
243
|
+
# @yield 需要交互输入的代码块
|
|
244
|
+
# @return [Object] 代码块的返回值
|
|
245
|
+
def with_paused_spinner(&block)
|
|
246
|
+
instance.with_paused_spinner(&block)
|
|
247
|
+
end
|
|
248
|
+
|
|
188
249
|
# ============================================
|
|
189
250
|
# 类方法委托,简化调用方式
|
|
190
251
|
# 使用 Funlog.info("消息") 代替 Funlog.instance.info("消息")
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
|
|
2
2
|
require 'open3'
|
|
3
3
|
require 'security'
|
|
4
|
+
require 'pindo/base/funlog'
|
|
4
5
|
|
|
5
6
|
module Pindo
|
|
6
7
|
|
|
7
8
|
class KeychainHelper
|
|
8
9
|
class << self
|
|
9
10
|
|
|
10
|
-
def import_file(path, keychain_path, keychain_password: nil, certificate_password: "", skip_set_partition_list: false, output: false)
|
|
11
|
+
def import_file(path, keychain_path, keychain_password: nil, certificate_password: "", skip_set_partition_list: false, output: false, strict: false)
|
|
11
12
|
# puts path
|
|
12
13
|
|
|
13
14
|
# puts "Could not find file '#{path}'" unless File.exist?(path)
|
|
14
15
|
|
|
15
16
|
password_part = " -P #{certificate_password.shellescape}"
|
|
16
17
|
|
|
17
|
-
command = "security import #{path.shellescape} -k
|
|
18
|
+
command = "security import #{path.shellescape} -k #{keychain_path.shellescape}"
|
|
18
19
|
command << password_part
|
|
19
20
|
command << " -T /usr/bin/codesign" # to not be asked for permission when running a tool like `gym` (before Sierra)
|
|
20
21
|
command << " -T /usr/bin/security"
|
|
@@ -30,20 +31,22 @@ module Pindo
|
|
|
30
31
|
# UI.command_output(stdout.read.to_s) if output
|
|
31
32
|
# puts "123"
|
|
32
33
|
# Set partition list only if success since it can be a time consuming process if a lot of keys are installed
|
|
33
|
-
if thrd.value.success?
|
|
34
|
+
if thrd.value.success?
|
|
34
35
|
# puts "安装成功!"
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
unless skip_set_partition_list
|
|
37
|
+
keychain_password ||= resolve_keychain_password(keychain_path)
|
|
38
|
+
set_partition_list(path, keychain_path, keychain_password: keychain_password, output: output, strict: strict)
|
|
39
|
+
end
|
|
37
40
|
else
|
|
38
41
|
# Output verbose if file is already installed since not an error otherwise we will show the whole error
|
|
39
42
|
err = stderr.read.to_s.strip
|
|
40
43
|
|
|
41
44
|
if err.include?("SecKeychainItemImport") && err.include?("The specified item already exists in the keychain")
|
|
45
|
+
raise Informative, "钥匙串已存在相关材料,无法确认私钥访问权限;请检查签名身份及访问配置", cause: nil if strict
|
|
42
46
|
# UI.verbose("'}' is already installed on this machine")
|
|
43
47
|
# puts "证书key已经存在,安装完成!"
|
|
44
48
|
else
|
|
45
|
-
|
|
46
|
-
# raise Informative, err
|
|
49
|
+
raise Informative, "钥匙串导入失败(访问权限、钥匙串状态或 P12 格式不受支持)", cause: nil if strict
|
|
47
50
|
end
|
|
48
51
|
end
|
|
49
52
|
end
|
|
@@ -56,7 +59,7 @@ module Pindo
|
|
|
56
59
|
return result
|
|
57
60
|
end
|
|
58
61
|
|
|
59
|
-
def set_partition_list(path, keychain_path, keychain_password: nil, output: false)
|
|
62
|
+
def set_partition_list(path, keychain_path, keychain_password: nil, output: false, strict: false)
|
|
60
63
|
# When security supports partition lists, also add the partition IDs
|
|
61
64
|
# See https://openradar.appspot.com/28524119
|
|
62
65
|
if help_backticks('security -h | grep set-key-partition-list', print: false).length > 0
|
|
@@ -79,6 +82,7 @@ module Pindo
|
|
|
79
82
|
Open3.popen3(command) do |stdin, stdout, stderr, thrd|
|
|
80
83
|
unless thrd.value.success?
|
|
81
84
|
err = stderr.read.to_s.strip
|
|
85
|
+
raise Informative, "钥匙串访问配置失败,请检查 macOS 钥匙串密码", cause: nil if strict
|
|
82
86
|
|
|
83
87
|
# Inform user when no/wrong password was used as its needed to prevent UI permission popup from Xcode when signing
|
|
84
88
|
if err.include?("SecKeychainItemSetAccessWithPassword")
|
|
@@ -130,7 +134,10 @@ module Pindo
|
|
|
130
134
|
# UI.important("Enter the password for #{keychain_path}")
|
|
131
135
|
# UI.important("This passphrase will be stored in your local keychain with the name #{server} and used in future runs")
|
|
132
136
|
# UI.important("This prompt can be avoided by specifying the 'keychain_password' option or 'MATCH_KEYCHAIN_PASSWORD' environment variable")
|
|
133
|
-
|
|
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
|
|
134
141
|
Security::InternetPassword.add(server, "", keychain_password)
|
|
135
142
|
# else
|
|
136
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")
|
|
@@ -6,6 +6,9 @@ require 'pindo/module/cert/keychain_helper'
|
|
|
6
6
|
require 'pindo/module/cert/provisioning_helper'
|
|
7
7
|
require 'pindo/module/utils/file_downloader'
|
|
8
8
|
require 'jpsclient'
|
|
9
|
+
require 'json'
|
|
10
|
+
require 'digest'
|
|
11
|
+
require 'pindo/module/cert/p12_material'
|
|
9
12
|
|
|
10
13
|
module Pindo
|
|
11
14
|
module CertProcess
|
|
@@ -51,7 +54,8 @@ module Pindo
|
|
|
51
54
|
bundle_id_array: bundle_id_array,
|
|
52
55
|
cert_type: cert_type,
|
|
53
56
|
platform_type: platform_type,
|
|
54
|
-
temp_dir: temp_dir
|
|
57
|
+
temp_dir: temp_dir,
|
|
58
|
+
signing_certificate: cert_info[:certificate]
|
|
55
59
|
)
|
|
56
60
|
|
|
57
61
|
# 6. 验证并保存证书信息,提取 Team ID
|
|
@@ -78,14 +82,7 @@ module Pindo
|
|
|
78
82
|
provisioning_info_array
|
|
79
83
|
|
|
80
84
|
ensure
|
|
81
|
-
|
|
82
|
-
if temp_dir && Dir.exist?(temp_dir)
|
|
83
|
-
if debug_mode?
|
|
84
|
-
Funlog.instance.info("调试模式:临时目录已保留: #{temp_dir}")
|
|
85
|
-
else
|
|
86
|
-
FileUtils.remove_entry_secure(temp_dir)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
85
|
+
FileUtils.remove_entry_secure(temp_dir) if temp_dir && Dir.exist?(temp_dir)
|
|
89
86
|
end
|
|
90
87
|
end
|
|
91
88
|
|
|
@@ -118,6 +115,17 @@ module Pindo
|
|
|
118
115
|
end
|
|
119
116
|
end
|
|
120
117
|
|
|
118
|
+
# JPS_DEBUG 会打印原始响应(含授权下载 URL);证书接口必须禁用。
|
|
119
|
+
def certificate_api(method, **parameters)
|
|
120
|
+
previous_debug = ENV['JPS_DEBUG']
|
|
121
|
+
ENV['JPS_DEBUG'] = '0'
|
|
122
|
+
@jps_client.public_send(method, **parameters)
|
|
123
|
+
rescue ::StandardError
|
|
124
|
+
raise Informative, "证书接口请求失败,请检查接口权限和网络", cause: nil
|
|
125
|
+
ensure
|
|
126
|
+
previous_debug.nil? ? ENV.delete('JPS_DEBUG') : ENV['JPS_DEBUG'] = previous_debug
|
|
127
|
+
end
|
|
128
|
+
|
|
121
129
|
# 下载并安装证书
|
|
122
130
|
def download_and_install_certificates(apple_id:, bundle_id_array:, cert_type:, platform_type:, temp_dir:)
|
|
123
131
|
# 映射到 JPS API 参数
|
|
@@ -130,7 +138,7 @@ module Pindo
|
|
|
130
138
|
|
|
131
139
|
Funlog.instance.info("查询证书: apple_id=#{apple_id}, platform=#{platform}, profile_type=#{profile_type}") if debug_mode?
|
|
132
140
|
|
|
133
|
-
result =
|
|
141
|
+
result = certificate_api(:get_apple_cert_list,
|
|
134
142
|
apple_dev_account: apple_id,
|
|
135
143
|
bundle_id: main_bundle_id,
|
|
136
144
|
platform: platform,
|
|
@@ -152,8 +160,7 @@ module Pindo
|
|
|
152
160
|
cert_url = cert_data['cerKeyUrl'] # API 返回字段: cerKeyUrl
|
|
153
161
|
cert_p12_url = cert_data['privateKeyUrl'] # API 返回字段: privateKeyUrl
|
|
154
162
|
cert_name = cert_data['name'] # API 返回字段: name
|
|
155
|
-
|
|
156
|
-
cert_password = ''
|
|
163
|
+
|
|
157
164
|
|
|
158
165
|
Funlog.instance.info("找到证书: #{cert_name}") if debug_mode?
|
|
159
166
|
|
|
@@ -176,18 +183,21 @@ module Pindo
|
|
|
176
183
|
download_file_from_url(url: cert_p12_url, dest_path: key_path)
|
|
177
184
|
Funlog.instance.info("私钥文件路径: #{key_path}") if debug_mode?
|
|
178
185
|
|
|
186
|
+
cert_password = p12_export_password(key_path)
|
|
187
|
+
material = P12Material.load(cert_path: cert_path, key_path: key_path, password: cert_password)
|
|
188
|
+
|
|
179
189
|
# 安装证书到 Keychain(明文,无需 AES 解密)
|
|
180
190
|
install_certificate_to_keychain(cert_path: cert_path, key_path: key_path, cert_password: cert_password)
|
|
181
191
|
|
|
182
192
|
return {
|
|
183
193
|
cert_path: cert_path,
|
|
184
|
-
|
|
194
|
+
certificate: material.certificate,
|
|
185
195
|
cert_name: cert_name
|
|
186
196
|
}
|
|
187
197
|
end
|
|
188
198
|
|
|
189
199
|
# 下载并安装描述文件
|
|
190
|
-
def download_and_install_profiles(apple_id:, bundle_id_array:, cert_type:, platform_type:, temp_dir:)
|
|
200
|
+
def download_and_install_profiles(apple_id:, bundle_id_array:, cert_type:, platform_type:, temp_dir:, signing_certificate:)
|
|
191
201
|
# 映射到 JPS API 参数
|
|
192
202
|
jps_params = map_to_jps_api_params(cert_type: cert_type, platform_type: platform_type)
|
|
193
203
|
platform = jps_params[:platform]
|
|
@@ -205,7 +215,7 @@ module Pindo
|
|
|
205
215
|
|
|
206
216
|
# 一次性查询所有 bundle_id 的描述文件
|
|
207
217
|
Funlog.instance.info("查询描述文件: bundle_ids=#{bundle_ids.join(', ')}") if debug_mode?
|
|
208
|
-
result =
|
|
218
|
+
result = certificate_api(:get_apple_profile_list,
|
|
209
219
|
apple_dev_account: apple_id,
|
|
210
220
|
bundle_ids: bundle_ids, # 修改为 bundle_ids,传递数组
|
|
211
221
|
platform: platform,
|
|
@@ -252,16 +262,8 @@ module Pindo
|
|
|
252
262
|
}
|
|
253
263
|
end
|
|
254
264
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
tasks: download_tasks,
|
|
258
|
-
max_threads: 4,
|
|
259
|
-
silent: !debug_mode?
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
# 检查下载结果
|
|
263
|
-
if batch_result[:failed] > 0
|
|
264
|
-
raise Informative, "部分描述文件下载失败 (#{batch_result[:failed]}/#{batch_result[:total]})"
|
|
265
|
+
download_tasks.each do |task|
|
|
266
|
+
download_file_from_url(**task)
|
|
265
267
|
end
|
|
266
268
|
|
|
267
269
|
Funlog.instance.fancyinfo_success("所有描述文件下载完成") unless debug_mode?
|
|
@@ -285,6 +287,8 @@ module Pindo
|
|
|
285
287
|
# 解析描述文件元数据以获取 bundle_id
|
|
286
288
|
profile_info = parse_provisioning_profile(profile_path: temp_profile_path, silent: true)
|
|
287
289
|
|
|
290
|
+
validate_profile_certificate!(profile_info, signing_certificate)
|
|
291
|
+
|
|
288
292
|
# 从解析结果中获取实际的 bundle_id
|
|
289
293
|
actual_bundle_id = profile_bundle_id || extract_bundle_id_from_profile(profile_info)
|
|
290
294
|
|
|
@@ -336,8 +340,8 @@ module Pindo
|
|
|
336
340
|
}
|
|
337
341
|
|
|
338
342
|
# 提取签名身份
|
|
339
|
-
if
|
|
340
|
-
cert_info = Pindo::CertHelper.send(:get_cert_info,
|
|
343
|
+
if signing_certificate
|
|
344
|
+
cert_info = Pindo::CertHelper.send(:get_cert_info, signing_certificate.to_der).to_h
|
|
341
345
|
provisioning_info['signing_identity'] = cert_info["Common Name"]
|
|
342
346
|
end
|
|
343
347
|
|
|
@@ -360,51 +364,89 @@ module Pindo
|
|
|
360
364
|
return provisioning_info_array
|
|
361
365
|
end
|
|
362
366
|
|
|
363
|
-
#
|
|
367
|
+
# 安静下载敏感材料,强制 TLS 验证和受限权限
|
|
364
368
|
def download_file_from_url(url:, dest_path:, silent: false)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
raise Informative, "下载文件失败"
|
|
369
|
+
uri = URI.parse(url.to_s)
|
|
370
|
+
raise Informative, "证书下载地址必须使用 HTTPS" unless uri.is_a?(URI::HTTPS)
|
|
371
|
+
URI.open(uri, ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
372
|
+
open_timeout: 10, read_timeout: 30) do |remote|
|
|
373
|
+
File.open(dest_path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
|
|
374
|
+
IO.copy_stream(remote, file)
|
|
375
|
+
end
|
|
373
376
|
end
|
|
377
|
+
rescue ::StandardError
|
|
378
|
+
raise Informative, "证书材料下载失败,请检查下载权限与网络", cause: nil
|
|
374
379
|
end
|
|
375
380
|
|
|
376
|
-
#
|
|
377
|
-
def
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
# 密码只按当前 P12 的内容指纹查找;不缓存、不读取 API 猜测字段。
|
|
382
|
+
def p12_export_password(key_path)
|
|
383
|
+
source = ENV['PINDO_P12_PASSWORD_FILE']
|
|
384
|
+
return '' if source.nil? || source.empty?
|
|
380
385
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
386
|
+
File.open(source, File::RDONLY | File::NOFOLLOW) do |file|
|
|
387
|
+
stat = file.stat
|
|
388
|
+
unless stat.file? && stat.uid == Process.uid && [0o600, 0o400].include?(stat.mode & 0o777)
|
|
389
|
+
raise Informative, "P12 密码文件必须属于当前用户且权限为 0600 或 0400"
|
|
390
|
+
end
|
|
391
|
+
passwords = JSON.parse(file.read)
|
|
392
|
+
fingerprint = Digest::SHA256.file(key_path).hexdigest
|
|
393
|
+
password = passwords.fetch(fingerprint, '')
|
|
394
|
+
raise TypeError unless password.is_a?(String)
|
|
395
|
+
password
|
|
385
396
|
end
|
|
397
|
+
rescue ::StandardError
|
|
398
|
+
raise Informative, "无法读取 P12 密码文件;请检查权限和 SHA-256 密码映射", cause: nil
|
|
399
|
+
end
|
|
386
400
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
keychain_path = FastlaneCore::Helper.keychain_path(keychain)
|
|
401
|
+
def validate_profile_certificate!(profile, certificate)
|
|
402
|
+
certificates = profile['DeveloperCertificates'] || []
|
|
403
|
+
matched = certificates.any? do |entry|
|
|
404
|
+
der = entry.respond_to?(:string) ? entry.string : entry
|
|
405
|
+
OpenSSL::X509::Certificate.new(der).to_der == certificate.to_der
|
|
406
|
+
end
|
|
407
|
+
raise Informative, "描述文件不包含本次 P12 的签名证书,请更新描述文件" unless matched
|
|
408
|
+
rescue OpenSSL::X509::CertificateError, TypeError
|
|
409
|
+
raise Informative, "描述文件中的签名证书无效", cause: nil
|
|
410
|
+
end
|
|
398
411
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
412
|
+
def signing_identity_installed?(cert_path, keychain_path)
|
|
413
|
+
fingerprint = FastlaneCore::CertChecker.sha1_fingerprint(cert_path)
|
|
414
|
+
output, _error, status = Open3.capture3('security', 'find-identity', '-v', '-p', 'codesigning', keychain_path)
|
|
415
|
+
raise Informative, "无法检查钥匙串签名身份" unless status.success?
|
|
416
|
+
output.include?(fingerprint)
|
|
417
|
+
end
|
|
402
418
|
|
|
403
|
-
|
|
419
|
+
def install_certificate_to_keychain(cert_path:, key_path:, cert_password:)
|
|
420
|
+
material = P12Material.load(cert_path: cert_path, key_path: key_path, password: cert_password)
|
|
421
|
+
unless Pindo::CertHelper.is_cert_valid?(cert_path)
|
|
422
|
+
raise Informative, "证书已经过期,请重新生成新证书!"
|
|
423
|
+
end
|
|
424
|
+
raise Informative, "非Mac电脑不支持安装证书!" unless Pindo::CertHelper.isMac?
|
|
425
|
+
|
|
426
|
+
keychain_path = FastlaneCore::Helper.keychain_path('login.keychain')
|
|
427
|
+
return if signing_identity_installed?(cert_path, keychain_path)
|
|
428
|
+
|
|
429
|
+
keychain_password = Pindoconfig.instance.cert_key_password
|
|
430
|
+
if material.is_a?(P12Material::PemMaterial)
|
|
431
|
+
# security 需要完整的证书与私钥;在受限目录中将 PEM 封装为 P12。
|
|
432
|
+
Dir.mktmpdir('pindo_pem_import_') do |directory|
|
|
433
|
+
import_path = File.join(directory, 'identity.p12')
|
|
434
|
+
p12 = OpenSSL::PKCS12.create(cert_password, 'pindo', material.key, material.certificate)
|
|
435
|
+
File.open(import_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(p12.to_der) }
|
|
436
|
+
KeychainHelper.import_file(import_path, keychain_path,
|
|
437
|
+
keychain_password: keychain_password, certificate_password: cert_password, strict: true)
|
|
404
438
|
end
|
|
405
439
|
else
|
|
406
|
-
|
|
440
|
+
KeychainHelper.import_file(key_path, keychain_path,
|
|
441
|
+
keychain_password: keychain_password, certificate_password: cert_password, strict: true)
|
|
442
|
+
end
|
|
443
|
+
unless signing_identity_installed?(cert_path, keychain_path)
|
|
444
|
+
raise Informative, "钥匙串未出现证书与私钥组成的签名身份,请检查钥匙串访问权限"
|
|
407
445
|
end
|
|
446
|
+
rescue Informative
|
|
447
|
+
raise
|
|
448
|
+
rescue ::StandardError
|
|
449
|
+
raise Informative, "证书私钥导入失败,请检查钥匙串密码和访问权限", cause: nil
|
|
408
450
|
end
|
|
409
451
|
|
|
410
452
|
# 安装描述文件(与 Git 模式逻辑相同,跳过 AES 解密)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'match'
|
|
2
|
+
require 'pindo/base/funlog'
|
|
2
3
|
require 'pindo/config/pindoconfig'
|
|
3
4
|
require 'pindo/config/ios_config_parser'
|
|
4
5
|
require 'pindo/module/cert/mode/base_cert_operator'
|
|
@@ -144,7 +145,8 @@ module Pindo
|
|
|
144
145
|
ENV["GIT_CONFIG_VALUE_#{index}"] = '/dev/null'
|
|
145
146
|
ENV['GIT_CONFIG_COUNT'] = (index + 1).to_s
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
# 收起 spinner:match 过程中会提示输入仓库 passphrase、Apple ID 密码和 2FA 验证码
|
|
149
|
+
Funlog.with_paused_spinner { Match::Runner.new.run(config) }
|
|
148
150
|
ensure
|
|
149
151
|
saved_env.each { |key, value| ENV[key] = value } if saved_env
|
|
150
152
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
module CertProcess
|
|
5
|
+
# Validate the freshly downloaded material before touching a user's keychain.
|
|
6
|
+
class P12Material
|
|
7
|
+
PemMaterial = Struct.new(:key, :certificate)
|
|
8
|
+
|
|
9
|
+
def self.load(cert_path:, key_path:, password:)
|
|
10
|
+
certificate = OpenSSL::X509::Certificate.new(File.binread(cert_path))
|
|
11
|
+
content = File.binread(key_path)
|
|
12
|
+
material = if content.include?('-----BEGIN ')
|
|
13
|
+
parse_pem(content, password, certificate)
|
|
14
|
+
else
|
|
15
|
+
parse(key_path, password)
|
|
16
|
+
end
|
|
17
|
+
unless material.key
|
|
18
|
+
raise Informative, 'P12 私钥缺失,请重新导出包含私钥的 P12', cause: nil
|
|
19
|
+
end
|
|
20
|
+
unless material.certificate
|
|
21
|
+
raise Informative, 'P12 证书缺失,请重新导出包含证书的 P12', cause: nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless material.certificate.to_der == certificate.to_der && certificate.check_private_key(material.key)
|
|
25
|
+
raise Informative, '私钥或证书与下载的 CER 证书不匹配,请更新同一套证书材料', cause: nil
|
|
26
|
+
end
|
|
27
|
+
material
|
|
28
|
+
rescue OpenSSL::X509::CertificateError, OpenSSL::PKey::PKeyError
|
|
29
|
+
raise Informative, '证书或私钥格式无效,请重新导出证书材料', cause: nil
|
|
30
|
+
rescue SystemCallError, IOError
|
|
31
|
+
raise Informative, '无法读取证书材料,请重新下载', cause: nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.parse_pem(content, password, certificate)
|
|
35
|
+
unless content.match?(/-----BEGIN (?:RSA |EC |DSA |ENCRYPTED )?PRIVATE KEY-----/)
|
|
36
|
+
raise Informative, 'PEM 私钥缺失,请提供包含私钥的 PEM 文件', cause: nil
|
|
37
|
+
end
|
|
38
|
+
key = OpenSSL::PKey.read(content, password.to_s)
|
|
39
|
+
unless key.respond_to?(:private?) && key.private?
|
|
40
|
+
raise Informative, 'PEM 私钥缺失,请提供包含私钥的 PEM 文件', cause: nil
|
|
41
|
+
end
|
|
42
|
+
PemMaterial.new(key, certificate)
|
|
43
|
+
rescue OpenSSL::PKey::PKeyError => error
|
|
44
|
+
if error.message.match?(/unsupported|unknown cipher|unknown pbe|fetch failed|disabled|not supported/i)
|
|
45
|
+
raise Informative, 'PEM 加密算法不兼容当前 OpenSSL,请以现代算法重新导出', cause: nil
|
|
46
|
+
elsif content.include?('ENCRYPTED')
|
|
47
|
+
raise Informative, 'PEM 私钥解密失败,请检查导出密码及文件完整性', cause: nil
|
|
48
|
+
else
|
|
49
|
+
raise Informative, 'PEM 私钥格式无效或文件已损坏', cause: nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
private_class_method :parse_pem
|
|
53
|
+
|
|
54
|
+
def self.parse(key_path, password)
|
|
55
|
+
OpenSSL::PKCS12.new(File.binread(key_path), password)
|
|
56
|
+
rescue OpenSSL::PKCS12::PKCS12Error => error
|
|
57
|
+
if error.message.match?(/unsupported|unknown cipher|unknown pbe|fetch failed|disabled|not supported/i)
|
|
58
|
+
raise Informative, 'P12 加密算法不兼容当前 OpenSSL;请使用兼容旧算法的工具读取后,以现代加密算法重新导出 P12', cause: nil
|
|
59
|
+
elsif error.message.match?(/mac verify|invalid password|cipherfinal|bad decrypt/i)
|
|
60
|
+
raise Informative, 'P12 导出密码错误或文件完整性校验失败,请核对该文件对应的导出密码', cause: nil
|
|
61
|
+
else
|
|
62
|
+
raise Informative, 'P12 文件格式无效或已损坏,请重新导出(不能据此判定为密码错误)', cause: nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
private_class_method :parse
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'find'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'xcodeproj'
|
|
4
|
+
require 'pindo/base/funlog'
|
|
5
|
+
require 'pindo/base/informative'
|
|
6
|
+
|
|
7
|
+
module Pindo
|
|
8
|
+
# iOS 27 起,链接 iOS 27 SDK 却未采用 UIScene 生命周期的 App 启动即被 UIKit 终止
|
|
9
|
+
# (EXC_BREAKPOINT:_UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption)。
|
|
10
|
+
# 在 gym 之前拦截,避免导出装上就崩的 IPA。Info.plist 路径解析复用 XcodeBuildHelper,
|
|
11
|
+
# 由调用方 xcode_build_helper.rb 负责加载。
|
|
12
|
+
module SceneLifecycleGuard
|
|
13
|
+
REQUIRED_SDK_MAJOR = 27
|
|
14
|
+
|
|
15
|
+
SCENE_MANIFEST_GENERATION_SETTING = 'INFOPLIST_KEY_UIApplicationSceneManifest_Generation'.freeze
|
|
16
|
+
|
|
17
|
+
# Apple 把 AppDelegate 实现 application:configurationForConnectingSceneSession:options:
|
|
18
|
+
# (Swift 为 configurationForConnecting)也算作采用;只认方法实现,不认调用和注释
|
|
19
|
+
SCENE_CONFIGURATION_PATTERN = Regexp.union(
|
|
20
|
+
/-\s*\(\s*UISceneConfiguration\s*\*\s*\)\s*application\s*:[^;{]*configurationForConnectingSceneSession\s*:/,
|
|
21
|
+
/func\s+application\s*\([^)]*configurationForConnecting\b/
|
|
22
|
+
).freeze
|
|
23
|
+
|
|
24
|
+
SOURCE_EXTENSIONS = %w[.m .mm .swift].freeze
|
|
25
|
+
|
|
26
|
+
EXCLUDED_DIRECTORY_NAMES = %w[.git Pods build DerivedData].freeze
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# @param project_dir [String] 工程目录(绝对路径)
|
|
30
|
+
# @param project_fullname [String] .xcodeproj 路径
|
|
31
|
+
# @raise [Informative] 使用 iOS 27+ SDK 构建且主 App target 未采用 UIScene 生命周期
|
|
32
|
+
def check!(project_dir:, project_fullname:)
|
|
33
|
+
project_obj = Xcodeproj::Project.open(project_fullname)
|
|
34
|
+
app_target = main_app_target(project_obj)
|
|
35
|
+
return if app_target.nil? || macos_target?(app_target)
|
|
36
|
+
|
|
37
|
+
sdk_version = iphoneos_sdk_version
|
|
38
|
+
if sdk_version.nil?
|
|
39
|
+
Funlog.instance.fancyinfo_warning("无法获取 iOS SDK 版本,跳过 UIScene 生命周期检查")
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
return if sdk_version.split('.').first.to_i < REQUIRED_SDK_MAJOR
|
|
43
|
+
|
|
44
|
+
release_config = release_configuration(app_target)
|
|
45
|
+
info_plist_path = XcodeBuildHelper.resolve_info_plist_path(
|
|
46
|
+
project_dir, release_config.build_settings['INFOPLIST_FILE'], target: app_target
|
|
47
|
+
)
|
|
48
|
+
return if scene_manifest_declared?(info_plist_path)
|
|
49
|
+
return if scene_manifest_generated?(release_config)
|
|
50
|
+
return if delegate_provides_scene_configuration?(project_dir)
|
|
51
|
+
|
|
52
|
+
raise Informative, adoption_error_message(sdk_version, info_plist_path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 构建将使用的 iphoneos SDK 版本(随 xcode-select / DEVELOPER_DIR),取不到返回 nil
|
|
56
|
+
def iphoneos_sdk_version
|
|
57
|
+
stdout, _stderr, status = Open3.capture3('xcrun', '--sdk', 'iphoneos', '--show-sdk-version')
|
|
58
|
+
version = stdout.strip
|
|
59
|
+
status.success? && version.match?(/\A\d+(\.\d+)*\z/) ? version : nil
|
|
60
|
+
rescue SystemCallError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def main_app_target(project_obj)
|
|
67
|
+
application_uti = Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application]
|
|
68
|
+
project_obj.targets.find do |target|
|
|
69
|
+
target.respond_to?(:product_type) && target.product_type == application_uti
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def release_configuration(target)
|
|
74
|
+
target.build_configurations.find { |config| config.name == 'Release' } || target.build_configurations.first
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def macos_target?(target)
|
|
78
|
+
release_configuration(target).resolve_build_setting('SDKROOT').to_s.start_with?('macosx')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def scene_manifest_declared?(info_plist_path)
|
|
82
|
+
return false if info_plist_path.nil?
|
|
83
|
+
|
|
84
|
+
manifest = Xcodeproj::Plist.read_from_path(info_plist_path)['UIApplicationSceneManifest']
|
|
85
|
+
configurations = manifest.is_a?(Hash) ? manifest['UISceneConfigurations'] : nil
|
|
86
|
+
return false unless configurations.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
configurations.values.any? { |roles| roles.is_a?(Array) && !roles.empty? }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Xcode 只在 GENERATE_INFOPLIST_FILE = YES 时采用 INFOPLIST_KEY_* 设置
|
|
92
|
+
def scene_manifest_generated?(release_config)
|
|
93
|
+
settings = release_config.build_settings
|
|
94
|
+
settings['GENERATE_INFOPLIST_FILE'].to_s.casecmp?('YES') &&
|
|
95
|
+
settings[SCENE_MANIFEST_GENERATION_SETTING].to_s.casecmp?('YES')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def delegate_provides_scene_configuration?(project_dir)
|
|
99
|
+
each_source_file(project_dir).any? do |path|
|
|
100
|
+
File.binread(path).match?(SCENE_CONFIGURATION_PATTERN)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def each_source_file(project_dir)
|
|
105
|
+
return enum_for(:each_source_file, project_dir) unless block_given?
|
|
106
|
+
|
|
107
|
+
Find.find(project_dir) do |path|
|
|
108
|
+
if File.directory?(path)
|
|
109
|
+
Find.prune if path != project_dir && EXCLUDED_DIRECTORY_NAMES.include?(File.basename(path))
|
|
110
|
+
elsif SOURCE_EXTENSIONS.include?(File.extname(path))
|
|
111
|
+
yield path
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def adoption_error_message(sdk_version, info_plist_path)
|
|
117
|
+
[
|
|
118
|
+
"使用 iOS SDK #{sdk_version} 构建的 App 未采用 UIScene 生命周期,在 iOS 27 上启动即崩溃" \
|
|
119
|
+
"(EXC_BREAKPOINT:_UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption)。",
|
|
120
|
+
"Unity 导出工程:把 GoodUnityBuild 升级到 1.17.0 及以上后重新导出" \
|
|
121
|
+
"(自动加入 GoodUnitySceneDelegate 与 Info.plist UIApplicationSceneManifest)。",
|
|
122
|
+
"原生工程:添加 scene delegate 并在 Info.plist 声明 UIApplicationSceneManifest," \
|
|
123
|
+
"或由 AppDelegate 实现 application:configurationForConnectingSceneSession:options:。",
|
|
124
|
+
"检查的 Info.plist:#{info_plist_path || '未找到'}"
|
|
125
|
+
].join("\n")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
2
|
require 'xcodeproj'
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'pindo/module/xcode/scene_lifecycle_guard'
|
|
4
5
|
|
|
5
6
|
module Pindo
|
|
6
7
|
|
|
@@ -676,6 +677,7 @@ module Pindo
|
|
|
676
677
|
# @param icloud_id [String, nil] iCloud ID(可选)
|
|
677
678
|
# @return [String, nil] 生成的 IPA 文件路径,失败返回 nil
|
|
678
679
|
def build_project(project_dir:, icloud_id: nil)
|
|
680
|
+
project_dir = File.expand_path(project_dir)
|
|
679
681
|
project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by { |f| File.mtime(f) }
|
|
680
682
|
|
|
681
683
|
if project_fullname.nil?
|
|
@@ -686,6 +688,9 @@ module Pindo
|
|
|
686
688
|
# 修复 Xcode 16 链接器兼容性问题
|
|
687
689
|
fix_xcode16_linker_flags(project_dir: project_dir)
|
|
688
690
|
|
|
691
|
+
# iOS 27+ SDK 构建的 App 未采用 UIScene 生命周期会启动即崩,gym 之前拦截
|
|
692
|
+
SceneLifecycleGuard.check!(project_dir: project_dir, project_fullname: project_fullname)
|
|
693
|
+
|
|
689
694
|
# 清理旧的 build 目录 (使用绝对路径)
|
|
690
695
|
build_dir = File.join(project_dir, "build")
|
|
691
696
|
FileUtils.rm_rf(build_dir) if File.exist?(build_dir)
|
|
@@ -705,6 +710,15 @@ module Pindo
|
|
|
705
710
|
config = FastlaneCore::Configuration.create(Gym::Options.available_options, gym_options)
|
|
706
711
|
Gym::Manager.new.work(config)
|
|
707
712
|
|
|
713
|
+
# An external Unity export may replace the caller's directory while gym runs.
|
|
714
|
+
# Xcodeproj::Project.open reads Pathname.pwd even for absolute paths.
|
|
715
|
+
# Rebind only an invalid cwd; leave valid callers/concurrent tasks unchanged.
|
|
716
|
+
begin
|
|
717
|
+
Dir.pwd
|
|
718
|
+
rescue Errno::ENOENT
|
|
719
|
+
Dir.chdir(project_dir)
|
|
720
|
+
end
|
|
721
|
+
|
|
708
722
|
# 检测平台类型
|
|
709
723
|
require 'xcodeproj'
|
|
710
724
|
project_obj = Xcodeproj::Project.open(project_fullname)
|
data/lib/pindo/version.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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- wade
|
|
@@ -404,6 +404,7 @@ files:
|
|
|
404
404
|
- lib/pindo/module/cert/mode/custom_git_cert_operator.rb
|
|
405
405
|
- lib/pindo/module/cert/mode/custom_https_cert_operator.rb
|
|
406
406
|
- lib/pindo/module/cert/mode/match_git_cert_operator.rb
|
|
407
|
+
- lib/pindo/module/cert/p12_material.rb
|
|
407
408
|
- lib/pindo/module/cert/pem_helper.rb
|
|
408
409
|
- lib/pindo/module/cert/provisioning_helper.rb
|
|
409
410
|
- lib/pindo/module/pgyer/media_url_helper.rb
|
|
@@ -485,6 +486,7 @@ files:
|
|
|
485
486
|
- lib/pindo/module/xcode/mac_app_resign_helper.rb
|
|
486
487
|
- lib/pindo/module/xcode/res/xcode_res_constant.rb
|
|
487
488
|
- lib/pindo/module/xcode/res/xcode_res_handler.rb
|
|
489
|
+
- lib/pindo/module/xcode/scene_lifecycle_guard.rb
|
|
488
490
|
- lib/pindo/module/xcode/xcode_app_config.rb
|
|
489
491
|
- lib/pindo/module/xcode/xcode_build_config.rb
|
|
490
492
|
- lib/pindo/module/xcode/xcode_build_helper.rb
|