Class: Pindo::CertHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/pindo/module/cert/cert_helper.rb

Class Method Summary collapse

Class Method Details

.clean_git_certs(apple_id:, pindo_dir:, deploy_cert_giturl:, dev_cert_giturl:, demo_apple_id:) ⇒ Object

证书管理辅助方法

清理 Git 仓库中的证书

Parameters:

  • apple_id (String)

    Apple ID

  • pindo_dir (String)

    Pindo 目录路径

  • deploy_cert_giturl (String)

    部署证书 Git URL

  • dev_cert_giturl (String)

    开发证书 Git URL

  • demo_apple_id (String)

    Demo Apple ID



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/pindo/module/cert/cert_helper.rb', line 117

def clean_git_certs(apple_id:, pindo_dir:, deploy_cert_giturl:, dev_cert_giturl:, demo_apple_id:)
  require_relative '../../base/git_handler'

  # 确定使用哪个 Git URL
  git_url = apple_id.eql?(demo_apple_id) ? dev_cert_giturl : deploy_cert_giturl

  puts "正在清理 Git 仓库中的证书..."
  puts "Apple ID: #{apple_id}"
  puts "Git URL: #{git_url}"

  # 克隆或更新证书仓库
  cert_repo_dir = Pindo::GitHandler.getcode_to_dir(
    reponame: Pindo::GitHandler.get_repo_base_name(repo_url: git_url),
    remote_url: git_url,
    path: pindo_dir,
    new_branch: apple_id
  )

  # 删除 certs 和 profiles 目录
  certs_dir = File.join(cert_repo_dir, "certs")
  profiles_dir = File.join(cert_repo_dir, "profiles")

  FileUtils.rm_rf(certs_dir) if File.exist?(certs_dir)
  FileUtils.rm_rf(profiles_dir) if File.exist?(profiles_dir)

  puts "✓ 已删除 certs 和 profiles 目录"

  # 提交并推送到远程仓库
  Pindo::GitHandler.prepare_gitenv()
  Pindo::GitHandler.git_addpush_repo(path: cert_repo_dir, message: "remove #{apple_id} certs")

  puts "✓ Git 仓库证书清理完成"
end

.clean_local_certsObject

清理本地证书删除系统中所有的 Apple Development 和 Apple Distribution 证书清理 Provisioning Profiles 文件夹



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/pindo/module/cert/cert_helper.rb', line 154

def clean_local_certs
  puts "正在清理本地证书..."

  # 获取所有代码签名证书
  output, _ = Open3.capture2('security find-identity -p codesigning')
  identity_ids_0 = []
  identity_ids_1 = output.split("\n").map { |line| line.split(' ')[1] if line.include?('Apple Development') }.compact
  identity_ids_2 = output.split("\n").map { |line| line.split(' ')[1] if line.include?('Apple Distribution') }.compact
  identity_ids = identity_ids_0 + identity_ids_1 + identity_ids_2
  identity_ids = identity_ids.uniq

  puts "找到 #{identity_ids.size} 个证书:"
  puts identity_ids

  # 删除证书
  identity_ids.each do |identity_id|
    system "security delete-certificate -Z #{identity_id}"
  end

  # 清理 Provisioning Profiles
  profile_file_dir = File.expand_path("~/Library/MobileDevice")
  profile_file_path = File.join(profile_file_dir, "Provisioning Profiles")
  if File.exist?(profile_file_path)
    FileUtils.rm_rf(profile_file_path)
    puts "✓ 已清理 Provisioning Profiles 文件夹"
  end

  puts "✓ 本地证书清理完成"
end

.create_and_install_certs(cert_mode:, storage:, apple_id:, bundle_id_array:, cert_type:, platform_type:, project_dir: nil, match_flag: false) ⇒ Array<Hash>

创建并安装证书(门面方法)

这是创建证书的统一入口,会根据 cert_mode 和 storage 参数自动选择合适的证书操作类(CertOperator)来执行具体操作

Parameters:

  • cert_mode (String)

    证书操作方式 (‘match’/‘custom’)

  • storage (String)

    证书存储后端 (‘git’/‘https’/‘s3’)

  • apple_id (String)

    Apple ID

  • bundle_id_array (Array<String>)

    Bundle ID 数组

  • cert_type (String)

    证书类型 (development/adhoc/appstore)

  • platform_type (String)

    平台类型 (ios/macos)

  • project_dir (String, nil) (defaults to: nil)

    项目目录(可选,如果提供则配置 Xcode 工程)

  • match_flag (Boolean) (defaults to: false)

    是否使用 –match 参数(向后兼容)

Returns:

  • (Array<Hash>)

    provisioning_info_array



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/pindo/module/cert/cert_helper.rb', line 40

def create_and_install_certs(
  cert_mode:,
  storage:,
  apple_id:,
  bundle_id_array:,
  cert_type:,
  platform_type:,
  project_dir: nil,
  match_flag: false
)
  # 选择合适的证书操作类
  operator = determine_cert_operator(
    cert_mode: cert_mode,
    storage: storage,
    match_flag: match_flag
  )

  # 调用具体的操作类执行创建和安装
  operator.create_and_install_certs(
    apple_id: apple_id,
    bundle_id_array: bundle_id_array,
    cert_type: cert_type,
    platform_type: platform_type,
    project_dir: project_dir
  )
end

.create_upload_cert_info(apple_id:, cert_type:, platform_type:) ⇒ Object

创建上传证书信息

Parameters:

  • apple_id (String)

    Apple ID

  • cert_type (String)

    证书类型

  • platform_type (String)

    平台类型



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/pindo/module/cert/cert_helper.rb', line 249

def create_upload_cert_info(apple_id:, cert_type:, platform_type:)
  pindo_single_config = Pindoconfig.instance

  cert_dest_dir = File.join(Dir.pwd, "cert")
  FileUtils.mkdir_p(cert_dest_dir) unless File.exist?(cert_dest_dir)

  cert_type_temp = cert_type
  cert_sub_dir = cert_type.downcase

  # 确定证书子目录
  if platform_type.downcase.eql?("macos")
    if cert_type.downcase.include?("development")
      cert_sub_dir = "development"
    elsif cert_type.downcase.eql?("appstore")
      cert_sub_dir = "distribution"
    elsif cert_type.downcase.eql?("developer_id")
      cert_type_temp = "developer_id"
      cert_sub_dir = "developer_id_application"
    else
      # 向后兼容:adhoc → developer_id
      cert_type_temp = "developer_id"
      cert_sub_dir = "developer_id_application"
    end
  else
    cert_sub_dir = "distribution" unless cert_type.downcase.include?("development")
  end

  # 确定 Git URL
  cert_git_url = apple_id.eql?(pindo_single_config.demo_apple_id) ?
                pindo_single_config.dev_cert_giturl :
                pindo_single_config.deploy_cert_giturl

  # 克隆证书仓库
  cert_reponame = cert_git_url.split("/").last.chomp(".git")
  certs_dir = Pindo::GitHandler.getcode_to_dir(
    reponame: cert_reponame,
    remote_url: cert_git_url,
    path: pindo_single_config.pindo_dir,
    new_branch: apple_id
  )

  # 查找密钥文件
  keys = Dir[File.join(certs_dir, "certs", cert_sub_dir, "*.p12")]
  decrypt_password = AESHelper.get_password(keychain_name: cert_git_url)
  output_dir = Dir.mktmpdir

  begin
    key_path = AESHelper.decrypt_specific_file(
      src_file: keys.first,
      password: decrypt_password,
      output_dir: output_dir
    )
  rescue Informative => e
    AESHelper.delete_password(keychain_name: cert_git_url)
    AESHelper.clear_password_cache_for(keychain_name: cert_git_url)
    raise Informative, "密钥解析失败,已清除错误的密码,请重新运行输入正确的密码"
  end

  # 解密成功,保存密码到 Keychain
  begin
    AESHelper.store_password(keychain_name: cert_git_url, password: decrypt_password)
    Funlog.instance.fancyinfo_success("密钥解密成功,密码已保存到Keychain")
  rescue => e
    Funlog.instance.fancyinfo_error("密码保存到Keychain失败: #{e.message}")
    Funlog.instance.fancyinfo_error("下次使用时需要重新输入密码")
  end

  FileUtils.copy(key_path, File.join(cert_dest_dir, "#{cert_type_temp}.p12"))
end

.create_upload_provisioning_info(apple_id:, cert_type:, platform_type:, provisioning_info_array:) ⇒ Hash

创建上传 Provisioning 信息

Parameters:

  • apple_id (String)

    Apple ID

  • cert_type (String)

    证书类型

  • platform_type (String)

    平台类型

  • provisioning_info_array (Array<Hash>)

    Provisioning 信息数组

Returns:

  • (Hash)

    账号证书集合



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/pindo/module/cert/cert_helper.rb', line 325

def create_upload_provisioning_info(apple_id:, cert_type:, platform_type:, provisioning_info_array:)
  cert_dir = File.join(Dir.pwd, "cert")
  cert_json_file = File.join(cert_dir, "certs.json")

  FileUtils.mkdir_p(cert_dir) unless File.exist?(cert_dir)

  # 读取现有 cert.json
  cert_json = []
  begin
    cert_json = JSON.parse(File.read(cert_json_file)) if File.exist?(cert_json_file)
    cert_json ||= []
  rescue StandardError => e
    cert_json = []
  end

  # 获取基本信息
  bundle_id = provisioning_info_array.select { |s| s["type"].to_s.eql?("bundle_id") }.first["bundle_id"]
  team_id_value = provisioning_info_array.first["team_id"]
  bundle_id_signing_identity = provisioning_info_array.first["signing_identity"]

  # 查找或创建账号证书集
   = {}
  select_result = cert_json.select { |x| x["account_name"].eql?(apple_id) }.first

  if select_result.nil?
     = {}
  else
     = select_result
    cert_json.delete_if { |x| x["account_name"].eql?(apple_id) }
  end

  ["account_name"] = apple_id
  ["team_id"] = team_id_value
  ["certs"] = ["certs"] || []

  # 查找或创建证书项
  cert_item = {}
  cert_item_result = ["certs"].select { |x| x["cert_id"].eql?(bundle_id_signing_identity) }.first

  if cert_item_result.nil?
    cert_item = {}
  else
    cert_item = cert_item_result
    ["certs"].delete_if { |x| x["cert_id"].eql?(bundle_id_signing_identity) }
  end

  # 确定 provision_start_name
  provision_start_name = "Development"
  cert_type_temp = cert_type

  if platform_type.downcase.include?("macos")
    if cert_type.downcase.include?("development")
      provision_start_name = "development"
    elsif cert_type.downcase.eql?("appstore")
      provision_start_name = "appstore"
    elsif cert_type.downcase.eql?("developer_id")
      cert_type_temp = "developer_id"
      provision_start_name = "direct"
    else
      # 向后兼容:adhoc → developer_id
      cert_type_temp = "developer_id"
      provision_start_name = "direct"
    end
  else
    if cert_type.downcase.include?("development")
      provision_start_name = "development"
    elsif cert_type.downcase.include?("adhoc")
      provision_start_name = "adhoc"
    else
      provision_start_name = "appstore"
    end
  end

  # 设置证书项
  cert_item["cert_id"] = cert_item["cert_id"] || bundle_id_signing_identity
  cert_item["password"] = "goodcert1"
  cert_item["cert_file"] = "#{cert_type_temp}.p12"
  cert_item["cert_type"] = cert_type_temp
  cert_item["cert_provisioning_group"] = cert_item["cert_provisioning_group"] || []

  # 查找或创建 provisioning group
  cert_provisioning_group_item = {}
  provisioning_group_id = [cert_type_temp, platform_type, bundle_id].join("_")

  provisioning_group_result = cert_item["cert_provisioning_group"].select { |x| x["provisioning_group_id"].eql?(provisioning_group_id) }.first

  if provisioning_group_result.nil?
    cert_provisioning_group_item = {}
  else
    cert_provisioning_group_item = provisioning_group_result
    cert_item["cert_provisioning_group"].delete_if { |x| x["provisioning_group_id"].eql?(provisioning_group_id) }
  end

  cert_provisioning_group_item["provisioning_group_id"] = provisioning_group_id
  cert_provisioning_group_item["platform_type"] = platform_type
  cert_provisioning_group_item["provisioning_main_bundle_id"] = bundle_id
  cert_provisioning_group_item["provisioning_items"] = []

  group_id = [provision_start_name, platform_type, bundle_id].join("_")

  # 处理每个 provisioning profile
  provisioning_info_array.each do |provisioning_info|
    bundle_id_temp = provisioning_info['bundle_id']
    provisioning_id = [provision_start_name, bundle_id_temp].join("_")
    real_path = provisioning_info["profile_path"]

    extname = File.extname(real_path)
    cert_provisioning_group_item["provisioning_items"] << {
      "bundle_id" => bundle_id_temp,
      "provisioning_id" => provisioning_id,
      "group_id" => group_id,
      "provisioning_file" => provisioning_id + extname
    }

    FileUtils.cp_r(real_path, File.join(cert_dir, provisioning_id + extname))
  end

  # 组装最终结果
  cert_item["cert_provisioning_group"] << cert_provisioning_group_item
  ["certs"] << cert_item
  cert_json << 

  # 写入 JSON 文件
  File.open(cert_json_file, "w") do |f|
    f.write(JSON.pretty_generate(cert_json.compact))
  end

  
end

.fetch_and_install_certs(cert_mode:, storage:, apple_id:, bundle_id_array:, cert_type:, platform_type:, project_dir: nil, match_flag: false) ⇒ Array<Hash>

获取并安装证书(门面方法)

这是获取证书的统一入口,会根据 cert_mode 和 storage 参数自动选择合适的证书操作类(CertOperator)来执行具体操作

Parameters:

  • cert_mode (String)

    证书操作方式 (‘match’/‘custom’)

  • storage (String)

    证书存储后端 (‘git’/‘https’/‘s3’)

  • apple_id (String)

    Apple ID

  • bundle_id_array (Array<String>)

    Bundle ID 数组

  • cert_type (String)

    证书类型 (development/adhoc/appstore)

  • platform_type (String)

    平台类型 (ios/macos)

  • project_dir (String, nil) (defaults to: nil)

    项目目录(可选,如果提供则配置 Xcode 工程)

  • match_flag (Boolean) (defaults to: false)

    是否使用 –match 参数(向后兼容)

Returns:

  • (Array<Hash>)

    provisioning_info_array



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/pindo/module/cert/cert_helper.rb', line 81

def fetch_and_install_certs(
  cert_mode:,
  storage:,
  apple_id:,
  bundle_id_array:,
  cert_type:,
  platform_type:,
  project_dir: nil,
  match_flag: false
)
  # 选择合适的证书操作类
  operator = determine_cert_operator(
    cert_mode: cert_mode,
    storage: storage,
    match_flag: match_flag
  )

  # 调用具体的操作类执行获取和安装
  operator.fetch_and_install_certs(
    apple_id: apple_id,
    bundle_id_array: bundle_id_array,
    cert_type: cert_type,
    platform_type: platform_type,
    project_dir: project_dir
  )
end

.install_and_config_certs(build_type:, platform_type: 'ios', project_dir: Dir.pwd) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/pindo/module/cert/cert_helper.rb', line 200

def install_and_config_certs(build_type:, platform_type: 'ios', project_dir: Dir.pwd)
  # 标准化构建类型
  cert_type = Pindo::Options::BuildOptions.normalize_build_type(build_type)

  # 使用 IosConfigParser 单例
  require 'pindo/config/ios_config_parser'
  config_parser = Pindo::IosConfigParser.instance

  # 检查配置是否已加载
  if config_parser.config_json.nil?
    raise Informative, "install_and_config_certs 方法中,配置未加载 config.json"
  end

  apple_id = config_parser.apple_id
  bundle_id_array = config_parser.get_bundle_id_array

  # 根据 build_type 决定存储方式
  # - appstore: 使用 Git 存储(严格版本控制)
  # - dev/adhoc: 使用 HTTPS 存储(快速便捷)
  storage_mode = (cert_type == 'appstore') ? 'git' : 'https'

  # 调用 fetch_and_install_certs
  provisioning_info_array = fetch_and_install_certs(
    cert_mode: 'custom',
    storage: storage_mode,
    apple_id: apple_id,
    bundle_id_array: bundle_id_array,
    cert_type: cert_type,
    platform_type: platform_type,
    project_dir: project_dir,
    match_flag: false
  )

  team_id = provisioning_info_array.first["team_id"]

  {
    provisioning_info_array: provisioning_info_array,
    team_id: team_id
  }
end

.is_cert_valid?(cer_certificate_path) ⇒ Boolean

Returns:

  • (Boolean)


189
190
191
192
193
# File 'lib/pindo/module/cert/cert_helper.rb', line 189

def is_cert_valid?(cer_certificate_path)
  cert = OpenSSL::X509::Certificate.new(File.binread(cer_certificate_path))
  now = Time.now.utc
  return (now <=> cert.not_after) == -1
end

.isMac?Boolean

Returns:

  • (Boolean)


195
196
197
# File 'lib/pindo/module/cert/cert_helper.rb', line 195

def isMac?
  (/darwin/ =~ RUBY_PLATFORM) != nil
end

.select_cert_or_key(paths:) ⇒ Object



184
185
186
187
# File 'lib/pindo/module/cert/cert_helper.rb', line 184

def select_cert_or_key(paths:)
  cert_id_path = ENV['MATCH_CERTIFICATE_ID'] ? paths.find { |path| path.include?(ENV['MATCH_CERTIFICATE_ID']) } : nil
  cert_id_path || paths.last
end