pindo 5.18.17 → 5.19.1

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/git_handler.rb +44 -83
  3. data/lib/pindo/command/appstore/adhocbuild.rb +13 -2
  4. data/lib/pindo/command/appstore/autobuild.rb +2 -1
  5. data/lib/pindo/command/appstore/iap.rb +58 -54
  6. data/lib/pindo/command/repo/create.rb +1 -1
  7. data/lib/pindo/command/repo/search.rb +1 -1
  8. data/lib/pindo/command/utils/installskills.rb +2 -2
  9. data/lib/pindo/command/utils/renewproj.rb +0 -1
  10. data/lib/pindo/config/build_info_manager.rb +31 -0
  11. data/lib/pindo/config/ios_config_parser.rb +212 -140
  12. data/lib/pindo/module/android/android_build_helper.rb +39 -16
  13. data/lib/pindo/module/android/gradle_helper.rb +8 -14
  14. data/lib/pindo/module/appstore/appstore_in_app_purchase.rb +80 -56
  15. data/lib/pindo/module/cert/cert_helper.rb +2 -2
  16. data/lib/pindo/module/pgyer/pgyerhelper.rb +18 -20
  17. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +1 -37
  18. data/lib/pindo/module/task/model/build/android_build_task.rb +40 -0
  19. data/lib/pindo/module/task/model/build/ios_build_adhoc_task.rb +9 -35
  20. data/lib/pindo/module/task/model/build/ios_build_appstore_task.rb +5 -5
  21. data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +14 -4
  22. data/lib/pindo/module/task/model/build_task.rb +2 -0
  23. data/lib/pindo/module/task/model/git/git_commit_task.rb +11 -1
  24. data/lib/pindo/module/task/model/git/git_tag_task.rb +92 -77
  25. data/lib/pindo/module/task/model/jps/jps_workflow_message_task.rb +8 -10
  26. data/lib/pindo/module/task/model/unity/unity_export_task.rb +44 -0
  27. data/lib/pindo/module/task/pindo_task.rb +2 -2
  28. data/lib/pindo/module/task/task_manager.rb +14 -6
  29. data/lib/pindo/module/task/task_reporter.rb +45 -0
  30. data/lib/pindo/module/unity/nuget_helper.rb +5 -20
  31. data/lib/pindo/module/utils/git_repo_helper.rb +69 -36
  32. data/lib/pindo/module/xcode/ipa_resign_helper.rb +33 -62
  33. data/lib/pindo/module/xcode/res/xcode_res_handler.rb +25 -4
  34. data/lib/pindo/module/xcode/xcode_build_config.rb +7 -1
  35. data/lib/pindo/module/xcode/xcode_build_helper.rb +36 -4
  36. data/lib/pindo/module/xcode/xcode_res_helper.rb +8 -0
  37. data/lib/pindo/options/helpers/bundleid_selector.rb +23 -6
  38. data/lib/pindo/version.rb +1 -1
  39. metadata +2 -2
@@ -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
@@ -6,6 +6,7 @@ require_relative 'java_env_helper'
6
6
  require_relative 'keystore_helper'
7
7
  require_relative 'workflow_gradle_injector'
8
8
  require 'fileutils'
9
+ require 'find'
9
10
 
10
11
  module Pindo
11
12
  class AndroidBuildHelper
@@ -24,6 +25,10 @@ module Pindo
24
25
  raise ArgumentError, "项目目录不能为空" if project_dir.nil?
25
26
  raise ArgumentError, "项目目录不存在" unless File.directory?(project_dir)
26
27
 
28
+ # 构建前先清理 macOS 自动生成的 .DS_Store
29
+ # 避免其被带入 base / asset pack 模块的 assets/ 导致冲突,或引起 Gradle clean 在遍历删除时出现竞态
30
+ prune_ds_store_files!(project_dir)
31
+
27
32
  # 设置 Java Home 环境变量
28
33
  Pindo::JavaEnvHelper.setup_java_home_from_project(project_dir)
29
34
 
@@ -396,20 +401,19 @@ module Pindo
396
401
  puts ' AAB 构建前先执行 Gradle clean'
397
402
  gradle_clean_resilient!(project_path)
398
403
 
399
- cmd = "cd \"#{project_path}\" && ./gradlew --no-daemon #{bundle_task}"
400
- system(cmd)
404
+ # 数组参数 + chdir,避免 project_path 拼接进 shell
405
+ system("./gradlew", "--no-daemon", bundle_task, chdir: project_path)
401
406
  end
402
407
 
403
408
  # clean 失败(占用、.DS_Store 竞态等)时:停 Daemon → 再删 .DS_Store → 物理删除各模块 build/,与 Gradle clean 产物一致,不中断后续 bundle
404
409
  private def gradle_clean_resilient!(project_path)
405
- prune_ds_store_before_gradle_clean!(project_path)
406
- cmd = %(cd "#{project_path}" && ./gradlew --no-daemon clean)
407
- return if system(cmd)
410
+ prune_ds_store_files!(project_path)
411
+ return if system("./gradlew", "--no-daemon", "clean", chdir: project_path)
408
412
 
409
413
  puts ' ⚠ Gradle clean 失败,正在自动恢复:--stop、清理 .DS_Store、删除各模块 build/ ...'
410
- system(%(cd "#{project_path}" && ./gradlew --stop), out: File::NULL, err: File::NULL)
414
+ system("./gradlew", "--stop", chdir: project_path, out: File::NULL, err: File::NULL)
411
415
  sleep 0.3
412
- prune_ds_store_before_gradle_clean!(project_path)
416
+ prune_ds_store_files!(project_path)
413
417
  remove_android_module_build_dirs!(project_path)
414
418
  end
415
419
 
@@ -445,18 +449,37 @@ module Pindo
445
449
  puts " ⚠ 物理删除 build 目录时出现异常(将继续尝试 bundle): #{e.message}"
446
450
  end
447
451
 
448
- # macOS clean 遍历删除 build 目录时 Finder 可能写入 .DS_Store,Gradle 会报 Unable to delete / New files were found
449
- private def prune_ds_store_before_gradle_clean!(project_path)
450
- return unless project_path && File.directory?(project_path)
451
-
452
- system('find', project_path, '-name', '.DS_Store', '-type', 'f', '-delete',
453
- out: File::NULL, err: File::NULL)
452
+ # 清理 macOS 自动生成的 .DS_Store 文件
453
+ # 作用:
454
+ # 1) 避免被带入 base 与 asset pack 模块的 assets/ 造成打包冲突
455
+ # 2) 避免 Gradle clean 遍历删除 build 目录时 Finder 回写 .DS_Store 触发
456
+ # Unable to delete / New files were found 的竞态错误
457
+ private def prune_ds_store_files!(project_path)
458
+ return 0 unless project_path && File.directory?(project_path)
459
+
460
+ count = 0
461
+ Find.find(project_path) do |path|
462
+ next unless File.basename(path) == '.DS_Store'
463
+ next unless File.file?(path)
464
+
465
+ begin
466
+ File.delete(path)
467
+ count += 1
468
+ rescue StandardError
469
+ # 个别文件删除失败不影响整体流程
470
+ next
471
+ end
472
+ end
473
+ puts " 已清理 #{count} 个 .DS_Store 文件" if count.positive?
474
+ count
475
+ rescue StandardError => e
476
+ puts " ⚠ 清理 .DS_Store 异常:#{e.message}"
477
+ 0
454
478
  end
455
479
 
456
480
  def build_so_library(project_path)
457
- # 编译so库 (Thread Safe)
458
- cmd = "cd \"#{project_path}\" && ./gradlew unityLibrary:BuildIl2CppTask"
459
- system(cmd)
481
+ # 编译so库 (Thread Safe);数组参数 + chdir,避免 project_path 拼接进 shell
482
+ system("./gradlew", "unityLibrary:BuildIl2CppTask", chdir: project_path)
460
483
  end
461
484
 
462
485
  def copy_so_files(source_path, target_path)
@@ -518,18 +518,16 @@ module Pindo
518
518
  return false
519
519
  end
520
520
 
521
- # 3. 使用unzip -t测试ZIP文件完整性
522
- test_cmd = "unzip -t '#{zip_file}' > /dev/null 2>&1"
523
- system(test_cmd)
521
+ # 3. 使用unzip -t测试ZIP文件完整性(数组参数,避免路径注入)
522
+ system("unzip", "-t", zip_file, out: File::NULL, err: File::NULL)
524
523
  rescue => e
525
524
  false
526
525
  end
527
526
 
528
527
  # 使用curl下载文件
529
528
  def download_with_curl(url, output_path)
530
- # 简化的curl命令,避免特殊字符问题
531
- cmd = "curl -L -o '#{output_path}' '#{url}'"
532
- system(cmd)
529
+ # 数组参数形式,避免 URL/路径中的 shell 元字符注入
530
+ system("curl", "-L", "-o", output_path, url)
533
531
  rescue => e
534
532
  false
535
533
  end
@@ -537,9 +535,8 @@ module Pindo
537
535
  # 解压Gradle分发包
538
536
  def extract_gradle_distribution(zip_file, extract_dir, gradle_version)
539
537
  begin
540
- # 使用unzip命令解压
541
- extract_cmd = "cd '#{File.dirname(extract_dir)}' && unzip -q '#{zip_file}'"
542
- if system(extract_cmd)
538
+ # 使用unzip命令解压(数组参数 + chdir,避免路径注入)
539
+ if system("unzip", "-q", zip_file, chdir: File.dirname(extract_dir))
543
540
  # 设置执行权限
544
541
  gradle_script = File.join(extract_dir, "bin/gradle")
545
542
  if File.exist?(gradle_script)
@@ -577,11 +574,8 @@ module Pindo
577
574
 
578
575
  # 使用官方Gradle命令生成wrapper
579
576
  def generate_gradle_wrapper_official(project_path, gradle_version)
580
- Dir.chdir(project_path) do
581
- # 使用gradle wrapper命令
582
- cmd = "gradle wrapper"
583
- system(cmd)
584
- end
577
+ # 数组参数 + chdir,避免 shell 解析
578
+ system("gradle", "wrapper", chdir: project_path)
585
579
  rescue => e
586
580
  false
587
581
  end