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
@@ -86,10 +86,10 @@ module Pindo
86
86
  tmp_dir = File.join(ipa_dir, tmp_time)
87
87
  FileUtils.mkdir(tmp_dir) unless File.exist?(tmp_dir)
88
88
 
89
- # 解压 IPA
90
- command = "unzip -q \"#{resign_ipa_full_name}\" -d #{tmp_dir}"
91
- puts command
92
- system command
89
+ # 解压 IPA(数组参数避免注入;失败立即报错,不静默继续)
90
+ unless system("unzip", "-q", resign_ipa_full_name, "-d", tmp_dir)
91
+ raise Informative, "IPA 解压失败: #{resign_ipa_full_name}"
92
+ end
93
93
 
94
94
  payload_path = File.join(tmp_dir, "Payload")
95
95
  if File.exist?(payload_path)
@@ -129,14 +129,13 @@ module Pindo
129
129
  # 修改 Info.plist 中的 Bundle ID
130
130
  modify_ipa_info_plist(modify_content_path: modify_content_path, bundle_id: new_bundle_id)
131
131
 
132
- # 重新打包 IPA
133
- current_dir = Dir.pwd
132
+ # 重新打包 IPA(数组参数 + chdir,避免 shell;* 由 Ruby 展开;失败立即报错)
134
133
  if File.exist?(payload_path)
135
- Dir.chdir(tmp_dir)
136
- command = "zip -qry \"#{resign_ipa_full_name}\" * "
137
- puts command
138
- system command
139
- Dir.chdir(current_dir)
134
+ # 排除点文件,对齐原 shell `*` 语义(避免把 .DS_Store 等打进 IPA 根目录)
135
+ entries = Dir.children(tmp_dir).reject { |e| e.start_with?('.') }
136
+ unless system("zip", "-qry", resign_ipa_full_name, *entries, chdir: tmp_dir)
137
+ raise Informative, "IPA 重新打包失败: #{resign_ipa_full_name}"
138
+ end
140
139
  end
141
140
 
142
141
  # 清理临时目录
@@ -160,24 +159,21 @@ module Pindo
160
159
 
161
160
  # 确保 PlistBuddy 可用
162
161
  unless File.exist?("/usr/local/bin/PlistBuddy")
163
- command = 'ln -s /usr/libexec/PlistBuddy /usr/local/bin/PlistBuddy'
164
- system command
162
+ system("ln", "-s", "/usr/libexec/PlistBuddy", "/usr/local/bin/PlistBuddy")
165
163
  end
166
164
 
167
- # 读取旧的 Bundle ID
168
- old_bundle_id_command = '/usr/local/bin/PlistBuddy -c "Print :CFBundleIdentifier" ' + main_info_plist
169
- puts old_bundle_id_command
165
+ # 读取旧的 Bundle ID(数组参数,避免路径拼接进 shell)
170
166
  old_bundle_id = ""
171
- IO.popen(old_bundle_id_command) { |f| old_bundle_id = f.gets }
172
- old_bundle_id = old_bundle_id.strip
167
+ IO.popen(["/usr/local/bin/PlistBuddy", "-c", "Print :CFBundleIdentifier", main_info_plist]) { |f| old_bundle_id = f.gets }
168
+ old_bundle_id = old_bundle_id.to_s.strip
173
169
  puts "旧 Bundle ID: #{old_bundle_id}"
174
170
 
175
- # 修改主应用的 Bundle ID
171
+ # 修改主应用的 Bundle ID(失败立即报错)
176
172
  new_main_bundle_id = bundle_id
177
173
 
178
- exchange_bundleid_command = '/usr/local/bin/PlistBuddy -c "Set :CFBundleIdentifier ' + new_main_bundle_id + '" ' + main_info_plist
179
- puts exchange_bundleid_command
180
- system exchange_bundleid_command
174
+ unless system("/usr/local/bin/PlistBuddy", "-c", "Set :CFBundleIdentifier #{new_main_bundle_id}", main_info_plist)
175
+ raise Informative, "修改主应用 Bundle ID 失败: #{main_info_plist}"
176
+ end
181
177
 
182
178
  # 如果 Bundle ID 发生变化,更新 URL Schemes
183
179
  if old_bundle_id != new_main_bundle_id
@@ -193,10 +189,8 @@ module Pindo
193
189
  appex_path = File.join(plugin_path, file)
194
190
  tmp_info_plist = File.join(appex_path, "Info.plist")
195
191
 
196
- old_bundle_id_command = '/usr/local/bin/PlistBuddy -c "Print :CFBundleIdentifier" ' + tmp_info_plist
197
- puts old_bundle_id_command
198
192
  old_plugin_bundle_id = ""
199
- IO.popen(old_bundle_id_command) { |f| old_plugin_bundle_id = f.gets }
193
+ IO.popen(["/usr/local/bin/PlistBuddy", "-c", "Print :CFBundleIdentifier", tmp_info_plist]) { |f| old_plugin_bundle_id = f.gets }
200
194
  puts "old_bundle_id = #{old_plugin_bundle_id}"
201
195
 
202
196
  # 根据插件类型修改 Bundle ID
@@ -219,9 +213,9 @@ module Pindo
219
213
  plugin_types.each do |suffix|
220
214
  if old_bundle_id.include?(suffix)
221
215
  new_plugin_bundle_id = new_base_bundle_id + suffix
222
- exchange_bundleid_command = '/usr/local/bin/PlistBuddy -c "Set :CFBundleIdentifier ' + new_plugin_bundle_id + '" ' + plist_path
223
- puts exchange_bundleid_command
224
- system exchange_bundleid_command
216
+ unless system("/usr/local/bin/PlistBuddy", "-c", "Set :CFBundleIdentifier #{new_plugin_bundle_id}", plist_path)
217
+ puts " ⚠ 修改插件 Bundle ID 失败: #{plist_path}"
218
+ end
225
219
  break
226
220
  end
227
221
  end
@@ -322,49 +316,26 @@ module Pindo
322
316
  puts " 旧 Scheme: #{old_scheme} (基于 #{old_bundle_id})"
323
317
  puts " 新 Scheme: #{new_scheme} (基于 #{new_bundle_id})"
324
318
 
325
- # 将 binary plist 转换为 XML 格式
326
- convert_command = "plutil -convert xml1 \"#{plist_path}\" 2>/dev/null"
327
- unless system(convert_command)
319
+ # 将 binary plist 转换为 XML 格式(数组参数,避免 plist_path 拼接进 shell)
320
+ unless system("plutil", "-convert", "xml1", plist_path, err: File::NULL)
328
321
  puts " ✗ 转换 plist 为 XML 失败"
329
322
  return
330
323
  end
331
324
 
332
- # 检查文件中是否存在旧的 scheme
333
- check_command = "grep -q \"#{old_scheme}\" \"#{plist_path}\""
334
- scheme_exists = system(check_command)
335
-
336
- unless scheme_exists
325
+ # Ruby 文件操作做字面量替换,彻底规避 sed/grep 的 shell 注入
326
+ content = File.read(plist_path)
327
+ unless content.include?(old_scheme)
337
328
  puts " ✓ 没有找到需要更新的 URL Scheme"
338
- # 转换回 binary 格式
339
- convert_back_command = "plutil -convert binary1 \"#{plist_path}\" 2>/dev/null"
340
- system(convert_back_command)
329
+ system("plutil", "-convert", "binary1", plist_path, err: File::NULL)
341
330
  return
342
331
  end
343
332
 
344
- # 使用 sed 进行全文替换
345
- # 使用临时文件避免 sed -i 在不同系统上的兼容性问题
346
- temp_file = "#{plist_path}.tmp"
347
- sed_command = "sed 's/#{old_scheme}/#{new_scheme}/g' \"#{plist_path}\" > \"#{temp_file}\" && mv \"#{temp_file}\" \"#{plist_path}\""
333
+ replace_count = content.scan(old_scheme).size
334
+ File.write(plist_path, content.gsub(old_scheme, new_scheme))
348
335
 
349
- if system(sed_command)
350
- # 统计替换次数
351
- count_command = "grep -o \"#{new_scheme}\" \"#{plist_path}\" | wc -l"
352
- replace_count = `#{count_command}`.strip.to_i
353
-
354
- # 转换回 binary 格式
355
- convert_back_command = "plutil -convert binary1 \"#{plist_path}\" 2>/dev/null"
356
- system(convert_back_command)
357
-
358
- puts " ✓ 已替换 #{replace_count} 处 URL Scheme: #{old_scheme} → #{new_scheme}"
359
- else
360
- puts " ✗ 替换 URL Scheme 失败"
361
- # 清理临时文件
362
- FileUtils.rm_f(temp_file) if File.exist?(temp_file)
363
-
364
- # 转换回 binary 格式(即使失败也要转回)
365
- convert_back_command = "plutil -convert binary1 \"#{plist_path}\" 2>/dev/null"
366
- system(convert_back_command)
367
- end
336
+ # 转换回 binary 格式
337
+ system("plutil", "-convert", "binary1", plist_path, err: File::NULL)
338
+ puts " 已替换 #{replace_count} 处 URL Scheme: #{old_scheme} #{new_scheme}"
368
339
  end
369
340
 
370
341
  end
@@ -12,13 +12,13 @@ module Pindo
12
12
  @project_obj = Xcodeproj::Project.open(proj_fullname)
13
13
  end
14
14
 
15
- def get_xcodeproj_icon_path()
16
-
15
+ # 查找工程的 AppIcon.appiconset 目录,找不到返回 nil(不抛异常)
16
+ def find_xcodeproj_icon_path()
17
17
  icon_path = nil
18
18
  select_target = @project_obj.targets.select { |target| target.respond_to?(:product_type) && target.product_type.include?(Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application]) }.first
19
19
 
20
20
  project_dir = @project_obj.project_dir
21
-
21
+
22
22
  if !select_target.nil?
23
23
  file_refs = select_target.resources_build_phase.files_references.select { |file| file.display_name.include?("Assets.xcassets") } || []
24
24
  file_refs.each do |file_ref|
@@ -40,12 +40,33 @@ module Pindo
40
40
  end
41
41
 
42
42
  if icon_path.nil? || icon_path.empty? || !File.exist?(icon_path)
43
- raise Informative, "没有找到Xcode icon 目录"
43
+ return nil
44
44
  end
45
45
 
46
46
  return icon_path
47
47
  end
48
48
 
49
+ # 工程是否包含 AppIcon 目录(macOS 命令行工具等无 icon 工程返回 false)
50
+ def has_app_icon_dir?()
51
+ !find_xcodeproj_icon_path().nil?
52
+ end
53
+
54
+ # 工程是否为命令行工具(com.apple.product-type.tool)
55
+ def command_line_tool?()
56
+ tool_uti = Xcodeproj::Constants::PRODUCT_TYPE_UTI[:command_line_tool]
57
+ @project_obj.targets.any? do |target|
58
+ target.respond_to?(:product_type) && !target.product_type.nil? && target.product_type == tool_uti
59
+ end
60
+ end
61
+
62
+ def get_xcodeproj_icon_path()
63
+ icon_path = find_xcodeproj_icon_path()
64
+ if icon_path.nil?
65
+ raise Informative, "没有找到Xcode icon 目录"
66
+ end
67
+ return icon_path
68
+ end
69
+
49
70
  def install_icon_res(new_icon_dir:nil)
50
71
  icon_path = get_xcodeproj_icon_path()
51
72
  begin
@@ -356,7 +356,7 @@ module Pindo
356
356
  project.save
357
357
 
358
358
  if updated_targets.empty?
359
- Funlog.instance.fancyinfo_error("未找到需要更新的application target")
359
+ Funlog.instance.fancyinfo_warning("未找到需要更新的application target(如命令行工具工程),跳过版本更新")
360
360
  return false
361
361
  else
362
362
  Funlog.instance.fancyinfo_success("iOS版本更新完成!")
@@ -480,6 +480,12 @@ module Pindo
480
480
 
481
481
  puts "\n检测到项目 Icon URL: #{icon_url}"
482
482
 
483
+ # 命令行工具且无 AppIcon 目录时跳过 Icon 替换,视为成功
484
+ if XcodeResHelper.skip_icon_replacement?(proj_dir: project_dir)
485
+ Funlog.instance.fancyinfo_success("命令行工具工程无 AppIcon 目录,跳过 Icon 替换")
486
+ return true
487
+ end
488
+
483
489
  # 创建临时目录(所有 icon 处理都在此目录内完成)
484
490
  temp_icon_dir = File.join(project_dir, ".pindo_temp_ios_icon")
485
491
  icon_download_path = nil
@@ -699,8 +699,15 @@ module Pindo
699
699
  project_build_platform = project_obj.root_object.build_configuration_list.get_setting("SDKROOT")["Release"]
700
700
  is_macos = !project_build_platform.nil? && project_build_platform.eql?("macosx")
701
701
 
702
- # 根据平台查找输出文件
703
- if is_macos
702
+ # 命令行工具(com.apple.product-type.tool)产物为裸可执行文件,而非 .app/.ipa
703
+ tool_uti = Xcodeproj::Constants::PRODUCT_TYPE_UTI[:command_line_tool]
704
+ tool_target = project_obj.targets.find { |t| t.respond_to?(:product_type) && t.product_type == tool_uti }
705
+
706
+ # 根据工程类型查找输出文件
707
+ if tool_target
708
+ output_file = find_command_line_tool_output(build_dir: build_dir, target: tool_target)
709
+ puts "命令行工具: 查找可执行文件" if output_file
710
+ elsif is_macos
704
711
  # macOS 平台查找 .app 文件
705
712
  build_path = File.join(build_dir, "*.app")
706
713
  output_file = Dir.glob(build_path).select { |f| File.directory?(f) }.max_by { |f| File.mtime(f) }
@@ -715,6 +722,24 @@ module Pindo
715
722
  output_file
716
723
  end
717
724
 
725
+ # 查找命令行工具的输出可执行文件
726
+ # @param build_dir [String] 构建输出目录
727
+ # @param target [Xcodeproj::Project::Object::PBXNativeTarget] 命令行工具 target
728
+ # @return [String, nil] 可执行文件路径
729
+ def find_command_line_tool_output(build_dir:, target:)
730
+ product_name = target.build_configurations.first.build_settings['PRODUCT_NAME']
731
+ product_name = target.name if product_name.nil? || product_name.to_s.include?('$(')
732
+
733
+ # 优先取 gym 导出到 build 根目录的同名可执行文件
734
+ candidate = File.join(build_dir, product_name)
735
+ return candidate if File.file?(candidate)
736
+
737
+ # 回退:build 根目录下任意无扩展名的普通文件(排除 .dSYM 等目录)
738
+ Dir.glob(File.join(build_dir, "*")).find do |f|
739
+ File.file?(f) && File.extname(f).empty?
740
+ end
741
+ end
742
+
718
743
  # 获取 Gym 构建参数
719
744
  # @param project_fullname [String] 工程文件完整路径
720
745
  # @param icloud_id [String, nil] iCloud ID(可选)
@@ -735,8 +760,15 @@ module Pindo
735
760
  project_obj = Xcodeproj::Project.open(project_fullname)
736
761
 
737
762
  project_build_platform = project_obj.root_object.build_configuration_list.get_setting("SDKROOT")["Release"]
738
- main_target = project_obj.targets.select { |target| target.respond_to?(:product_type) && target.product_type.include?(Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application]) }.first
739
- provisioning_profile_name = main_target.build_configurations.first.build_settings['PROVISIONING_PROFILE_SPECIFIER'].downcase
763
+ main_target = project_obj.targets.select { |target| target.respond_to?(:product_type) && !target.product_type.nil? && target.product_type.include?(Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application]) }.first
764
+ # 命令行工具等无 application target 的工程,回退到第一个可用 native target
765
+ main_target ||= project_obj.targets.find { |target| target.respond_to?(:product_type) && !target.product_type.nil? }
766
+ if main_target.nil?
767
+ raise Informative, "未找到可编译的 target"
768
+ end
769
+ # 命令行工具通常无 provisioning profile,做 nil 保护
770
+ profile_specifier = main_target.build_configurations.first.build_settings['PROVISIONING_PROFILE_SPECIFIER']
771
+ provisioning_profile_name = profile_specifier.nil? ? "" : profile_specifier.downcase
740
772
 
741
773
  # 确定构建类型和 iCloud 环境
742
774
  build_type = "app-store"
@@ -114,6 +114,14 @@ module Pindo
114
114
  end
115
115
  end
116
116
 
117
+ # 是否应跳过 Icon 替换:工程既无 AppIcon 目录、又是命令行工具时才跳过
118
+ def skip_icon_replacement?(proj_dir:nil)
119
+ xcodeproj_file_name = Dir.glob(File.join(proj_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
120
+ return false if xcodeproj_file_name.nil?
121
+ xcodereshandler = XcodeResHandler.new(proj_fullname: xcodeproj_file_name)
122
+ !xcodereshandler.has_app_icon_dir? && xcodereshandler.command_line_tool?
123
+ end
124
+
117
125
  def install_icon(proj_dir:nil, new_icon_dir:nil)
118
126
  xcodeproj_file_name = Dir.glob(File.join(proj_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
119
127
  xcodereshandler = XcodeResHandler.new(proj_fullname: xcodeproj_file_name)
@@ -95,17 +95,26 @@ module Pindo
95
95
  end
96
96
 
97
97
  # 加载 all_bundleid_group 映射表
98
- # @return [Hash] group_name => bundle_id 的映射,如 {"fancyapptest" => "com.heroneverdie101.fancyapptest"}
98
+ # @return [Hash, nil] group_name => bundle_id 的映射,如 {"fancyapptest" => "com.heroneverdie101.fancyapptest"};加载失败返回 nil
99
99
  def self.load_bundleid_group
100
100
  config_file = File.join(pindo_env_configdir, 'bundleid_config.json')
101
- return {} unless File.exist?(config_file)
101
+ unless File.exist?(config_file)
102
+ puts "加载 bundleid_config.json 失败: 文件不存在 #{config_file},请运行 pindo setup 更新配置"
103
+ return nil
104
+ end
102
105
 
103
106
  begin
104
107
  config = JSON.parse(File.read(config_file))
105
- config['all_bundleid_group'] || {}
108
+ group = config['all_bundleid_group']
109
+ unless group.is_a?(Hash) && !group.empty?
110
+ puts "加载 bundleid_config.json 失败: all_bundleid_group 缺失或为空 #{config_file},请运行 pindo setup 更新配置"
111
+ return nil
112
+ end
113
+
114
+ group
106
115
  rescue => e
107
- puts "加载 bundleid_config.json 失败: #{e.message}"
108
- {}
116
+ puts "加载 bundleid_config.json 失败: #{e.message},请运行 pindo setup 更新配置"
117
+ nil
109
118
  end
110
119
  end
111
120
 
@@ -116,7 +125,15 @@ module Pindo
116
125
  return nil if group_name.nil? || group_name.empty?
117
126
 
118
127
  group = load_bundleid_group
119
- group[group_name]
128
+ return nil unless group
129
+
130
+ bundle_id = group[group_name]
131
+ if bundle_id.nil? || bundle_id.empty?
132
+ puts "解析 Bundle ID 失败: all_bundleid_group 中未找到 #{group_name},请运行 pindo setup 更新配置"
133
+ return nil
134
+ end
135
+
136
+ bundle_id
120
137
  end
121
138
 
122
139
  # 获取 pindo 环境配置目录
data/lib/pindo/version.rb CHANGED
@@ -6,7 +6,7 @@ require 'time'
6
6
 
7
7
  module Pindo
8
8
 
9
- VERSION = "5.18.17"
9
+ VERSION = "5.19.1"
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.18.17
4
+ version: 5.19.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - wade
@@ -526,7 +526,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
526
526
  - !ruby/object:Gem::Version
527
527
  version: 3.0.0
528
528
  requirements: []
529
- rubygems_version: 3.6.9
529
+ rubygems_version: 4.0.11
530
530
  specification_version: 4
531
531
  summary: easy work
532
532
  test_files: []