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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a77caec349df71376df7ea433567423ae745f767297fb29377e265f123b63d8
4
- data.tar.gz: 5f8040b7e13937405b635906ad34dcd4466af83292d65b823968d22fce9f3081
3
+ metadata.gz: e6cdbb729bb1840389cfd9e6252bc60d44d9faa010c47295eeed07a0989b3c42
4
+ data.tar.gz: ef3fa0cbf75139df0333a8dbf0d9f57388c93814da2fb9077673fb0e26df46e7
5
5
  SHA512:
6
- metadata.gz: 9fb90c4d057aab583df7780e4e8880c47507da9c64f9b05dfb48b778c87ae1695cf33711c6b777ad6540284f97a101ba5c28f10e8d6a91dfc6243b6a4a25843f
7
- data.tar.gz: 57f746e6f8a01dcebab6615d1e30de5d7b6a1eb2b221bf8db50a10c66a0aab240279064b86f89bef7e0ccace8186eaf87da42a008a2f50f2a403f0d5ee5ec593
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
- if @args_macos_flag
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. 查找 IPA 文件
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, "未找到需要重签名的 IPA 文件"
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.gsub(/\.ipa$/, '_resigned.ipa')
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. 如果通过选项或位置参数指定了 IPA 文件且存在,直接返回
218
- if @args_ipa_file && File.exist?(@args_ipa_file)
219
- puts "\n使用指定的 IPA 文件: #{@args_ipa_file}"
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
- ipa_files = Dir.glob(File.join(current_dir, "*.ipa"))
241
+ pattern = macos_platform? ? "*.app" : "*.ipa"
242
+ ipa_files = Dir.glob(File.join(current_dir, pattern))
227
243
 
228
- # 过滤掉 _resigned.ipa 并找到最新的
229
- ipa_file = ipa_files.max_by do |f|
230
- File.basename(f).include?("_resigned") ? Time.local(0, 1, 1) : File.mtime(f)
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
- puts "\n⚠️ 未找到 IPA 文件"
241
- puts "请输入 IPA 文件路径(支持相对路径或绝对路径):"
255
+ package_name = macos_platform? ? "macOS .app" : "IPA"
256
+ puts "\n⚠️ 未找到 #{package_name}"
257
+ puts "请输入 #{package_name} 路径(支持相对路径或绝对路径):"
242
258
 
243
- ipa_path = ask("IPA 文件路径: ") do |q|
244
- q.validate = /\.ipa$/i
245
- q.responses[:not_valid] = "❌ 请输入有效的 IPA 文件路径(必须以 .ipa 结尾)"
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
- puts "✓ 使用输入的 IPA 文件: #{ipa_path}"
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: bundle_id,
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
- # 根据 Xcode 工程类型动态设置 package_type(macOS 工程使用 'mac',否则使用 'ipa')
287
- is_macos = Pindo::BuildHelper.share_instance.macos_project?(Dir.pwd)
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
- windows_exe_path = File.expand_path(ipa_file_upload)
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("重签名 IPA", options)
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. 确定要重签名的 IPA 文件
100
+ # 1. 确定要重签名的安装包
99
101
  ipa_to_resign = determine_ipa_file
100
102
  unless ipa_to_resign && File.exist?(ipa_to_resign)
101
- raise "未找到需要重签名的 IPA 文件"
103
+ raise "未找到需要重签名的安装包"
102
104
  end
103
105
 
106
+ validate_package_type!(ipa_to_resign)
107
+
104
108
  puts "\n▶ 重签名信息"
105
- puts " IPA: #{File.basename(ipa_to_resign)}"
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
- resigned_ipa = Pindo::IpaResignHelper.resign_ipa(
121
- ipa_file_path: ipa_to_resign,
122
- bundle_id: @bundle_id
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 "重签名失败:未生成重签名后的 IPA 文件"
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
- ipa_files = Dir.glob(File.join(@ipa_path, "*.ipa"))
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
- argv.flag?(item.key.to_s, nil)
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
- argv.option(item.key.to_s, nil)
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
@@ -6,7 +6,7 @@ require 'time'
6
6
 
7
7
  module Pindo
8
8
 
9
- VERSION = "5.19.3"
9
+ VERSION = "5.19.4"
10
10
 
11
11
  class VersionCheck
12
12
  RUBYGEMS_API = 'https://rubygems.org/api/v1/gems/pindo.json'
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.3
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