pindo 5.19.3 → 5.19.4
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/command/ios/autoresign.rb +70 -29
- data/lib/pindo/module/pgyer/pgyerhelper.rb +20 -17
- data/lib/pindo/module/resign/ipa_resign_adapter.rb +14 -0
- data/lib/pindo/module/resign/mac_app_resign_adapter.rb +16 -0
- data/lib/pindo/module/task/model/resign/ipa_local_resign_task.rb +44 -10
- data/lib/pindo/module/xcode/mac_app_resign_helper.rb +243 -0
- data/lib/pindo/options/core/option_value_parser.rb +12 -2
- data/lib/pindo/options/groups/jps_options.rb +2 -1
- data/lib/pindo/options/groups/tool_options.rb +18 -0
- data/lib/pindo/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6cdbb729bb1840389cfd9e6252bc60d44d9faa010c47295eeed07a0989b3c42
|
|
4
|
+
data.tar.gz: ef3fa0cbf75139df0333a8dbf0d9f57388c93814da2fb9077673fb0e26df46e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ca2fab411eed7712b8d73a7693a6cd32ae2d4ae79c94c451c5b6fb61a2b4835cc7fecc3452c96cacea273f4f7f3512f5edf848a3053a3692364a813f0f02531
|
|
7
|
+
data.tar.gz: 2de8b83dfa93049cbe1e0a1d97e2d824892a92c1b788f1bd85c135fd52082079426f4ea0dc7d10b5463e0241626dbab5c2add78ba01afe2d45349f54c0349b2a
|
|
@@ -17,7 +17,7 @@ module Pindo
|
|
|
17
17
|
class Autoresign < Ios
|
|
18
18
|
|
|
19
19
|
include Appselect
|
|
20
|
-
self.summary = 'IPA
|
|
20
|
+
self.summary = 'iOS IPA / macOS App 重签名工具'
|
|
21
21
|
|
|
22
22
|
# 启用缓存机制
|
|
23
23
|
def self.use_cache?
|
|
@@ -25,7 +25,7 @@ module Pindo
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
self.description = <<-DESC
|
|
28
|
-
使用最新的证书重签名IPA
|
|
28
|
+
使用最新的证书重签名 iOS IPA 或 macOS App。
|
|
29
29
|
|
|
30
30
|
支持功能:
|
|
31
31
|
|
|
@@ -35,6 +35,8 @@ module Pindo
|
|
|
35
35
|
|
|
36
36
|
* 支持指定IPA路径
|
|
37
37
|
|
|
38
|
+
* 使用 --macos 对 macOS .app 目录进行 Developer ID / Development 重签名
|
|
39
|
+
|
|
38
40
|
* 支持上传到JPS测试平台
|
|
39
41
|
|
|
40
42
|
* 支持发送测试通知
|
|
@@ -47,6 +49,10 @@ module Pindo
|
|
|
47
49
|
|
|
48
50
|
$ pindo ios autoresign demo.ipa # 重签名指定IPA文件
|
|
49
51
|
|
|
52
|
+
$ pindo ios autoresign Demo.app --macos # 重签名指定 macOS App
|
|
53
|
+
|
|
54
|
+
$ pindo ios autoresign --app=Demo.app --macos # 使用选项指定 macOS App
|
|
55
|
+
|
|
50
56
|
$ pindo ios autoresign --adhoc # 使用adhoc证书重签名
|
|
51
57
|
|
|
52
58
|
$ pindo ios autoresign --deploy # 使用发布bundle id
|
|
@@ -65,7 +71,7 @@ module Pindo
|
|
|
65
71
|
DESC
|
|
66
72
|
|
|
67
73
|
self.arguments = [
|
|
68
|
-
CLAide::Argument.new('path/to/demo.ipa', true),
|
|
74
|
+
CLAide::Argument.new('path/to/demo.ipa|path/to/Demo.app', true),
|
|
69
75
|
]
|
|
70
76
|
|
|
71
77
|
def self.option_items
|
|
@@ -73,11 +79,11 @@ module Pindo
|
|
|
73
79
|
items = []
|
|
74
80
|
|
|
75
81
|
# 添加工具参数(ToolOptions)
|
|
76
|
-
items.concat(Pindo::Options::ToolOptions.select(:ipa, :deploy))
|
|
82
|
+
items.concat(Pindo::Options::ToolOptions.select(:ipa, :app, :deploy))
|
|
77
83
|
|
|
78
84
|
# 添加构建类型参数(BuildOptions)
|
|
79
85
|
items.concat(Pindo::Options::BuildOptions.select(
|
|
80
|
-
:dev, :adhoc, :develop_id, :build_type
|
|
86
|
+
:bundleid, :dev, :adhoc, :develop_id, :build_type
|
|
81
87
|
))
|
|
82
88
|
|
|
83
89
|
# 添加证书相关参数(CertOptions)
|
|
@@ -106,9 +112,10 @@ module Pindo
|
|
|
106
112
|
@options = initialize_options(argv)
|
|
107
113
|
|
|
108
114
|
# 优先使用选项参数,如果没有则使用位置参数
|
|
109
|
-
@args_ipa_file = @options[:ipa] || positional_ipa
|
|
115
|
+
@args_ipa_file = @options[:app] || @options[:ipa] || positional_ipa
|
|
110
116
|
|
|
111
117
|
@args_deploy_flag = @options[:deploy]
|
|
118
|
+
@args_bundle_id = @options[:bundleid]
|
|
112
119
|
|
|
113
120
|
# JPS 参数(从 options 中获取)
|
|
114
121
|
@args_upload_flag = @options[:upload]
|
|
@@ -154,7 +161,8 @@ module Pindo
|
|
|
154
161
|
|
|
155
162
|
# 处理平台类型
|
|
156
163
|
@platform_type = @options[:platform]
|
|
157
|
-
|
|
164
|
+
# --app 只用于 macOS,指定即隐含 macOS 平台,无需再显式加 --macos
|
|
165
|
+
if @args_macos_flag || @options[:app]
|
|
158
166
|
@platform_type = 'macos'
|
|
159
167
|
end
|
|
160
168
|
|
|
@@ -165,15 +173,18 @@ module Pindo
|
|
|
165
173
|
|
|
166
174
|
super
|
|
167
175
|
@additional_args = argv.remainder!
|
|
176
|
+
unless @additional_args.empty?
|
|
177
|
+
raise Informative, "只允许指定一个安装包路径,多余参数: #{@additional_args.join(' ')}"
|
|
178
|
+
end
|
|
168
179
|
end
|
|
169
180
|
|
|
170
181
|
|
|
171
182
|
def run
|
|
172
183
|
begin
|
|
173
|
-
# 1.
|
|
184
|
+
# 1. 查找待签名包(iOS 为 IPA,macOS 为 .app)
|
|
174
185
|
ipa_file_path = find_ipa_file()
|
|
175
186
|
unless ipa_file_path && File.exist?(ipa_file_path)
|
|
176
|
-
raise Informative, "
|
|
187
|
+
raise Informative, "未找到需要重签名的安装包"
|
|
177
188
|
end
|
|
178
189
|
|
|
179
190
|
# 2. 选择 Bundle ID
|
|
@@ -187,7 +198,7 @@ module Pindo
|
|
|
187
198
|
app_info_obj, workflow_info = prepare_jps_config()
|
|
188
199
|
|
|
189
200
|
# 5. 计算重签名后的 IPA 文件名
|
|
190
|
-
resigned_ipa_file = ipa_file_path
|
|
201
|
+
resigned_ipa_file = resigned_package_path(ipa_file_path)
|
|
191
202
|
|
|
192
203
|
# 6. 创建任务
|
|
193
204
|
tasks = create_resign_tasks(
|
|
@@ -214,21 +225,25 @@ module Pindo
|
|
|
214
225
|
|
|
215
226
|
# 查找 IPA 文件
|
|
216
227
|
def find_ipa_file
|
|
217
|
-
# 1.
|
|
218
|
-
if @args_ipa_file
|
|
219
|
-
|
|
228
|
+
# 1. 如果通过选项或位置参数指定了安装包,严格校验后直接返回
|
|
229
|
+
if @args_ipa_file
|
|
230
|
+
unless File.exist?(@args_ipa_file)
|
|
231
|
+
raise Informative, "指定的安装包不存在: #{@args_ipa_file}"
|
|
232
|
+
end
|
|
233
|
+
validate_package_path!(@args_ipa_file)
|
|
234
|
+
puts "\n使用指定的安装包: #{@args_ipa_file}"
|
|
220
235
|
puts "修改时间: #{File.mtime(@args_ipa_file)}"
|
|
221
236
|
return @args_ipa_file
|
|
222
237
|
end
|
|
223
238
|
|
|
224
239
|
# 2. 在当前目录查找最新的非 _resigned 的 IPA
|
|
225
240
|
current_dir = Dir.pwd
|
|
226
|
-
|
|
241
|
+
pattern = macos_platform? ? "*.app" : "*.ipa"
|
|
242
|
+
ipa_files = Dir.glob(File.join(current_dir, pattern))
|
|
227
243
|
|
|
228
|
-
#
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
end
|
|
244
|
+
# 排除已经重签名的产物并找到最新安装包
|
|
245
|
+
ipa_files.reject! { |f| File.basename(f).include?("_resigned") }
|
|
246
|
+
ipa_file = ipa_files.max_by { |f| File.mtime(f) }
|
|
232
247
|
|
|
233
248
|
if ipa_file && File.exist?(ipa_file)
|
|
234
249
|
puts "\n自动找到 IPA 文件: #{ipa_file}"
|
|
@@ -237,12 +252,13 @@ module Pindo
|
|
|
237
252
|
end
|
|
238
253
|
|
|
239
254
|
# 3. 如果没有找到,从终端输入
|
|
240
|
-
|
|
241
|
-
puts "
|
|
255
|
+
package_name = macos_platform? ? "macOS .app" : "IPA"
|
|
256
|
+
puts "\n⚠️ 未找到 #{package_name}"
|
|
257
|
+
puts "请输入 #{package_name} 路径(支持相对路径或绝对路径):"
|
|
242
258
|
|
|
243
|
-
ipa_path = ask("
|
|
244
|
-
q.validate = /\.ipa$/i
|
|
245
|
-
q.responses[:not_valid] = "❌ 请输入有效的
|
|
259
|
+
ipa_path = ask("安装包路径: ") do |q|
|
|
260
|
+
q.validate = macos_platform? ? /\.app$/i : /\.ipa$/i
|
|
261
|
+
q.responses[:not_valid] = "❌ 请输入有效的 #{package_name} 路径"
|
|
246
262
|
end
|
|
247
263
|
|
|
248
264
|
# 去除首尾空格
|
|
@@ -251,7 +267,8 @@ module Pindo
|
|
|
251
267
|
# 验证文件是否存在
|
|
252
268
|
if ipa_path && !ipa_path.empty?
|
|
253
269
|
if File.exist?(ipa_path)
|
|
254
|
-
|
|
270
|
+
validate_package_path!(ipa_path)
|
|
271
|
+
puts "✓ 使用输入的安装包: #{ipa_path}"
|
|
255
272
|
puts " 修改时间: #{File.mtime(ipa_path)}"
|
|
256
273
|
return ipa_path
|
|
257
274
|
else
|
|
@@ -262,8 +279,32 @@ module Pindo
|
|
|
262
279
|
nil
|
|
263
280
|
end
|
|
264
281
|
|
|
282
|
+
def macos_platform?
|
|
283
|
+
@platform_type.to_s.downcase == 'macos'
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def validate_package_path!(path)
|
|
287
|
+
if macos_platform?
|
|
288
|
+
unless File.directory?(path) && path.downcase.end_with?('.app')
|
|
289
|
+
raise Informative, "--macos 模式要求输入 macOS .app 目录: #{path}"
|
|
290
|
+
end
|
|
291
|
+
elsif !File.file?(path) || !path.downcase.end_with?('.ipa')
|
|
292
|
+
raise Informative, "iOS 模式要求输入 .ipa 文件: #{path}"
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def resigned_package_path(path)
|
|
297
|
+
extension = macos_platform? ? '.app' : '.ipa'
|
|
298
|
+
path.sub(/#{Regexp.escape(extension)}$/i, "_resigned#{extension}")
|
|
299
|
+
end
|
|
300
|
+
|
|
265
301
|
# 选择 Bundle ID(根据 build_type)
|
|
266
302
|
def select_bundle_id
|
|
303
|
+
if @args_bundle_id && !@args_bundle_id.empty?
|
|
304
|
+
puts "\n使用指定的 Bundle ID: #{@args_bundle_id}"
|
|
305
|
+
return @args_bundle_id
|
|
306
|
+
end
|
|
307
|
+
|
|
267
308
|
if @args_deploy_flag || @build_type != 'development'
|
|
268
309
|
get_selected_deploy_bundleid()
|
|
269
310
|
else
|
|
@@ -274,8 +315,9 @@ module Pindo
|
|
|
274
315
|
# 拉取应用配置
|
|
275
316
|
def pull_app_config(bundle_id)
|
|
276
317
|
require 'pindo/config/build_info_manager'
|
|
318
|
+
repo_name = bundle_id.sub(/\.\*$/, '')
|
|
277
319
|
Pindo::BuildInfoManager.share_instance.pull_appconfig_with_reponame(
|
|
278
|
-
repo_name:
|
|
320
|
+
repo_name: repo_name,
|
|
279
321
|
target_dir: Dir.pwd
|
|
280
322
|
)
|
|
281
323
|
end
|
|
@@ -283,9 +325,8 @@ module Pindo
|
|
|
283
325
|
# 准备 JPS 配置
|
|
284
326
|
def prepare_jps_config
|
|
285
327
|
if @args_upload_flag
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
package_type = is_macos ? 'mac' : 'ipa'
|
|
328
|
+
# 以平台类型为唯一判定口径,与 create_resign_tasks 的上传任务保持一致
|
|
329
|
+
package_type = macos_platform? ? 'mac' : 'ipa'
|
|
289
330
|
app_info_obj, workflow_info = PgyerHelper.share_instace.prepare_upload(
|
|
290
331
|
working_directory: Dir.pwd,
|
|
291
332
|
conf: @args_conf,
|
|
@@ -327,7 +368,7 @@ module Pindo
|
|
|
327
368
|
# 2. 上传任务(仅在需要时创建)
|
|
328
369
|
if @args_upload_flag && app_info_obj
|
|
329
370
|
upload_task = Pindo::TaskSystem::JPSUploadTask.new(
|
|
330
|
-
'ipa',
|
|
371
|
+
macos_platform? ? 'mac' : 'ipa',
|
|
331
372
|
Dir.pwd,
|
|
332
373
|
resigned_ipa_file,
|
|
333
374
|
app_info_obj: app_info_obj,
|
|
@@ -751,23 +751,7 @@ module Pindo
|
|
|
751
751
|
if !ipa_file_upload.nil? &&
|
|
752
752
|
File.extname(ipa_file_upload).downcase.eql?(".exe")
|
|
753
753
|
|
|
754
|
-
|
|
755
|
-
windows_build_dir = File.dirname(windows_exe_path)
|
|
756
|
-
windows_zip_dir = File.dirname(windows_build_dir)
|
|
757
|
-
windows_build_name = File.basename(windows_exe_path, File.extname(windows_exe_path))
|
|
758
|
-
windows_exe_name = File.basename(windows_exe_path)
|
|
759
|
-
windows_zip_fullname = File.join(windows_zip_dir, windows_build_name + ".zip")
|
|
760
|
-
|
|
761
|
-
if File.exist?(windows_zip_fullname)
|
|
762
|
-
FileUtils.rm_rf(windows_zip_fullname)
|
|
763
|
-
end
|
|
764
|
-
|
|
765
|
-
Zip::File.open(windows_zip_fullname, Zip::File::CREATE) do |zipfile|
|
|
766
|
-
Dir.chdir(windows_build_dir) do
|
|
767
|
-
zipfile.add(windows_exe_name, windows_exe_name)
|
|
768
|
-
end
|
|
769
|
-
end
|
|
770
|
-
ipa_file_upload = windows_zip_fullname
|
|
754
|
+
ipa_file_upload = package_windows_exe_for_upload(ipa_file_upload)
|
|
771
755
|
end
|
|
772
756
|
unless !ipa_file_upload.nil? && File.exist?(ipa_file_upload)
|
|
773
757
|
return
|
|
@@ -898,6 +882,25 @@ module Pindo
|
|
|
898
882
|
|
|
899
883
|
end
|
|
900
884
|
|
|
885
|
+
def package_windows_exe_for_upload(exe_file_upload)
|
|
886
|
+
windows_exe_path = File.expand_path(exe_file_upload)
|
|
887
|
+
windows_build_dir = File.dirname(windows_exe_path)
|
|
888
|
+
windows_zip_dir = File.dirname(windows_build_dir)
|
|
889
|
+
windows_build_name = File.basename(windows_exe_path, File.extname(windows_exe_path))
|
|
890
|
+
windows_exe_name = File.basename(windows_exe_path)
|
|
891
|
+
windows_zip_fullname = File.join(windows_zip_dir, windows_build_name + ".zip")
|
|
892
|
+
|
|
893
|
+
if File.exist?(windows_zip_fullname)
|
|
894
|
+
FileUtils.rm_rf(windows_zip_fullname)
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
Zip::File.open(windows_zip_fullname, Zip::File::CREATE) do |zipfile|
|
|
898
|
+
zipfile.add(windows_exe_name, windows_exe_path)
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
windows_zip_fullname
|
|
902
|
+
end
|
|
903
|
+
|
|
901
904
|
# 上传 media 文件(图片、视频等)到 JPS
|
|
902
905
|
# 上传成功后自动更新 commit_log 记录
|
|
903
906
|
#
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'pindo/module/xcode/ipa_resign_helper'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
module Resign
|
|
5
|
+
class IpaResignAdapter
|
|
6
|
+
def resign(package_path:, bundle_id:, **_options)
|
|
7
|
+
Pindo::IpaResignHelper.resign_ipa(
|
|
8
|
+
ipa_file_path: package_path,
|
|
9
|
+
bundle_id: bundle_id
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'pindo/module/xcode/mac_app_resign_helper'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
module Resign
|
|
5
|
+
class MacAppResignAdapter
|
|
6
|
+
def resign(package_path:, bundle_id:, signing_identity:, profiles:, **_options)
|
|
7
|
+
Pindo::MacAppResignHelper.resign_app(
|
|
8
|
+
app_path: package_path,
|
|
9
|
+
bundle_id: bundle_id,
|
|
10
|
+
signing_identity: signing_identity,
|
|
11
|
+
profiles: profiles
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -3,6 +3,8 @@ require 'pindo/module/task/task_config'
|
|
|
3
3
|
require 'pindo/module/cert/cert_helper'
|
|
4
4
|
require 'pindo/config/ios_config_parser'
|
|
5
5
|
require 'pindo/module/xcode/ipa_resign_helper'
|
|
6
|
+
require 'pindo/module/resign/ipa_resign_adapter'
|
|
7
|
+
require 'pindo/module/resign/mac_app_resign_adapter'
|
|
6
8
|
|
|
7
9
|
module Pindo
|
|
8
10
|
module TaskSystem
|
|
@@ -62,7 +64,7 @@ module Pindo
|
|
|
62
64
|
# 设置任务优先级
|
|
63
65
|
options[:priority] ||= TaskPriority::HIGH
|
|
64
66
|
|
|
65
|
-
super("
|
|
67
|
+
super("本地重签名", options)
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
# 验证任务参数
|
|
@@ -95,14 +97,16 @@ module Pindo
|
|
|
95
97
|
|
|
96
98
|
# 执行重签名
|
|
97
99
|
def do_work
|
|
98
|
-
# 1.
|
|
100
|
+
# 1. 确定要重签名的安装包
|
|
99
101
|
ipa_to_resign = determine_ipa_file
|
|
100
102
|
unless ipa_to_resign && File.exist?(ipa_to_resign)
|
|
101
|
-
raise "
|
|
103
|
+
raise "未找到需要重签名的安装包"
|
|
102
104
|
end
|
|
103
105
|
|
|
106
|
+
validate_package_type!(ipa_to_resign)
|
|
107
|
+
|
|
104
108
|
puts "\n▶ 重签名信息"
|
|
105
|
-
puts "
|
|
109
|
+
puts " 安装包: #{File.basename(ipa_to_resign)}"
|
|
106
110
|
puts " Bundle ID: #{@bundle_id}"
|
|
107
111
|
puts " 类型: #{@build_type}"
|
|
108
112
|
cert_info = []
|
|
@@ -114,17 +118,23 @@ module Pindo
|
|
|
114
118
|
load_config_if_needed
|
|
115
119
|
|
|
116
120
|
# 3. 安装证书
|
|
117
|
-
install_certificate
|
|
121
|
+
provisioning_info_array = install_certificate
|
|
118
122
|
|
|
119
123
|
# 4. 执行重签名
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
signing_identity = Array(provisioning_info_array)
|
|
125
|
+
.map { |item| item['signing_identity'] }
|
|
126
|
+
.compact
|
|
127
|
+
.first
|
|
128
|
+
resigned_ipa = resign_adapter.resign(
|
|
129
|
+
package_path: ipa_to_resign,
|
|
130
|
+
bundle_id: @bundle_id,
|
|
131
|
+
signing_identity: signing_identity,
|
|
132
|
+
profiles: provisioning_info_array
|
|
123
133
|
)
|
|
124
134
|
|
|
125
135
|
# 5. 验证重签名结果
|
|
126
136
|
unless resigned_ipa && File.exist?(resigned_ipa)
|
|
127
|
-
raise "
|
|
137
|
+
raise "重签名失败:未生成重签名后的安装包"
|
|
128
138
|
end
|
|
129
139
|
|
|
130
140
|
puts "✅ 重签名完成: #{File.basename(resigned_ipa)}\n"
|
|
@@ -146,13 +156,36 @@ module Pindo
|
|
|
146
156
|
end
|
|
147
157
|
end
|
|
148
158
|
|
|
159
|
+
def macos_platform?
|
|
160
|
+
@platform_type.to_s.downcase == 'macos'
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validate_package_type!(path)
|
|
164
|
+
valid = if macos_platform?
|
|
165
|
+
File.directory?(path) && path.downcase.end_with?('.app')
|
|
166
|
+
else
|
|
167
|
+
File.file?(path) && path.downcase.end_with?('.ipa')
|
|
168
|
+
end
|
|
169
|
+
expected = macos_platform? ? 'macOS .app 目录' : '.ipa 文件'
|
|
170
|
+
raise "当前平台要求输入 #{expected}: #{path}" unless valid
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def resign_adapter
|
|
174
|
+
if macos_platform?
|
|
175
|
+
Pindo::Resign::MacAppResignAdapter.new
|
|
176
|
+
else
|
|
177
|
+
Pindo::Resign::IpaResignAdapter.new
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
149
181
|
# 查找最新的 IPA 文件(排除 _resigned.ipa)
|
|
150
182
|
def find_latest_ipa_file
|
|
151
183
|
unless File.directory?(@ipa_path)
|
|
152
184
|
return nil
|
|
153
185
|
end
|
|
154
186
|
|
|
155
|
-
|
|
187
|
+
pattern = macos_platform? ? '*.app' : '*.ipa'
|
|
188
|
+
ipa_files = Dir.glob(File.join(@ipa_path, pattern))
|
|
156
189
|
|
|
157
190
|
# 过滤掉 _resigned.ipa 文件
|
|
158
191
|
ipa_files = ipa_files.reject { |f| File.basename(f).include?("_resigned") }
|
|
@@ -204,6 +237,7 @@ module Pindo
|
|
|
204
237
|
unless provisioning_info_array && !provisioning_info_array.empty?
|
|
205
238
|
raise "证书获取失败:未找到有效的证书信息"
|
|
206
239
|
end
|
|
240
|
+
provisioning_info_array
|
|
207
241
|
end
|
|
208
242
|
end
|
|
209
243
|
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'find'
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
module Pindo
|
|
7
|
+
class MacAppResignHelper
|
|
8
|
+
BUNDLE_EXTENSIONS = %w[.app .appex .framework .xpc .bundle .plugin].freeze
|
|
9
|
+
# 需要内嵌描述文件的可执行 bundle(含独立 Bundle ID),
|
|
10
|
+
# .framework / .bundle / .plugin 属于共享代码,不单独内嵌 profile。
|
|
11
|
+
PROFILE_BUNDLE_EXTENSIONS = %w[.app .appex .xpc].freeze
|
|
12
|
+
MACHO_MAGICS = [
|
|
13
|
+
"\xFE\xED\xFA\xCE", "\xCE\xFA\xED\xFE", "\xFE\xED\xFA\xCF",
|
|
14
|
+
"\xCF\xFA\xED\xFE", "\xCA\xFE\xBA\xBE", "\xBE\xBA\xFE\xCA",
|
|
15
|
+
"\xCA\xFE\xBA\xBF", "\xBF\xBA\xFE\xCA"
|
|
16
|
+
].map(&:b).freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def resign_app(app_path:, bundle_id:, signing_identity:, profiles: [])
|
|
20
|
+
validate_input!(app_path, signing_identity)
|
|
21
|
+
output_path = resigned_path(app_path)
|
|
22
|
+
FileUtils.rm_rf(output_path)
|
|
23
|
+
FileUtils.cp_r(app_path, output_path, preserve: true)
|
|
24
|
+
|
|
25
|
+
resolved_bundle_id = resolve_bundle_id(output_path, bundle_id)
|
|
26
|
+
puts "\n▶ macOS App 重签名"
|
|
27
|
+
puts " 输入: #{app_path}"
|
|
28
|
+
puts " 输出: #{output_path}"
|
|
29
|
+
puts " Bundle ID: #{bundle_id} → #{resolved_bundle_id}" if bundle_id != resolved_bundle_id
|
|
30
|
+
puts " Bundle ID: #{resolved_bundle_id}" if bundle_id == resolved_bundle_id
|
|
31
|
+
puts " 签名身份: #{signing_identity}"
|
|
32
|
+
update_main_bundle_id(output_path, resolved_bundle_id)
|
|
33
|
+
|
|
34
|
+
Dir.mktmpdir('pindo-macos-entitlements-') do |tmp_dir|
|
|
35
|
+
nested_paths = signable_paths(output_path)
|
|
36
|
+
puts " 嵌套组件: #{nested_paths.length} 个"
|
|
37
|
+
nested_paths.each_with_index do |path, index|
|
|
38
|
+
puts " [#{index + 1}/#{nested_paths.length}] 签名: #{relative_path(path, output_path)}"
|
|
39
|
+
sign_component(path, output_path, signing_identity, profiles, tmp_dir, index)
|
|
40
|
+
end
|
|
41
|
+
puts " 签名主 App: #{File.basename(output_path)}"
|
|
42
|
+
sign_component(output_path, output_path, signing_identity, profiles, tmp_dir, 'main',
|
|
43
|
+
bundle_id: resolved_bundle_id)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
puts " 验证签名: codesign --verify --strict"
|
|
47
|
+
verify!(output_path)
|
|
48
|
+
puts " ✓ macOS App 签名验证通过"
|
|
49
|
+
output_path
|
|
50
|
+
rescue StandardError
|
|
51
|
+
FileUtils.rm_rf(output_path) if output_path && File.exist?(output_path)
|
|
52
|
+
raise
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def validate_input!(app_path, signing_identity)
|
|
58
|
+
unless File.directory?(app_path) && app_path.downcase.end_with?('.app')
|
|
59
|
+
raise Informative, "macOS 重签名要求输入 .app 目录: #{app_path}"
|
|
60
|
+
end
|
|
61
|
+
if signing_identity.to_s.strip.empty?
|
|
62
|
+
raise Informative, 'macOS 重签名未找到有效的 signing identity'
|
|
63
|
+
end
|
|
64
|
+
info_plist = File.join(app_path, 'Contents', 'Info.plist')
|
|
65
|
+
raise Informative, "不是有效的 macOS App,缺少 #{info_plist}" unless File.file?(info_plist)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def resigned_path(app_path)
|
|
69
|
+
app_path.sub(/\.app$/i, '_resigned.app')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def update_main_bundle_id(app_path, bundle_id)
|
|
73
|
+
return if bundle_id.to_s.empty?
|
|
74
|
+
|
|
75
|
+
plist = File.join(app_path, 'Contents', 'Info.plist')
|
|
76
|
+
success = system('/usr/libexec/PlistBuddy', '-c', "Set :CFBundleIdentifier #{bundle_id}", plist)
|
|
77
|
+
raise Informative, "修改 macOS App Bundle ID 失败: #{plist}" unless success
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def resolve_bundle_id(app_path, bundle_id)
|
|
81
|
+
return bundle_id unless bundle_id.to_s.include?('*')
|
|
82
|
+
|
|
83
|
+
plist = File.join(app_path, 'Contents', 'Info.plist')
|
|
84
|
+
candidate = plist_value(plist, 'CFBundleName')
|
|
85
|
+
candidate = plist_value(plist, 'CFBundleDisplayName') if candidate.to_s.empty?
|
|
86
|
+
candidate = File.basename(app_path, '.app') if candidate.to_s.empty?
|
|
87
|
+
suffix = candidate.to_s.downcase.gsub(/[^a-z0-9-]+/, '-')
|
|
88
|
+
.gsub(/\A-+|-+\z/, '')
|
|
89
|
+
suffix = 'app' if suffix.empty?
|
|
90
|
+
bundle_id.sub('*', suffix)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def plist_value(plist, key)
|
|
94
|
+
stdout, _stderr, status = Open3.capture3(
|
|
95
|
+
'/usr/libexec/PlistBuddy', '-c', "Print :#{key}", plist
|
|
96
|
+
)
|
|
97
|
+
status.success? ? stdout.strip : nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 对单个可签名组件重签名。
|
|
101
|
+
# 对需要描述文件的 bundle(.app/.appex/.xpc):内嵌匹配的 profile,并
|
|
102
|
+
# 优先使用 profile 内的 entitlements(含新的 team-identifier /
|
|
103
|
+
# application-identifier),从根本上避免沿用旧二进制里的旧身份。
|
|
104
|
+
# Developer ID 场景(profiles 为空)回退到从旧二进制导出的 entitlements。
|
|
105
|
+
def sign_component(path, app_root, identity, profiles, tmp_dir, index, bundle_id: nil)
|
|
106
|
+
entitlements =
|
|
107
|
+
if profile_required?(path)
|
|
108
|
+
component_bundle_id = bundle_id || bundle_identifier(path)
|
|
109
|
+
profile_path = matched_profile_path(component_bundle_id, profiles)
|
|
110
|
+
if profile_path
|
|
111
|
+
embed_profile_file(path, profile_path)
|
|
112
|
+
puts " 描述文件: #{File.basename(profile_path)}"
|
|
113
|
+
profile_entitlements(profile_path, tmp_dir, index) ||
|
|
114
|
+
extract_entitlements(path, tmp_dir, index)
|
|
115
|
+
elsif !Array(profiles).empty?
|
|
116
|
+
raise Informative,
|
|
117
|
+
"未找到匹配 #{component_bundle_id} 的描述文件,无法重签名: #{relative_path(path, app_root)}"
|
|
118
|
+
else
|
|
119
|
+
extract_entitlements(path, tmp_dir, index)
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
extract_entitlements(path, tmp_dir, index)
|
|
123
|
+
end
|
|
124
|
+
codesign(path, identity, entitlements)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# 仅 .app/.appex/.xpc 这类含 Info.plist 的可执行 bundle 需要描述文件。
|
|
128
|
+
def profile_required?(path)
|
|
129
|
+
return false unless File.directory?(path)
|
|
130
|
+
return false unless PROFILE_BUNDLE_EXTENSIONS.include?(File.extname(path).downcase)
|
|
131
|
+
|
|
132
|
+
!bundle_info_plist(path).nil?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def bundle_info_plist(bundle_path)
|
|
136
|
+
[
|
|
137
|
+
File.join(bundle_path, 'Contents', 'Info.plist'),
|
|
138
|
+
File.join(bundle_path, 'Info.plist'),
|
|
139
|
+
File.join(bundle_path, 'Resources', 'Info.plist')
|
|
140
|
+
].find { |candidate| File.file?(candidate) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def bundle_identifier(bundle_path)
|
|
144
|
+
plist = bundle_info_plist(bundle_path)
|
|
145
|
+
plist && plist_value(plist, 'CFBundleIdentifier')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def matched_profile_path(bundle_id, profiles)
|
|
149
|
+
profile = Array(profiles).find do |item|
|
|
150
|
+
profile_bundle_id = item['bundle_id'].to_s
|
|
151
|
+
profile_bundle_id == bundle_id ||
|
|
152
|
+
(profile_bundle_id.end_with?('.*') &&
|
|
153
|
+
bundle_id.to_s.start_with?(profile_bundle_id.delete_suffix('*')))
|
|
154
|
+
end
|
|
155
|
+
profile_path = profile && profile['profile_path']
|
|
156
|
+
profile_path if profile_path && File.file?(profile_path)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def embed_profile_file(bundle_path, profile_path)
|
|
160
|
+
contents_dir = File.join(bundle_path, 'Contents')
|
|
161
|
+
dest_dir = File.directory?(contents_dir) ? contents_dir : bundle_path
|
|
162
|
+
FileUtils.cp(profile_path, File.join(dest_dir, 'embedded.provisionprofile'))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# 从描述文件解出 Entitlements,写成独立 plist 供 codesign --entitlements 使用。
|
|
166
|
+
def profile_entitlements(profile_path, tmp_dir, index)
|
|
167
|
+
decoded, _stderr, status = Open3.capture3(
|
|
168
|
+
'/usr/bin/security', 'cms', '-D', '-i', profile_path
|
|
169
|
+
)
|
|
170
|
+
return nil unless status.success?
|
|
171
|
+
|
|
172
|
+
profile_plist = File.join(tmp_dir, "profile-#{index}.plist")
|
|
173
|
+
File.write(profile_plist, decoded)
|
|
174
|
+
entitlements_xml, _err, extract_status = Open3.capture3(
|
|
175
|
+
'/usr/bin/plutil', '-extract', 'Entitlements', 'xml1', '-o', '-', profile_plist
|
|
176
|
+
)
|
|
177
|
+
return nil unless extract_status.success? && entitlements_xml.include?('<plist')
|
|
178
|
+
|
|
179
|
+
entitlements_path = File.join(tmp_dir, "profile-entitlements-#{index}.plist")
|
|
180
|
+
File.write(entitlements_path, entitlements_xml)
|
|
181
|
+
entitlements_path
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def relative_path(path, app_path)
|
|
185
|
+
path.delete_prefix("#{app_path}/")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Sign deepest nested code first. The main app is deliberately excluded
|
|
189
|
+
# here and signed last by resign_app.
|
|
190
|
+
def signable_paths(app_path)
|
|
191
|
+
paths = []
|
|
192
|
+
Find.find(app_path) do |path|
|
|
193
|
+
next if path == app_path
|
|
194
|
+
if File.directory?(path) && BUNDLE_EXTENSIONS.include?(File.extname(path).downcase)
|
|
195
|
+
paths << path
|
|
196
|
+
elsif File.file?(path) && (File.extname(path).downcase == '.dylib' || macho?(path))
|
|
197
|
+
paths << path
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
paths.uniq.sort_by { |path| [-path.count(File::SEPARATOR), File.directory?(path) ? 1 : 0] }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def macho?(path)
|
|
204
|
+
File.open(path, 'rb') { |file| MACHO_MAGICS.include?(file.read(4)) }
|
|
205
|
+
rescue Errno::EACCES, Errno::ENOENT
|
|
206
|
+
false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def extract_entitlements(path, tmp_dir, index)
|
|
210
|
+
stdout, _stderr, status = Open3.capture3(
|
|
211
|
+
'/usr/bin/codesign', '-d', '--entitlements', ':-', path
|
|
212
|
+
)
|
|
213
|
+
return nil unless status.success? && stdout.include?('<plist')
|
|
214
|
+
|
|
215
|
+
entitlements_path = File.join(tmp_dir, "#{index}.plist")
|
|
216
|
+
File.write(entitlements_path, stdout)
|
|
217
|
+
entitlements_path
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def codesign(path, identity, entitlements)
|
|
221
|
+
command = [
|
|
222
|
+
'/usr/bin/codesign', '--force', '--sign', identity,
|
|
223
|
+
'--timestamp', '--options', 'runtime'
|
|
224
|
+
]
|
|
225
|
+
command.concat(['--entitlements', entitlements]) if entitlements
|
|
226
|
+
command << path
|
|
227
|
+
_stdout, stderr, status = Open3.capture3(*command)
|
|
228
|
+
return if status.success?
|
|
229
|
+
|
|
230
|
+
raise Informative, "codesign 失败: #{path}\n#{stderr.strip}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def verify!(app_path)
|
|
234
|
+
_stdout, stderr, status = Open3.capture3(
|
|
235
|
+
'/usr/bin/codesign', '--verify', '--strict', '--verbose=2', app_path
|
|
236
|
+
)
|
|
237
|
+
return if status.success?
|
|
238
|
+
|
|
239
|
+
raise Informative, "macOS App 签名验证失败: #{app_path}\n#{stderr.strip}"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -41,13 +41,23 @@ module Pindo
|
|
|
41
41
|
# @param item [OptionItem] 参数定义
|
|
42
42
|
# @return [Object, nil] 提取的值
|
|
43
43
|
def self.extract_from_argv(argv, item)
|
|
44
|
+
option_names = [item.key.to_s] + item.aliases.map(&:to_s)
|
|
45
|
+
|
|
44
46
|
if item.boolean?
|
|
45
47
|
# Boolean 类型:使用 flag?
|
|
46
|
-
|
|
48
|
+
option_names.each do |option_name|
|
|
49
|
+
value = argv.flag?(option_name, nil)
|
|
50
|
+
return value unless value.nil?
|
|
51
|
+
end
|
|
47
52
|
else
|
|
48
53
|
# 其他类型:使用 option
|
|
49
|
-
|
|
54
|
+
option_names.each do |option_name|
|
|
55
|
+
value = argv.option(option_name, nil)
|
|
56
|
+
return value unless value.nil?
|
|
57
|
+
end
|
|
50
58
|
end
|
|
59
|
+
|
|
60
|
+
nil
|
|
51
61
|
end
|
|
52
62
|
end
|
|
53
63
|
end
|
|
@@ -115,9 +115,10 @@ module Pindo
|
|
|
115
115
|
file: OptionItem.new(
|
|
116
116
|
key: :file,
|
|
117
117
|
name: '上传文件',
|
|
118
|
-
description: '指定要上传的文件路径(支持 ipa/apk/app/exe/html
|
|
118
|
+
description: '指定要上传的文件路径(支持 ipa/apk/app/exe/html;兼容 --ipa 别名)',
|
|
119
119
|
type: String,
|
|
120
120
|
env_name: 'PINDO_JPS_FILE',
|
|
121
|
+
aliases: [:ipa],
|
|
121
122
|
optional: true,
|
|
122
123
|
example: 'pindo jps upload --file=path/to/app.ipa'
|
|
123
124
|
),
|
|
@@ -28,6 +28,24 @@ module Pindo
|
|
|
28
28
|
example: 'pindo ios autoresign --ipa=path/to/demo.ipa'
|
|
29
29
|
),
|
|
30
30
|
|
|
31
|
+
app: OptionItem.new(
|
|
32
|
+
key: :app,
|
|
33
|
+
name: 'macOS App',
|
|
34
|
+
description: '指定要重签名的 macOS .app 目录',
|
|
35
|
+
type: String,
|
|
36
|
+
env_name: 'PINDO_APP_FILE',
|
|
37
|
+
optional: true,
|
|
38
|
+
verify_block: proc do |value|
|
|
39
|
+
unless value.downcase.end_with?('.app')
|
|
40
|
+
raise "App 路径格式错误: #{value},必须以 .app 结尾"
|
|
41
|
+
end
|
|
42
|
+
unless File.directory?(value)
|
|
43
|
+
raise "App 目录不存在: #{value}"
|
|
44
|
+
end
|
|
45
|
+
end,
|
|
46
|
+
example: 'pindo ios autoresign --macos --app=path/to/Demo.app'
|
|
47
|
+
),
|
|
48
|
+
|
|
31
49
|
apk: OptionItem.new(
|
|
32
50
|
key: :apk,
|
|
33
51
|
name: 'APK 文件',
|
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.19.
|
|
4
|
+
version: 5.19.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- wade
|
|
@@ -406,6 +406,8 @@ files:
|
|
|
406
406
|
- lib/pindo/module/cert/pem_helper.rb
|
|
407
407
|
- lib/pindo/module/cert/provisioning_helper.rb
|
|
408
408
|
- lib/pindo/module/pgyer/pgyerhelper.rb
|
|
409
|
+
- lib/pindo/module/resign/ipa_resign_adapter.rb
|
|
410
|
+
- lib/pindo/module/resign/mac_app_resign_adapter.rb
|
|
409
411
|
- lib/pindo/module/task/core/concurrent_execution_strategy.rb
|
|
410
412
|
- lib/pindo/module/task/core/dependency_checker.rb
|
|
411
413
|
- lib/pindo/module/task/core/execution_strategy.rb
|
|
@@ -477,6 +479,7 @@ files:
|
|
|
477
479
|
- lib/pindo/module/xcode/applovin_xcode_helper.rb
|
|
478
480
|
- lib/pindo/module/xcode/cocoapods_helper.rb
|
|
479
481
|
- lib/pindo/module/xcode/ipa_resign_helper.rb
|
|
482
|
+
- lib/pindo/module/xcode/mac_app_resign_helper.rb
|
|
480
483
|
- lib/pindo/module/xcode/res/xcode_res_constant.rb
|
|
481
484
|
- lib/pindo/module/xcode/res/xcode_res_handler.rb
|
|
482
485
|
- lib/pindo/module/xcode/xcode_app_config.rb
|