pindo 5.18.17 → 5.18.20

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.
@@ -1,5 +1,6 @@
1
1
  require 'singleton'
2
2
  require 'json'
3
+ require 'pindo/base/informative'
3
4
 
4
5
  module Pindo
5
6
  # iOS 配置解析器(单例模式)
@@ -21,7 +22,32 @@ module Pindo
21
22
  class IosConfigParser
22
23
  include Singleton
23
24
 
24
- attr_reader :config_file_path, :config_json, :app_repo_name
25
+ # AdHoc 合并时需要替换前缀的所有 Bundle ID 字段
26
+ ADHOC_BUNDLE_ID_KEYS = %w[
27
+ app_identifier
28
+ app_identifier_pushcontent
29
+ app_identifier_pushservice
30
+ app_identifier_keyboard
31
+ app_identifier_imessage
32
+ app_identifier_extension
33
+ app_identifier_siri
34
+ app_identifier_siriui
35
+ app_identifier_widget
36
+ app_identifier_extensionad
37
+ app_identifier_extensionporn
38
+ app_identifier_watchapp
39
+ app_identifier_watchapp_extension
40
+ ].freeze
41
+
42
+ # AdHoc 合并 app_setting 时需要保留原值(服务器/URL)的 key
43
+ ADHOC_EXCLUDED_APP_SETTING_KEYS = %w[
44
+ kGUKeyAppClientHost
45
+ kGUKeyResBaseUrl
46
+ app_web_host
47
+ app_client_url
48
+ ].freeze
49
+
50
+ attr_reader :config_file_path, :config_json, :app_repo_name, :adhoc_config_dir
25
51
 
26
52
  def initialize
27
53
  @config_file_path = nil
@@ -29,6 +55,8 @@ module Pindo
29
55
  @config_data = {}
30
56
  @overrides = {}
31
57
  @app_repo_name = nil # 保存原始 Bundle ID,用于配置仓库路径
58
+ @adhoc_merged = false # 是否已合并 AdHoc 配置(幂等标志)
59
+ @adhoc_config_dir = nil # 已合并的 AdHoc 配置仓库目录(缓存,避免重复克隆)
32
60
  end
33
61
 
34
62
  # 加载配置文件
@@ -47,6 +75,11 @@ module Pindo
47
75
  raise "配置文件格式错误: #{e.message}"
48
76
  end
49
77
 
78
+ # 从磁盘重新加载的配置是未合并的,重置 AdHoc 标志,
79
+ # 否则后续 merge 会被幂等短路错误跳过,导致用原始配置构建
80
+ @adhoc_merged = false
81
+ @adhoc_config_dir = nil
82
+
50
83
  # 解析配置
51
84
  parse_config_data
52
85
 
@@ -69,6 +102,14 @@ module Pindo
69
102
  @config_data = {}
70
103
  @overrides = {}
71
104
  @app_repo_name = nil
105
+ @adhoc_merged = false
106
+ @adhoc_config_dir = nil
107
+ end
108
+
109
+ # AdHoc 配置是否已合并(幂等短路用)
110
+ # @return [Boolean]
111
+ def adhoc_merged?
112
+ @adhoc_merged == true
72
113
  end
73
114
 
74
115
  # ==================== 账号信息 ====================
@@ -270,6 +311,34 @@ module Pindo
270
311
  @config_data[:ios_deployment_target]
271
312
  end
272
313
 
314
+ # ==================== Unity AppConfig 映射 ====================
315
+
316
+ # 提取需要写入 Unity 工程 AppConfig(TextAsset JSON)的字段
317
+ # 仅包含在 config.json 中有明确来源的项,已去除 nil/空值
318
+ # @return [Hash{String=>String}] AppConfig 键 => 值
319
+ def unity_appconfig_values
320
+ return {} unless loaded?
321
+
322
+ app_info = @config_json['app_info'] || {}
323
+ app_setting = @config_json['app_setting'] || {}
324
+
325
+ {
326
+ "apple_app_id" => app_info['app_id_ios'],
327
+ "appsflyer_dev_key" => app_setting['kGUKeyAppsFlyerDevKey'],
328
+ "support_email" => app_setting['kGUKeyAppSupportEmail'],
329
+ "fb_app_id" => app_info['facebook_app_id'],
330
+ "fb_client_token" => app_info['facebook_client_token'],
331
+ "fb_app_label" => app_info['app_display_name'],
332
+ "mecha_host" => app_setting['kGUKeyAppClientHost'],
333
+ "mecha_app_id" => app_setting['kGUKeyAppPlatform'],
334
+ "google_web_client_id" => app_setting['kGUKeyGoogleWebClientId'],
335
+ "google_ios_client_id" => app_setting['kGUKeyGoogleIosClientId']
336
+ }.each_with_object({}) do |(key, value), result|
337
+ next if value.nil? || value.to_s.strip.empty?
338
+ result[key] = value.to_s.strip
339
+ end
340
+ end
341
+
273
342
  # ==================== 配置验证 ====================
274
343
 
275
344
  # 验证必需的配置项是否存在
@@ -297,151 +366,38 @@ module Pindo
297
366
  # ==================== AdHoc 发布配置合并 ====================
298
367
 
299
368
  # 合并 AdHoc 发布配置
300
- # 1. 替换所有 Bundle ID AdHoc 配置的 Bundle ID(确保使用 AdHoc 证书)
301
- # 2. 合并第三方 SDK 配置(FacebookAdMobAppLovin)
302
- # 3. 合并 app_setting(排除主机和 URL 配置)
369
+ # 1. 替换 Apple ID / 公司名为 AdHoc 配置值
370
+ # 2. 替换所有 Bundle ID(含 ExtensionApp GroupiCloud)为 AdHoc 前缀(确保使用 AdHoc 证书)
371
+ # 3. 合并第三方 SDK 配置(Facebook、AdMob、AppLovin)
372
+ # 4. 合并 app_setting(排除主机和 URL 配置)
373
+ #
374
+ # 幂等:已合并则直接短路,不再二次改写。
375
+ # 失败采用 fail-fast:配置文件缺失/解析失败/缺关键字段时抛 Informative,
376
+ # 由调用方决定是否中断,避免静默退化为"原始配置"产出错误身份的包。
377
+ #
303
378
  # @param adhoc_config_dir [String] AdHoc 配置仓库目录
304
- # @return [Boolean] 是否成功
379
+ # @return [Boolean] 是否成功合并
380
+ # @raise [Pindo::Informative] AdHoc 配置无效或缺失关键字段
305
381
  def modify_config_with_adhoc(adhoc_config_dir:)
306
- return false unless loaded?
307
- return false if adhoc_config_dir.nil? || adhoc_config_dir.empty?
308
-
309
- begin
310
- # 1. 读取 AdHoc 配置
311
- adhoc_config_file = File.join(adhoc_config_dir, "config.json")
312
- unless File.exist?(adhoc_config_file)
313
- puts "AdHoc 配置文件不存在: #{adhoc_config_file}"
314
- return false
315
- end
316
-
317
- adhoc_config_json = JSON.parse(File.read(adhoc_config_file))
318
-
319
- # 1.5 替换 Apple ID(使用 AdHoc 配置的 Apple ID)
320
- if adhoc_config_json.dig('account_info', 'apple_acount_id')
321
- original_apple_id = @config_json.dig('account_info', 'apple_acount_id')&.strip # 去除前后空格
322
- adhoc_apple_id = adhoc_config_json['account_info']['apple_acount_id']&.strip # 去除前后空格
323
-
324
- if original_apple_id && adhoc_apple_id && !adhoc_apple_id.empty? && original_apple_id != adhoc_apple_id
325
- @config_json['account_info']['apple_acount_id'] = adhoc_apple_id
326
- puts " 替换 Apple ID:"
327
- puts " 原始 Apple ID: #{original_apple_id}"
328
- puts " AdHoc Apple ID: #{adhoc_apple_id}"
329
- end
330
-
331
- # 如果 AdHoc 配置包含 company_name,也一并替换
332
- if adhoc_config_json.dig('account_info', 'acount_company_name')
333
- company_name = adhoc_config_json['account_info']['acount_company_name']&.strip # 去除前后空格
334
- @config_json['account_info']['acount_company_name'] = company_name if company_name && !company_name.empty?
335
- end
336
- end
337
-
338
- # 2. 替换所有 Bundle ID(使用 AdHoc 配置的 Bundle ID)
339
- original_bundle_id = @config_json.dig('app_info', 'app_identifier')
340
- adhoc_bundle_id = adhoc_config_json.dig('app_info', 'app_identifier')
341
-
342
- if original_bundle_id && adhoc_bundle_id && original_bundle_id != adhoc_bundle_id
343
- puts " 替换 Bundle ID 前缀:"
344
- puts " 原始前缀: #{original_bundle_id}"
345
- puts " AdHoc 前缀: #{adhoc_bundle_id}"
346
-
347
- # Bundle ID 字段映射表
348
- bundle_id_keys = [
349
- 'app_identifier', # 主应用
350
- 'app_identifier_pushcontent', # Push Content Extension
351
- 'app_identifier_pushservice', # Push Service Extension
352
- 'app_identifier_keyboard', # Keyboard Extension
353
- 'app_identifier_imessage', # iMessage Extension
354
- 'app_identifier_extension', # 通用 Extension
355
- 'app_identifier_siri', # Siri Extension
356
- 'app_identifier_siriui', # Siri UI Extension
357
- 'app_identifier_widget', # Widget Extension
358
- 'app_identifier_extensionad', # 广告 Extension
359
- 'app_identifier_extensionporn',# Porn Extension
360
- 'app_identifier_watchapp', # Watch App
361
- 'app_identifier_watchapp_extension' # Watch App Extension
362
- ]
363
-
364
- # 遍历所有 Bundle ID 字段并替换前缀
365
- bundle_id_keys.each do |key|
366
- current_value = @config_json.dig('app_info', key)
367
- next if current_value.nil? || current_value.empty?
368
-
369
- # 如果当前值以原始主 Bundle ID 开头,替换为 AdHoc Bundle ID
370
- if current_value.start_with?(original_bundle_id)
371
- # 计算后缀部分(如 .extension、.keyboard 等)
372
- suffix = current_value[original_bundle_id.length..-1]
373
- new_value = adhoc_bundle_id + suffix
374
-
375
- @config_json['app_info'][key] = new_value
376
- puts " ✓ #{key}: #{current_value} → #{new_value}"
377
- end
378
- end
379
- end
380
-
381
- # 2.5 替换 App Group ID 和 iCloud Container ID
382
- if original_bundle_id && adhoc_bundle_id && original_bundle_id != adhoc_bundle_id
383
- # 处理 app_group_id (格式: group.com.example.app)
384
- if @config_json.dig('app_setting', 'app_group_id')
385
- current_group_id = @config_json['app_setting']['app_group_id']
386
- if current_group_id.include?(original_bundle_id)
387
- new_group_id = current_group_id.gsub(original_bundle_id, adhoc_bundle_id)
388
- @config_json['app_setting']['app_group_id'] = new_group_id
389
- puts " ✓ app_group_id: #{current_group_id} → #{new_group_id}"
390
- end
391
- end
392
-
393
- # 处理 app_icloud_id (格式: iCloud.com.example.app)
394
- if @config_json.dig('app_setting', 'app_icloud_id')
395
- current_icloud_id = @config_json['app_setting']['app_icloud_id']
396
- if current_icloud_id.include?(original_bundle_id)
397
- new_icloud_id = current_icloud_id.gsub(original_bundle_id, adhoc_bundle_id)
398
- @config_json['app_setting']['app_icloud_id'] = new_icloud_id
399
- puts " ✓ app_icloud_id: #{current_icloud_id} → #{new_icloud_id}"
400
- end
401
- end
402
- end
403
-
404
- # 3. 合并第三方 SDK 配置
405
- # Facebook 配置
406
- if adhoc_config_json.dig('app_info', 'facebook_app_id')
407
- @config_json['app_info']['facebook_app_id'] = adhoc_config_json['app_info']['facebook_app_id']
408
- @config_json['app_info']['facebook_client_token'] = adhoc_config_json['app_info']['facebook_client_token']
409
- end
410
-
411
- # AdMob 配置
412
- if @config_json.dig('app_info', 'admob_app_id') && adhoc_config_json.dig('app_info', 'admob_app_id')
413
- @config_json['app_info']['admob_app_id'] = adhoc_config_json['app_info']['admob_app_id']
414
- end
415
-
416
- # AppLovin 配置
417
- if @config_json.dig('app_info', 'applovin_app_id') && adhoc_config_json.dig('app_info', 'applovin_app_id')
418
- @config_json['app_info']['applovin_app_id'] = adhoc_config_json['app_info']['applovin_app_id']
419
- end
382
+ raise Informative, "配置未加载,无法合并 AdHoc 配置" unless loaded?
383
+ return true if adhoc_merged? # 幂等短路
384
+ raise Informative, "AdHoc 配置目录无效" if adhoc_config_dir.nil? || adhoc_config_dir.empty?
420
385
 
421
- # 4. 合并 app_setting(排除主机和 URL 配置)
422
- excluded_keys = ['kGUKeyAppClientHost', 'kGUKeyResBaseUrl', 'app_web_host', 'app_client_url']
386
+ adhoc_config_json = read_adhoc_config(adhoc_config_dir)
423
387
 
424
- if @config_json['app_setting'] && adhoc_config_json['app_setting']
425
- @config_json['app_setting'].each_key do |key|
426
- next if excluded_keys.include?(key)
388
+ merge_adhoc_apple_account(adhoc_config_json)
389
+ replace_adhoc_bundle_identifiers(adhoc_config_json)
390
+ merge_adhoc_sdk_config(adhoc_config_json)
391
+ merge_adhoc_app_setting(adhoc_config_json)
427
392
 
428
- if adhoc_config_json['app_setting'][key]
429
- @config_json['app_setting'][key] = adhoc_config_json['app_setting'][key]
430
- end
431
- end
432
- end
433
-
434
- # 5. 重新解析配置数据,同步更新 @config_data
435
- # 这样 bundle_id、bundle_id_pushcontent 等属性方法才能返回替换后的值
436
- parse_config_data
393
+ # 重新解析配置数据,同步更新 @config_data
394
+ # 这样 bundle_id、bundle_id_pushcontent 等属性方法才能返回替换后的值
395
+ parse_config_data
437
396
 
438
- puts "✅ 已替换 Apple ID、Bundle ID 并合并 AdHoc 配置(Facebook、AdMob、AppLovin)"
439
- true
440
- rescue => e
441
- puts "合并发布配置失败: #{e.message}"
442
- puts e.backtrace.join("\n") if ENV['PINDO_DEBUG']
443
- false
444
- end
397
+ @adhoc_merged = true
398
+ @adhoc_config_dir = adhoc_config_dir
399
+ puts "✅ 已替换 Apple ID、Bundle ID 并合并 AdHoc 配置(Facebook、AdMob、AppLovin)"
400
+ true
445
401
  end
446
402
 
447
403
  # ==================== 调试和信息 ====================
@@ -483,6 +439,122 @@ module Pindo
483
439
 
484
440
  private
485
441
 
442
+ # ==================== AdHoc 合并辅助方法 ====================
443
+
444
+ # 读取并解析 AdHoc 配置仓库的 config.json(失败 fail-fast)
445
+ # @raise [Pindo::Informative] 文件不存在或 JSON 解析失败
446
+ def read_adhoc_config(adhoc_config_dir)
447
+ adhoc_config_file = File.join(adhoc_config_dir, "config.json")
448
+ unless File.exist?(adhoc_config_file)
449
+ raise Informative, "AdHoc 配置文件不存在: #{adhoc_config_file}"
450
+ end
451
+
452
+ JSON.parse(File.read(adhoc_config_file))
453
+ rescue JSON::ParserError => e
454
+ raise Informative, "AdHoc 配置文件解析失败: #{e.message}"
455
+ end
456
+
457
+ # 替换 Apple ID 与公司名
458
+ # 注:原配置无 account_info 容器则跳过(原代码此处会 NoMethodError,这里改为安全跳过)
459
+ def merge_adhoc_apple_account(adhoc_config_json)
460
+ return unless @config_json['account_info']
461
+
462
+ adhoc_account = adhoc_config_json['account_info'] || {}
463
+
464
+ adhoc_apple_id = adhoc_account['apple_acount_id']&.strip
465
+ if adhoc_apple_id && !adhoc_apple_id.empty?
466
+ original_apple_id = @config_json['account_info']['apple_acount_id']&.strip
467
+ if original_apple_id != adhoc_apple_id
468
+ @config_json['account_info']['apple_acount_id'] = adhoc_apple_id
469
+ puts " 替换 Apple ID:"
470
+ puts " 原始 Apple ID: #{original_apple_id}"
471
+ puts " AdHoc Apple ID: #{adhoc_apple_id}"
472
+ end
473
+ end
474
+
475
+ company_name = adhoc_account['acount_company_name']&.strip
476
+ if company_name && !company_name.empty?
477
+ @config_json['account_info']['acount_company_name'] = company_name
478
+ end
479
+ end
480
+
481
+ # 替换主 Bundle ID、所有 Extension Bundle ID、App Group、iCloud Container
482
+ # @raise [Pindo::Informative] AdHoc 配置缺少主 Bundle ID
483
+ def replace_adhoc_bundle_identifiers(adhoc_config_json)
484
+ original_bundle_id = @config_json.dig('app_info', 'app_identifier')
485
+ adhoc_bundle_id = adhoc_config_json.dig('app_info', 'app_identifier')
486
+
487
+ if adhoc_bundle_id.nil? || adhoc_bundle_id.empty?
488
+ raise Informative, "AdHoc 配置缺少 app_identifier(主 Bundle ID),无法替换证书身份"
489
+ end
490
+ return if original_bundle_id.nil? || original_bundle_id == adhoc_bundle_id
491
+
492
+ puts " 替换 Bundle ID 前缀:"
493
+ puts " 原始前缀: #{original_bundle_id}"
494
+ puts " AdHoc 前缀: #{adhoc_bundle_id}"
495
+
496
+ # 替换所有 app_identifier_* 字段:凡以原始主 Bundle ID 开头的,替换前缀保留后缀
497
+ ADHOC_BUNDLE_ID_KEYS.each do |key|
498
+ current_value = @config_json.dig('app_info', key)
499
+ next if current_value.nil? || current_value.empty?
500
+ next unless current_value.start_with?(original_bundle_id)
501
+
502
+ new_value = adhoc_bundle_id + current_value[original_bundle_id.length..-1]
503
+ @config_json['app_info'][key] = new_value
504
+ puts " ✓ #{key}: #{current_value} → #{new_value}"
505
+ end
506
+
507
+ # App Group ID(group.xxx)与 iCloud Container ID(iCloud.xxx)内嵌的 Bundle ID
508
+ replace_adhoc_app_setting_id('app_group_id', original_bundle_id, adhoc_bundle_id)
509
+ replace_adhoc_app_setting_id('app_icloud_id', original_bundle_id, adhoc_bundle_id)
510
+ end
511
+
512
+ # 替换 app_setting 中内嵌了原始 Bundle ID 的标识符(app_group_id / app_icloud_id)
513
+ def replace_adhoc_app_setting_id(key, original_bundle_id, adhoc_bundle_id)
514
+ current_value = @config_json.dig('app_setting', key)
515
+ return if current_value.nil? || !current_value.include?(original_bundle_id)
516
+
517
+ new_value = current_value.gsub(original_bundle_id, adhoc_bundle_id)
518
+ @config_json['app_setting'][key] = new_value
519
+ puts " ✓ #{key}: #{current_value} → #{new_value}"
520
+ end
521
+
522
+ # 合并第三方 SDK 配置(Facebook / AdMob / AppLovin)
523
+ # 统一规则:仅当 AdHoc 配置存在对应值时才覆盖,避免用 nil 抹掉原有有效值
524
+ def merge_adhoc_sdk_config(adhoc_config_json)
525
+ app_info = adhoc_config_json['app_info'] || {}
526
+
527
+ if app_info['facebook_app_id']
528
+ @config_json['app_info']['facebook_app_id'] = app_info['facebook_app_id']
529
+ # token 存在才覆盖,否则保留原有值
530
+ if app_info['facebook_client_token']
531
+ @config_json['app_info']['facebook_client_token'] = app_info['facebook_client_token']
532
+ end
533
+ end
534
+
535
+ if @config_json.dig('app_info', 'admob_app_id') && app_info['admob_app_id']
536
+ @config_json['app_info']['admob_app_id'] = app_info['admob_app_id']
537
+ end
538
+
539
+ if @config_json.dig('app_info', 'applovin_app_id') && app_info['applovin_app_id']
540
+ @config_json['app_info']['applovin_app_id'] = app_info['applovin_app_id']
541
+ end
542
+ end
543
+
544
+ # 合并 app_setting,逐 key 用 AdHoc 值覆盖原配置已有的 key
545
+ # 排除服务器/URL 相关 key,保留原环境后端地址(让测试包连真实后端)
546
+ def merge_adhoc_app_setting(adhoc_config_json)
547
+ adhoc_app_setting = adhoc_config_json['app_setting']
548
+ return unless @config_json['app_setting'] && adhoc_app_setting
549
+
550
+ @config_json['app_setting'].each_key do |key|
551
+ next if ADHOC_EXCLUDED_APP_SETTING_KEYS.include?(key)
552
+ next unless adhoc_app_setting[key]
553
+
554
+ @config_json['app_setting'][key] = adhoc_app_setting[key]
555
+ end
556
+ end
557
+
486
558
  # 解析配置数据
487
559
  def parse_config_data
488
560
  return unless @config_json