pindo 5.13.7 → 5.13.10

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/git_handler.rb +247 -42
  3. data/lib/pindo/command/android/autobuild.rb +104 -31
  4. data/lib/pindo/command/android/autoresign.rb +23 -322
  5. data/lib/pindo/command/android/keystore.rb +7 -130
  6. data/lib/pindo/command/appstore/adhocbuild.rb +52 -15
  7. data/lib/pindo/command/appstore/autobuild.rb +104 -8
  8. data/lib/pindo/command/appstore/autoresign.rb +3 -5
  9. data/lib/pindo/command/ios/autobuild.rb +96 -32
  10. data/lib/pindo/command/ios/build.rb +8 -186
  11. data/lib/pindo/command/jps/media.rb +146 -0
  12. data/lib/pindo/command/jps/upload.rb +49 -21
  13. data/lib/pindo/command/jps.rb +1 -0
  14. data/lib/pindo/command/unity/autobuild.rb +141 -32
  15. data/lib/pindo/command/unity/packpush.rb +5 -8
  16. data/lib/pindo/command/utils/repoinit.rb +0 -2
  17. data/lib/pindo/command/utils/tag.rb +58 -26
  18. data/lib/pindo/command/utils.rb +0 -1
  19. data/lib/pindo/command/web/autobuild.rb +98 -34
  20. data/lib/pindo/command.rb +0 -56
  21. data/lib/pindo/config/build_info_manager.rb +7 -8
  22. data/lib/pindo/module/android/android_config_helper.rb +2 -11
  23. data/lib/pindo/module/appselect.rb +15 -41
  24. data/lib/pindo/module/appstore/itcapp_helper.rb +3 -6
  25. data/lib/pindo/module/build/build_helper.rb +28 -18
  26. data/lib/pindo/module/build/git_repo_helper.rb +284 -405
  27. data/lib/pindo/module/cert/pem_helper.rb +3 -6
  28. data/lib/pindo/module/pgyer/pgyerhelper.rb +193 -25
  29. data/lib/pindo/module/task/model/appstore/appstore_task.rb +5 -0
  30. data/lib/pindo/module/task/model/build/android_build_adhoc_task.rb +13 -187
  31. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +36 -34
  32. data/lib/pindo/module/task/model/build/android_build_gplay_task.rb +13 -187
  33. data/lib/pindo/module/task/model/build/ios_build_adhoc_task.rb +9 -6
  34. data/lib/pindo/module/task/model/build/ios_build_appstore_task.rb +9 -6
  35. data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +37 -32
  36. data/lib/pindo/module/task/model/build/web_build_dev_task.rb +7 -5
  37. data/lib/pindo/module/task/model/build_task.rb +8 -11
  38. data/lib/pindo/module/task/model/git/git_commit_task.rb +118 -0
  39. data/lib/pindo/module/task/model/git/git_tag_task.rb +125 -0
  40. data/lib/pindo/module/task/model/git_task.rb +75 -0
  41. data/lib/pindo/module/task/model/jps/jps_message_task.rb +178 -0
  42. data/lib/pindo/module/task/model/{jps_resign_task.rb → jps/jps_resign_task.rb} +14 -23
  43. data/lib/pindo/module/task/model/jps/jps_upload_media_task.rb +248 -0
  44. data/lib/pindo/module/task/model/{jps_upload_task.rb → jps/jps_upload_task.rb} +39 -94
  45. data/lib/pindo/module/task/model/jps_task.rb +43 -0
  46. data/lib/pindo/module/task/model/{ipa_local_resign_task.rb → resign/ipa_local_resign_task.rb} +7 -2
  47. data/lib/pindo/module/task/model/unity/unity_config_task.rb +103 -0
  48. data/lib/pindo/module/task/model/{unity_export_task.rb → unity/unity_export_task.rb} +76 -78
  49. data/lib/pindo/module/task/model/unity/unity_update_task.rb +95 -0
  50. data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +156 -0
  51. data/lib/pindo/module/task/model/unity_task.rb +118 -0
  52. data/lib/pindo/module/task/pindo_task.rb +101 -1
  53. data/lib/pindo/module/task/task_manager.rb +29 -24
  54. data/lib/pindo/module/unity/nuget_helper.rb +7 -7
  55. data/lib/pindo/module/unity/unity_command_helper.rb +188 -0
  56. data/lib/pindo/module/unity/unity_env_helper.rb +208 -0
  57. data/lib/pindo/module/unity/unity_helper.rb +189 -746
  58. data/lib/pindo/module/unity/unity_proc_helper.rb +390 -0
  59. data/lib/pindo/options/core/global_options_state.rb +96 -26
  60. data/lib/pindo/options/core/option_configuration.rb +3 -0
  61. data/lib/pindo/options/core/option_item.rb +36 -0
  62. data/lib/pindo/options/groups/build_options.rb +23 -6
  63. data/lib/pindo/options/groups/git_options.rb +115 -0
  64. data/lib/pindo/options/groups/jps_options.rb +7 -0
  65. data/lib/pindo/options/groups/option_group.rb +15 -0
  66. data/lib/pindo/options/groups/unity_options.rb +49 -0
  67. data/lib/pindo/options/options.rb +2 -0
  68. data/lib/pindo/version.rb +2 -2
  69. metadata +25 -14
  70. data/lib/pindo/base/githelper.rb +0 -686
  71. data/lib/pindo/base/pindocontext.rb +0 -602
  72. data/lib/pindo/command/utils/feishu.rb +0 -134
  73. data/lib/pindo/module/build/version_helper.rb +0 -146
  74. data/lib/pindo/module/task/model/git_tag_task.rb +0 -80
@@ -32,20 +32,23 @@ module Pindo
32
32
  # PindoTask 基类(简化版,所有任务在主线程中执行)
33
33
  class PindoTask
34
34
  attr_accessor :id, :name, :status, :error, :result
35
- attr_reader :type, :priority, :dependencies
35
+ attr_reader :type, :task_key, :priority, :dependencies, :data_dependencies
36
36
  attr_accessor :context, :metadata
37
37
  attr_reader :created_at, :started_at, :finished_at
38
38
  attr_accessor :retry_count # 剩余重试次数
39
39
  attr_reader :retry_mode, :retry_delay, :max_retry_count # max_retry_count: 初始最大重试次数
40
40
  attr_accessor :callbacks_setup # 标记回调是否已经设置
41
+ attr_accessor :task_manager # TaskManager 实例(依赖注入)
41
42
 
42
43
  def initialize(name, options = {})
43
44
  @id = SecureRandom.uuid
44
45
  @name = name
45
46
  @type = self.class.task_type
47
+ @task_key = self.class.task_key
46
48
  @priority = options[:priority] || TaskPriority::HIGH
47
49
  @status = TaskStatus::PENDING
48
50
  @dependencies = options[:dependencies] || []
51
+ @data_dependencies = options[:data_dependencies] || []
49
52
  @context = options[:context] || {}
50
53
  @metadata = options[:metadata] || {}
51
54
  @error = nil
@@ -73,6 +76,10 @@ module Pindo
73
76
  raise NotImplementedError, "Subclass must define task_type"
74
77
  end
75
78
 
79
+ def self.task_key
80
+ raise NotImplementedError, "Subclass must define task_key"
81
+ end
82
+
76
83
  # 默认重试配置
77
84
  def self.default_retry_mode
78
85
  RetryMode::IMMEDIATE
@@ -135,6 +142,93 @@ module Pindo
135
142
  @status == TaskStatus::CANCELLED
136
143
  end
137
144
 
145
+ # ========== 依赖任务数据获取 ==========
146
+
147
+ # 获取指定依赖任务
148
+ # @param dep_task_id [String] 依赖任务的 ID
149
+ # @return [PindoTask, nil] 依赖任务对象,如果不存在返回 nil
150
+ def get_dependency_task(dep_task_id)
151
+ return nil unless @task_manager
152
+ @task_manager.find_task(dep_task_id)
153
+ end
154
+
155
+ # 获取指定依赖任务的结果
156
+ # @param dep_task_id [String] 依赖任务的 ID
157
+ # @return [Hash, nil] 依赖任务的 result,如果任务不存在或未完成返回 nil
158
+ def get_dependency_result(dep_task_id)
159
+ dep_task = get_dependency_task(dep_task_id)
160
+ return nil unless dep_task
161
+ return nil unless dep_task.finished?
162
+ dep_task.result
163
+ end
164
+
165
+ # 获取所有依赖任务的结果(按依赖顺序)
166
+ # @return [Hash] key 为任务 ID,value 为任务结果
167
+ def get_all_dependencies_results
168
+ results = {}
169
+ @dependencies.each do |dep_id|
170
+ results[dep_id] = get_dependency_result(dep_id)
171
+ end
172
+ results
173
+ end
174
+
175
+ # ========== 任务数据参数传递 ==========
176
+
177
+ # 获取任务的数据参数(用于传递给其他任务)
178
+ # @return [Hash] 包含任务标识和参数的哈希
179
+ def data_param
180
+ {
181
+ task_id: @id,
182
+ task_type: @type,
183
+ task_key: @task_key,
184
+ task_name: @name,
185
+ task_param: build_task_param
186
+ }
187
+ end
188
+
189
+ # 获取指定数据依赖任务的数据参数
190
+ # @param task_id [String] 任务 ID
191
+ # @return [Hash, nil] 任务的数据参数,如果任务不存在或未完成返回 nil
192
+ def get_data_param(task_id)
193
+ dep_task = get_dependency_task(task_id)
194
+ return nil unless dep_task
195
+ return nil unless dep_task.finished? && dep_task.status == TaskStatus::SUCCESS
196
+ dep_task.data_param
197
+ end
198
+
199
+ # 获取所有数据依赖任务的数据参数
200
+ # @return [Array<Hash>] 数据参数数组
201
+ def get_all_data_params
202
+ @data_dependencies.map { |task_id| get_data_param(task_id) }.compact
203
+ end
204
+
205
+ # 根据 task_key 获取数据依赖任务的数据参数
206
+ # @param task_key [Symbol] 任务键
207
+ # @return [Hash, nil] 第一个匹配的任务数据参数
208
+ def get_data_param_by_key(task_key)
209
+ @data_dependencies.each do |task_id|
210
+ param = get_data_param(task_id)
211
+ return param if param && param[:task_key] == task_key
212
+ end
213
+ nil
214
+ end
215
+
216
+ # 获取所有指定 task_key 的数据参数
217
+ # @param task_key [Symbol] 任务键
218
+ # @return [Array<Hash>] 匹配的任务数据参数数组
219
+ def get_all_data_params_by_key(task_key)
220
+ get_all_data_params.select { |param| param[:task_key] == task_key }
221
+ end
222
+
223
+ # 获取主数据参数(第一个数据依赖任务的参数)
224
+ # @return [Hash, nil] 主数据参数
225
+ def primary_data_param
226
+ return nil if @data_dependencies.empty?
227
+ get_data_param(@data_dependencies.first)
228
+ end
229
+
230
+ # ========== 回调方法 ==========
231
+
138
232
  # 添加回调
139
233
  def on(event, &block)
140
234
  @callbacks[event] << block if @callbacks[event]
@@ -167,6 +261,12 @@ module Pindo
167
261
  raise NotImplementedError, "Subclass must implement do_work method"
168
262
  end
169
263
 
264
+ # 构建任务参数(子类重写以提供自定义参数)
265
+ # @return [Hash] 任务参数
266
+ def build_task_param
267
+ {} # 默认返回空哈希
268
+ end
269
+
170
270
  private
171
271
 
172
272
  # 内部执行逻辑
@@ -108,6 +108,15 @@ module Pindo
108
108
  task&.cancel
109
109
  end
110
110
 
111
+ # 查找任务(公共方法,供 PindoTask 获取依赖任务使用)
112
+ # @param task_id [String] 任务 ID
113
+ # @return [PindoTask, nil] 任务对象
114
+ def find_task(task_id)
115
+ all_tasks = @pending_queue + @completed_tasks
116
+ all_tasks << @current_task if @current_task
117
+ all_tasks.find { |t| t.id == task_id }
118
+ end
119
+
111
120
  private
112
121
 
113
122
  # 获取下一个可执行的任务
@@ -168,6 +177,9 @@ module Pindo
168
177
  def execute_task(task)
169
178
  @current_task = task
170
179
 
180
+ # 注入 TaskManager 实例(依赖注入)
181
+ task.task_manager = self
182
+
171
183
  # 设置任务进度回调
172
184
  setup_task_callbacks(task)
173
185
 
@@ -224,13 +236,6 @@ module Pindo
224
236
  Funlog.warning("任务 #{task.name} 因#{reason}而被取消")
225
237
  end
226
238
 
227
- # 查找任务
228
- def find_task(task_id)
229
- all_tasks = @pending_queue + @completed_tasks
230
- all_tasks << @current_task if @current_task
231
- all_tasks.find { |t| t.id == task_id }
232
- end
233
-
234
239
  # 输出任务执行计划
235
240
  def print_execution_plan
236
241
  # 按类型分组统计
@@ -242,29 +247,21 @@ module Pindo
242
247
  puts "=" * 60
243
248
 
244
249
  tasks_by_type.each do |type, tasks|
245
- type_name = get_type_display_name(type)
246
- puts " #{type_name}: #{tasks.count} 个任务"
250
+ # 直接从任务类获取显示名称
251
+ type_name = tasks.first&.class&.task_type_name || type.to_s.capitalize
252
+ puts "\n #{type_name}: #{tasks.count} 个任务"
253
+
254
+ # 显示该类型下的每个任务名称
255
+ tasks.each do |task|
256
+ puts " - #{task.name}"
257
+ end
247
258
  end
248
259
 
249
- puts " 总计: #{@pending_queue.count} 个任务"
260
+ puts "\n 总计: #{@pending_queue.count} 个任务"
250
261
  puts "=" * 60 + "\e[0m"
251
262
  puts "\n"
252
263
  end
253
264
 
254
- # 获取任务类型的显示名称
255
- def get_type_display_name(type)
256
- case type
257
- when :unity_export
258
- "Unity 导出"
259
- when :build
260
- "编译构建"
261
- when :upload
262
- "上传发布"
263
- else
264
- type.to_s.capitalize
265
- end
266
- end
267
-
268
265
  # 输出任务执行头部
269
266
  def print_task_header(task)
270
267
  puts "\n"
@@ -285,6 +282,14 @@ module Pindo
285
282
  puts "\n"
286
283
  Funlog.error("任务失败: #{task.name}")
287
284
  Funlog.error("错误信息: #{error.message}")
285
+
286
+ # 打印堆栈信息用于调试(仅在 PINDO_DEBUG 模式下)
287
+ if ENV['PINDO_DEBUG'] && error.backtrace && !error.backtrace.empty?
288
+ puts "\n堆栈信息:"
289
+ error.backtrace.first(10).each do |line|
290
+ puts " #{line}"
291
+ end
292
+ end
288
293
  end
289
294
 
290
295
  # 输出任务底部分隔线
@@ -2,11 +2,11 @@ require 'fileutils'
2
2
  require 'json'
3
3
  require 'nokogiri'
4
4
  require 'open3'
5
+ require 'pindo/base/git_handler'
5
6
 
6
7
  module Pindo
7
8
  module Unity
8
9
  class NugetHelper
9
- extend Pindo::Githelper
10
10
 
11
11
  # ============================================
12
12
  # ID 格式转换
@@ -535,11 +535,11 @@ module Pindo
535
535
  default_message = "feat: 更新版本到 #{nuspec_version}"
536
536
 
537
537
  # 如果不是 Git 仓库,返回默认消息
538
- unless is_git_directory?(local_repo_dir: package_dir)
538
+ unless Pindo::GitHandler.is_git_directory?(local_repo_dir: package_dir)
539
539
  return default_message
540
540
  end
541
541
 
542
- git_root = git_root_directory(local_repo_dir: package_dir)
542
+ git_root = Pindo::GitHandler.git_root_directory(local_repo_dir: package_dir)
543
543
  unless git_root
544
544
  return default_message
545
545
  end
@@ -551,7 +551,7 @@ module Pindo
551
551
  Dir.chdir(git_root)
552
552
 
553
553
  # 获取所有 tags
554
- all_tags = git!(%W(-C #{git_root} tag -l)).split("\n")
554
+ all_tags = Pindo::GitHandler.git!(%W(-C #{git_root} tag -l)).split("\n")
555
555
 
556
556
  # 查找匹配当前版本的 tag(不区分大小写,支持 v 前缀)
557
557
  matching_tag = all_tags.find do |tag|
@@ -570,7 +570,7 @@ module Pindo
570
570
  Dir.chdir(git_root)
571
571
 
572
572
  # 获取所有 tags 并按版本号排序
573
- all_tags = git!(%W(-C #{git_root} tag -l)).split("\n")
573
+ all_tags = Pindo::GitHandler.git!(%W(-C #{git_root} tag -l)).split("\n")
574
574
  sorted_tags = all_tags.sort_by do |tag|
575
575
  version_str = tag.gsub(/^(v|V|release[\s_-]*)/i, '')
576
576
  version_str.split('.').map(&:to_i)
@@ -598,7 +598,7 @@ module Pindo
598
598
  # 获取最新 tag
599
599
  latest_tag = nil
600
600
  ["v", "V", "release", ""].each do |prefix|
601
- latest_tag = get_latest_version_tag(project_dir: git_root, tag_prefix: prefix)
601
+ latest_tag = Pindo::GitHandler.get_latest_version_tag(project_dir: git_root, tag_prefix: prefix)
602
602
  break if latest_tag
603
603
  end
604
604
 
@@ -615,7 +615,7 @@ module Pindo
615
615
 
616
616
  # 使用特殊分隔符来区分不同的 commits
617
617
  separator = "---COMMIT-SEPARATOR---"
618
- commits_raw = git!(%W(-C #{git_root} log #{git_range} --pretty=format:%B#{separator}))
618
+ commits_raw = Pindo::GitHandler.git!(%W(-C #{git_root} log #{git_range} --pretty=format:%B#{separator}))
619
619
 
620
620
  # 按分隔符拆分成单个 commits
621
621
  commits = commits_raw.split(separator).map(&:strip).reject(&:empty?)
@@ -0,0 +1,188 @@
1
+ require 'open3'
2
+ require 'pindo/base/funlog'
3
+ require 'pindo/config/pindoconfig'
4
+ require 'pindo/module/unity/unity_env_helper'
5
+ require 'pindo/module/unity/unity_proc_helper'
6
+
7
+
8
+ module Pindo
9
+ module Unity
10
+ # Unity 命令执行助手
11
+ # 负责执行 Unity 命令行操作
12
+ # 所有方法均为类方法,无需实例化
13
+ class UnityCommandHelper
14
+
15
+ # 执行 Unity 命令
16
+ # @param unity_exe_full_path [String] Unity 可执行文件路径
17
+ # @param project_path [String] Unity 项目路径
18
+ # @param method [String] Unity 方法名(默认: GoodUnityBuild.BuildManager.BatchBuild)
19
+ # @param additional_args [Hash] 额外参数
20
+ # @return [Hash] 执行结果 { success:, stdout:, stderr:, exit_status:, unity_version: }
21
+ def self.execute_unity_command(unity_exe_full_path, project_path, method: 'GoodUnityBuild.BuildManager.BatchBuild', additional_args: {})
22
+ if unity_exe_full_path.nil?
23
+ raise Informative, "Unity path not found!"
24
+ end
25
+
26
+ # 调试级别:通过环境变量或参数控制
27
+ debug_level = ENV['UNITY_BUILD_DEBUG'] || additional_args[:debug_level] || 'normal'
28
+ # debug_level: 'quiet' - 只显示错误
29
+ # 'normal' - 显示错误、警告和关键进度
30
+ # 'verbose' - 显示所有输出
31
+
32
+ # 检查项目路径是否存在
33
+ unless File.directory?(project_path)
34
+ raise Informative, "Unity项目路径不存在: #{project_path}"
35
+ end
36
+
37
+ # 检查项目是否是Unity项目
38
+ project_settings = File.join(project_path, "ProjectSettings")
39
+ unless File.directory?(project_settings)
40
+ raise Informative, "项目路径不是Unity项目: #{project_path}"
41
+ end
42
+
43
+ cmd_args = [
44
+ unity_exe_full_path,
45
+ "-batchmode",
46
+ "-quit",
47
+ "-projectPath",
48
+ project_path.to_s,
49
+ "-executeMethod",
50
+ method
51
+ ]
52
+
53
+ # Add any additional arguments
54
+ additional_args.each do |key, value|
55
+ cmd_args << "-#{key}"
56
+ cmd_args << value.to_s if value
57
+ end
58
+
59
+ puts "Unity command: #{cmd_args.join(' ')}"
60
+ puts ""
61
+
62
+ # 使用更智能的进度检测机制
63
+ progress_thread = nil
64
+ start_time = Time.now
65
+ last_output_time = Time.now
66
+
67
+ begin
68
+ # 使用 Open3.popen3 来实时监控输出
69
+ Open3.popen3(*cmd_args) do |stdin, stdout, stderr, wait_thr|
70
+ stdin.close
71
+
72
+ # 启动进度显示线程
73
+ progress_thread = Thread.new do
74
+ dots = 0
75
+ while wait_thr.alive?
76
+ sleep(2)
77
+ dots = (dots + 1) % 4
78
+ elapsed = (Time.now - start_time).to_i
79
+ print "\r\e[33mUnity 构建中#{'.' * dots}#{' ' * (3 - dots)} (已用时: #{elapsed}秒)\e[0m"
80
+ $stdout.flush
81
+ end
82
+ end
83
+
84
+ # 实时读取输出
85
+ stdout_buffer = ""
86
+ stderr_buffer = ""
87
+
88
+ # 定义错误关键词模式(优化正则,避免重复)
89
+ error_pattern = /error|exception|failed|Build completed with a result of 'Failed'/i
90
+ warning_pattern = /warning|warn/i
91
+ success_pattern = /Build completed successfully|Exiting batchmode successfully/i
92
+
93
+ # 使用非阻塞方式读取输出
94
+ while wait_thr.alive?
95
+ # 检查是否有输出可读
96
+ ready_streams = IO.select([stdout, stderr], nil, nil, 1)
97
+
98
+ if ready_streams
99
+ ready_streams[0].each do |stream|
100
+ if line = stream.gets
101
+ # 记录输出
102
+ if stream == stdout
103
+ stdout_buffer += line
104
+ else
105
+ stderr_buffer += line
106
+ end
107
+ last_output_time = Time.now
108
+
109
+ # 根据调试级别和内容类型显示输出
110
+ case debug_level
111
+ when 'verbose'
112
+ # 详细模式:显示所有输出
113
+ puts line
114
+ when 'quiet'
115
+ # 安静模式:只显示错误
116
+ if line.match?(error_pattern)
117
+ puts "\e[31m[错误] #{line.strip}\e[0m"
118
+ end
119
+ else # 'normal'
120
+ # 正常模式:显示错误、警告和关键进度
121
+ if line.match?(error_pattern)
122
+ puts "\n\e[31m[错误] #{line.strip}\e[0m"
123
+ elsif line.match?(warning_pattern)
124
+ puts "\n\e[33m[警告] #{line.strip}\e[0m"
125
+ elsif line.match?(success_pattern)
126
+ puts "\n\e[32m[成功] #{line.strip}\e[0m"
127
+ elsif line.match?(/\d+%/) || line.match?(/Building|Compiling|Processing/i)
128
+ # 显示进度相关信息
129
+ print "\r\e[36m[进度] #{line.strip}\e[0m"
130
+ $stdout.flush
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ end
138
+
139
+ # 读取剩余输出
140
+ stdout_buffer += stdout.read
141
+ stderr_buffer += stderr.read
142
+
143
+ # 停止进度显示线程
144
+ progress_thread.kill if progress_thread
145
+
146
+ # 检查构建是否真的成功
147
+ build_success = wait_thr.value.success?
148
+
149
+ # 检查是否有构建错误的关键词
150
+ if stdout_buffer.match?(/Build completed with a result of 'Failed'|Build completed with a result of 'Cancelled'|BuildPlayerWindow\+BuildMethod\+Invoke|error|Error|ERROR|exception|Exception|EXCEPTION|failed|Failed|FAILED/)
151
+ build_success = false
152
+ # puts "\n\e[31m检测到构建错误信息,构建可能失败\e[0m"
153
+ end
154
+
155
+ if build_success
156
+ print "\r\e[32mUnity 构建完成!\e[0m\n"
157
+ # 构建完成后检查并清理可能的 Unity 进程残留
158
+ UnityProcHelper.cleanup_unity_processes_after_build(unity_exe_full_path: unity_exe_full_path, project_path: project_path)
159
+ else
160
+ print "\r\e[31mUnity 构建失败!\e[0m\n"
161
+ puts "\e[31m构建输出:\e[0m"
162
+ puts stdout_buffer if !stdout_buffer.empty?
163
+ puts "\e[31m错误输出:\e[0m"
164
+ puts stderr_buffer if !stderr_buffer.empty?
165
+ # 构建失败时也清理可能的进程残留
166
+ UnityProcHelper.cleanup_unity_processes_after_build(unity_exe_full_path: unity_exe_full_path, project_path: project_path)
167
+ end
168
+
169
+ # 返回结果
170
+ {
171
+ success: build_success,
172
+ stdout: stdout_buffer,
173
+ stderr: stderr_buffer,
174
+ exit_status: wait_thr.value.exitstatus,
175
+ unity_version: UnityEnvHelper.extract_version_from_path(unity_exe_full_path)
176
+ }
177
+ end
178
+ rescue => e
179
+ # 停止进度显示线程
180
+ progress_thread.kill if progress_thread
181
+ print "\r\e[31mUnity 构建失败!\e[0m\n"
182
+ raise e
183
+ end
184
+ end
185
+
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,208 @@
1
+ module Pindo
2
+ module Unity
3
+ # Unity 环境管理助手
4
+ # 负责 Unity 版本查找、路径管理等环境相关功能
5
+ # 所有方法均为类方法,无需实例化
6
+ class UnityEnvHelper
7
+ # macOS Unity 安装路径
8
+ UNITY_MAC_PATHS = [
9
+ "/Applications/Unity/Unity.app/Contents/MacOS/Unity",
10
+ "/Applications/Unity/Hub/Editor/*/Unity.app/Contents/MacOS/Unity",
11
+ "/Applications/Unity/*/Unity.app/Contents/MacOS/Unity"
12
+ ]
13
+
14
+ # Windows Unity 安装路径
15
+ UNITY_WINDOWS_PATHS = [
16
+ "C:/Program Files/Unity/Editor/Unity.exe",
17
+ "C:/Program Files/Unity/Hub/Editor/*/Unity.exe",
18
+ "C:/Program Files/Unity/*/Editor/Unity.exe"
19
+ ]
20
+
21
+ # 查找 Unity 路径
22
+ # @param project_unity_version [String] 项目所需的 Unity 版本
23
+ # @param force_change_version [Boolean] 是否强制使用最新版本
24
+ # @return [String] Unity 可执行文件的完整路径
25
+ def self.find_unity_path(project_unity_version: nil, force_change_version: false)
26
+ if project_unity_version.nil? || project_unity_version.empty?
27
+ raise "Project Unity version is nil or empty"
28
+ end
29
+
30
+ unity_major_version = project_unity_version.split('.')[0..1].join('.')
31
+ paths = case RUBY_PLATFORM
32
+ when /darwin/
33
+ UNITY_MAC_PATHS
34
+ when /mswin|mingw|windows/
35
+ UNITY_WINDOWS_PATHS
36
+ else
37
+ raise "Unsupported platform: #{RUBY_PLATFORM}"
38
+ end
39
+
40
+ unity_versions = []
41
+
42
+ paths.each do |path|
43
+ if path.include?("*")
44
+ Dir.glob(path).each do |expanded_path|
45
+ version = extract_version_from_path(expanded_path)
46
+ if version
47
+ major_version = version.split('.')[0..1].join('.')
48
+ unity_versions << {
49
+ path: expanded_path,
50
+ version: version,
51
+ major_version: major_version
52
+ }
53
+ end
54
+ end
55
+ elsif File.exist?(path)
56
+ version = extract_version_from_path(path)
57
+ if version
58
+ major_version = version.split('.')[0..1].join('.')
59
+ unity_versions << {
60
+ path: path,
61
+ version: version,
62
+ major_version: major_version
63
+ }
64
+ end
65
+ end
66
+ end
67
+
68
+ if unity_versions.empty?
69
+ puts "调试信息: 搜索的Unity路径:"
70
+ paths.each do |path|
71
+ puts " - #{path}"
72
+ if path.include?("*")
73
+ Dir.glob(path).each do |expanded_path|
74
+ puts " 展开: #{expanded_path}"
75
+ end
76
+ elsif File.exist?(path)
77
+ puts " 存在: #{path}"
78
+ else
79
+ puts " 不存在: #{path}"
80
+ end
81
+ end
82
+ raise Informative, "未找到任何Unity版本,请检查Unity是否正确安装"
83
+ end
84
+
85
+ # 精确匹配项目版本
86
+ select_unity_versions = unity_versions.select { |v| v[:version] == project_unity_version } || []
87
+ if !select_unity_versions.nil? && !select_unity_versions.empty? && select_unity_versions.length >= 1
88
+ return select_unity_versions.first[:path]
89
+ end
90
+
91
+ # 按主版本匹配
92
+ unity_versions.sort_by! { |v| v[:major_version] }
93
+ select_unity_versions = unity_versions.select { |v| v[:major_version] == unity_major_version } if unity_major_version
94
+ if select_unity_versions.nil? || select_unity_versions.empty?
95
+ if force_change_version
96
+ puts "强制使用最新版本: #{unity_versions.last[:version]}"
97
+ return unity_versions.last[:path]
98
+ else
99
+ puts "调试信息: 项目Unity版本: #{project_unity_version}"
100
+ puts "调试信息: 可用的Unity版本:"
101
+ unity_versions.each do |v|
102
+ puts " - #{v[:version]} (#{v[:major_version]})"
103
+ end
104
+ raise Informative, "未找到匹配的Unity版本 #{project_unity_version},可用的版本: #{unity_versions.map { |v| v[:version] }.join(', ')}"
105
+ end
106
+ else
107
+ return select_unity_versions.first[:path]
108
+ end
109
+ end
110
+
111
+ # 从路径中提取 Unity 版本号
112
+ # @param path [String] Unity 可执行文件路径
113
+ # @return [String, nil] 版本号或 nil
114
+ def self.extract_version_from_path(path)
115
+ # macOS路径格式: /Applications/Unity/Hub/Editor/2021.3.45f1/Unity.app/Contents/MacOS/Unity
116
+ # macOS路径格式(变体): /Applications/Unity/Hub/Editor/2021.3.45f1c1/Unity.app/Contents/MacOS/Unity
117
+ # macOS路径格式(旧版): /Applications/Unity/Unity.app/Contents/MacOS/Unity
118
+ # Windows路径格式: C:/Program Files/Unity/Hub/Editor/2021.3.45f1/Editor/Unity.exe
119
+ # Windows路径格式(变体): C:/Program Files/Unity/Hub/Editor/2021.3.45f1c1/Editor/Unity.exe
120
+ # Windows路径格式(旧版): C:/Program Files/Unity/Editor/Unity.exe
121
+
122
+ # 尝试匹配 macOS Hub 路径格式
123
+ if match = path.match(/Editor\/([\d.]+[a-zA-Z]\d+(?:c\d+)?)\//)
124
+ return match[1]
125
+ end
126
+
127
+ # 尝试匹配 Windows Hub 路径格式
128
+ if match = path.match(/([\d.]+[a-zA-Z]\d+(?:c\d+)?)\/Editor\//)
129
+ return match[1]
130
+ end
131
+
132
+ # 尝试匹配 macOS 旧版路径格式 (从Info.plist提取版本)
133
+ if match = path.match(/Unity\.app\/Contents\/MacOS\/Unity$/)
134
+ info_plist_path = File.join(File.dirname(File.dirname(path)), "Info.plist")
135
+ if File.exist?(info_plist_path)
136
+ begin
137
+ content = File.read(info_plist_path)
138
+ if content =~ /<key>CFBundleVersion<\/key>\s*<string>([^<]+)<\/string>/
139
+ return $1.strip
140
+ elsif content =~ /<key>CFBundleShortVersionString<\/key>\s*<string>Unity version ([^<]+)<\/string>/
141
+ return $1.strip
142
+ end
143
+ rescue => e
144
+ puts "警告: 无法读取Info.plist文件: #{e.message}"
145
+ end
146
+ end
147
+ end
148
+
149
+ # 尝试匹配 Windows 旧版路径格式
150
+ if match = path.match(/Unity\.exe$/)
151
+ # 对于旧版Unity,尝试从父目录获取版本信息
152
+ parent_dir = File.dirname(path)
153
+ if File.basename(parent_dir) =~ /^([\d.]+[a-zA-Z]\d+(?:c\d+)?)$/
154
+ return $1
155
+ end
156
+ end
157
+
158
+ nil
159
+ end
160
+
161
+ # 比较版本号 (语义化版本比较)
162
+ # @param v1 [String] 版本1
163
+ # @param v2 [String] 版本2
164
+ # @return [Boolean] true 如果 v1 < v2, false 否则
165
+ def self.version_less_than?(v1, v2)
166
+ return false if v1.nil? || v2.nil?
167
+
168
+ Gem::Version.new(v1) < Gem::Version.new(v2)
169
+ rescue ArgumentError
170
+ # 版本格式无效时的降级处理
171
+ v1.to_s < v2.to_s
172
+ end
173
+
174
+ # 获取项目的 Unity 版本
175
+ # @param project_path [String] Unity 项目路径
176
+ # @return [String] Unity 版本号
177
+ def self.get_unity_version(project_path)
178
+ version_path = File.join(project_path, "ProjectSettings", "ProjectVersion.txt")
179
+ if File.exist?(version_path)
180
+ content = File.read(version_path)
181
+ if content =~ /m_EditorVersion: (.*)/
182
+ version = $1.strip
183
+ version
184
+ else
185
+ raise "Could not parse Unity version from #{version_path}"
186
+ end
187
+ else
188
+ raise "Project version file not found at #{version_path}"
189
+ end
190
+ end
191
+
192
+ # 检查是否为有效的 Unity 项目
193
+ # @param project_path [String] 项目路径
194
+ # @return [Boolean] true 如果是有效的 Unity 项目
195
+ def self.unity_project?(project_path)
196
+ # 检查关键Unity工程文件和目录是否存在
197
+ project_settings_path = File.join(project_path, "ProjectSettings")
198
+ assets_path = File.join(project_path, "Assets")
199
+ packages_path = File.join(project_path, "Packages")
200
+
201
+ File.directory?(project_settings_path) &&
202
+ File.directory?(assets_path) &&
203
+ File.directory?(packages_path) &&
204
+ File.exist?(File.join(project_settings_path, "ProjectSettings.asset"))
205
+ end
206
+ end
207
+ end
208
+ end