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.
- checksums.yaml +4 -4
- data/lib/pindo/base/git_handler.rb +247 -42
- data/lib/pindo/command/android/autobuild.rb +104 -31
- data/lib/pindo/command/android/autoresign.rb +23 -322
- data/lib/pindo/command/android/keystore.rb +7 -130
- data/lib/pindo/command/appstore/adhocbuild.rb +52 -15
- data/lib/pindo/command/appstore/autobuild.rb +104 -8
- data/lib/pindo/command/appstore/autoresign.rb +3 -5
- data/lib/pindo/command/ios/autobuild.rb +96 -32
- data/lib/pindo/command/ios/build.rb +8 -186
- data/lib/pindo/command/jps/media.rb +146 -0
- data/lib/pindo/command/jps/upload.rb +49 -21
- data/lib/pindo/command/jps.rb +1 -0
- data/lib/pindo/command/unity/autobuild.rb +141 -32
- data/lib/pindo/command/unity/packpush.rb +5 -8
- data/lib/pindo/command/utils/repoinit.rb +0 -2
- data/lib/pindo/command/utils/tag.rb +58 -26
- data/lib/pindo/command/utils.rb +0 -1
- data/lib/pindo/command/web/autobuild.rb +98 -34
- data/lib/pindo/command.rb +0 -56
- data/lib/pindo/config/build_info_manager.rb +7 -8
- data/lib/pindo/module/android/android_config_helper.rb +2 -11
- data/lib/pindo/module/appselect.rb +15 -41
- data/lib/pindo/module/appstore/itcapp_helper.rb +3 -6
- data/lib/pindo/module/build/build_helper.rb +28 -18
- data/lib/pindo/module/build/git_repo_helper.rb +284 -405
- data/lib/pindo/module/cert/pem_helper.rb +3 -6
- data/lib/pindo/module/pgyer/pgyerhelper.rb +193 -25
- data/lib/pindo/module/task/model/appstore/appstore_task.rb +5 -0
- data/lib/pindo/module/task/model/build/android_build_adhoc_task.rb +13 -187
- data/lib/pindo/module/task/model/build/android_build_dev_task.rb +36 -34
- data/lib/pindo/module/task/model/build/android_build_gplay_task.rb +13 -187
- data/lib/pindo/module/task/model/build/ios_build_adhoc_task.rb +9 -6
- data/lib/pindo/module/task/model/build/ios_build_appstore_task.rb +9 -6
- data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +37 -32
- data/lib/pindo/module/task/model/build/web_build_dev_task.rb +7 -5
- data/lib/pindo/module/task/model/build_task.rb +8 -11
- data/lib/pindo/module/task/model/git/git_commit_task.rb +118 -0
- data/lib/pindo/module/task/model/git/git_tag_task.rb +125 -0
- data/lib/pindo/module/task/model/git_task.rb +75 -0
- data/lib/pindo/module/task/model/jps/jps_message_task.rb +178 -0
- data/lib/pindo/module/task/model/{jps_resign_task.rb → jps/jps_resign_task.rb} +14 -23
- data/lib/pindo/module/task/model/jps/jps_upload_media_task.rb +248 -0
- data/lib/pindo/module/task/model/{jps_upload_task.rb → jps/jps_upload_task.rb} +39 -94
- data/lib/pindo/module/task/model/jps_task.rb +43 -0
- data/lib/pindo/module/task/model/{ipa_local_resign_task.rb → resign/ipa_local_resign_task.rb} +7 -2
- data/lib/pindo/module/task/model/unity/unity_config_task.rb +103 -0
- data/lib/pindo/module/task/model/{unity_export_task.rb → unity/unity_export_task.rb} +76 -78
- data/lib/pindo/module/task/model/unity/unity_update_task.rb +95 -0
- data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +156 -0
- data/lib/pindo/module/task/model/unity_task.rb +118 -0
- data/lib/pindo/module/task/pindo_task.rb +101 -1
- data/lib/pindo/module/task/task_manager.rb +29 -24
- data/lib/pindo/module/unity/nuget_helper.rb +7 -7
- data/lib/pindo/module/unity/unity_command_helper.rb +188 -0
- data/lib/pindo/module/unity/unity_env_helper.rb +208 -0
- data/lib/pindo/module/unity/unity_helper.rb +189 -746
- data/lib/pindo/module/unity/unity_proc_helper.rb +390 -0
- data/lib/pindo/options/core/global_options_state.rb +96 -26
- data/lib/pindo/options/core/option_configuration.rb +3 -0
- data/lib/pindo/options/core/option_item.rb +36 -0
- data/lib/pindo/options/groups/build_options.rb +23 -6
- data/lib/pindo/options/groups/git_options.rb +115 -0
- data/lib/pindo/options/groups/jps_options.rb +7 -0
- data/lib/pindo/options/groups/option_group.rb +15 -0
- data/lib/pindo/options/groups/unity_options.rb +49 -0
- data/lib/pindo/options/options.rb +2 -0
- data/lib/pindo/version.rb +2 -2
- metadata +25 -14
- data/lib/pindo/base/githelper.rb +0 -686
- data/lib/pindo/base/pindocontext.rb +0 -602
- data/lib/pindo/command/utils/feishu.rb +0 -134
- data/lib/pindo/module/build/version_helper.rb +0 -146
- 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
|
-
|
|
246
|
-
|
|
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
|