pindo 5.13.12 → 5.13.13
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/funlog.rb +62 -5
- data/lib/pindo/base/git_handler.rb +83 -22
- data/lib/pindo/base/output_sink.rb +69 -0
- data/lib/pindo/command/android/autobuild.rb +57 -8
- data/lib/pindo/command/appstore/autobuild.rb +10 -1
- data/lib/pindo/command/ios/autobuild.rb +59 -7
- data/lib/pindo/command/jps/media.rb +164 -13
- data/lib/pindo/command/jps/upload.rb +14 -9
- data/lib/pindo/command/unity/autobuild.rb +64 -10
- data/lib/pindo/command/utils/tag.rb +9 -1
- data/lib/pindo/command/web/autobuild.rb +59 -10
- data/lib/pindo/module/android/android_build_helper.rb +6 -7
- data/lib/pindo/module/build/git_repo_helper.rb +29 -25
- data/lib/pindo/module/pgyer/pgyerhelper.rb +174 -77
- data/lib/pindo/module/task/core/concurrent_execution_strategy.rb +237 -0
- data/lib/pindo/module/task/core/dependency_checker.rb +123 -0
- data/lib/pindo/module/task/core/execution_strategy.rb +61 -0
- data/lib/pindo/module/task/core/resource_lock_manager.rb +190 -0
- data/lib/pindo/module/task/core/serial_execution_strategy.rb +60 -0
- data/lib/pindo/module/task/core/task_executor.rb +131 -0
- data/lib/pindo/module/task/core/task_queue.rb +221 -0
- data/lib/pindo/module/task/model/build/android_build_dev_task.rb +1 -1
- data/lib/pindo/module/task/model/build/android_build_task.rb +6 -2
- data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +2 -3
- data/lib/pindo/module/task/model/build/ios_build_task.rb +6 -0
- data/lib/pindo/module/task/model/build_task.rb +22 -0
- data/lib/pindo/module/task/model/git/git_commit_task.rb +11 -2
- data/lib/pindo/module/task/model/git_task.rb +6 -0
- data/lib/pindo/module/task/model/jps/jps_message_task.rb +9 -11
- data/lib/pindo/module/task/model/jps/jps_upload_media_task.rb +204 -103
- data/lib/pindo/module/task/model/jps_task.rb +0 -1
- data/lib/pindo/module/task/model/unity_task.rb +38 -2
- data/lib/pindo/module/task/output/multi_line_output_manager.rb +380 -0
- data/lib/pindo/module/task/output/multi_line_task_display.rb +185 -0
- data/lib/pindo/module/task/output/stdout_redirector.rb +95 -0
- data/lib/pindo/module/task/pindo_task.rb +133 -9
- data/lib/pindo/module/task/task_manager.rb +98 -268
- data/lib/pindo/module/task/task_reporter.rb +135 -0
- data/lib/pindo/module/task/task_resources/resource_instance.rb +90 -0
- data/lib/pindo/module/task/task_resources/resource_registry.rb +105 -0
- data/lib/pindo/module/task/task_resources/resource_type.rb +59 -0
- data/lib/pindo/module/task/task_resources/types/directory_based_resource.rb +63 -0
- data/lib/pindo/module/task/task_resources/types/global_exclusive_resource.rb +33 -0
- data/lib/pindo/module/task/task_resources/types/global_shared_resource.rb +34 -0
- data/lib/pindo/module/xcode/xcode_build_helper.rb +26 -8
- data/lib/pindo/options/groups/jps_options.rb +10 -0
- data/lib/pindo/options/groups/task_options.rb +39 -0
- data/lib/pindo/version.rb +3 -2
- metadata +20 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
require 'pindo/base/funlog'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
module TaskSystem
|
|
5
|
+
# TaskReporter - 任务报告输出
|
|
6
|
+
#
|
|
7
|
+
# 职责:
|
|
8
|
+
# - 输出任务执行计划
|
|
9
|
+
# - 输出任务进度信息
|
|
10
|
+
# - 输出任务执行摘要
|
|
11
|
+
# - 格式化任务状态显示
|
|
12
|
+
class TaskReporter
|
|
13
|
+
def initialize(queue)
|
|
14
|
+
@queue = queue
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 输出任务执行计划
|
|
18
|
+
# @param strategy [Object] 执行策略(响应 :name 方法)
|
|
19
|
+
def print_execution_plan(strategy)
|
|
20
|
+
# 获取待执行任务的快照(按类型分组)
|
|
21
|
+
tasks_by_type = @queue.pending_grouped_by_type
|
|
22
|
+
|
|
23
|
+
puts "\n"
|
|
24
|
+
puts "\e[34m" + "=" * 60
|
|
25
|
+
puts " 任务执行计划 (#{strategy.name})"
|
|
26
|
+
puts "=" * 60
|
|
27
|
+
|
|
28
|
+
tasks_by_type.each do |type, tasks|
|
|
29
|
+
# 直接从任务类获取显示名称
|
|
30
|
+
type_name = if tasks.first&.class&.respond_to?(:task_type_name)
|
|
31
|
+
tasks.first.class.task_type_name
|
|
32
|
+
else
|
|
33
|
+
type.to_s.capitalize
|
|
34
|
+
end
|
|
35
|
+
puts "\n #{type_name}: #{tasks.count} 个任务"
|
|
36
|
+
|
|
37
|
+
# 显示该类型下的每个任务名称
|
|
38
|
+
tasks.each do |task|
|
|
39
|
+
puts " - #{task.name}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
puts "\n 总计: #{@queue.pending_count} 个任务"
|
|
44
|
+
puts "=" * 60 + "\e[0m"
|
|
45
|
+
puts "\n"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 输出任务执行头部
|
|
49
|
+
# @param task [PindoTask] 任务对象
|
|
50
|
+
def print_task_header(task)
|
|
51
|
+
puts ""
|
|
52
|
+
puts "\e[34m▶ #{task.name}\e[0m"
|
|
53
|
+
puts "─" * 60
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 输出任务成功信息
|
|
57
|
+
# @param task [PindoTask] 任务对象
|
|
58
|
+
def print_task_success(task)
|
|
59
|
+
time_str = task.execution_time ? task.execution_time.round(2) : 0
|
|
60
|
+
puts ""
|
|
61
|
+
puts "\e[32m✓ #{task.name} 完成 (#{time_str}s)\e[0m"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 输出任务失败信息
|
|
65
|
+
# @param task [PindoTask] 任务对象
|
|
66
|
+
# @param error [Exception] 错误对象
|
|
67
|
+
def print_task_failure(task, error)
|
|
68
|
+
puts "\n"
|
|
69
|
+
Funlog.error("任务失败: #{task.name}")
|
|
70
|
+
Funlog.error("错误信息: #{error.message}")
|
|
71
|
+
|
|
72
|
+
# 打印堆栈信息用于调试(仅在 PINDO_DEBUG 模式下)
|
|
73
|
+
if ENV['PINDO_DEBUG'] && error.backtrace && !error.backtrace.empty?
|
|
74
|
+
puts "\n堆栈信息:"
|
|
75
|
+
error.backtrace.first(10).each do |line|
|
|
76
|
+
puts " #{line}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 输出任务底部分隔线
|
|
82
|
+
def print_task_footer
|
|
83
|
+
# 简化输出,不需要额外的分隔线
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# 输出执行摘要
|
|
87
|
+
def print_execution_summary
|
|
88
|
+
completed = @queue.completed_snapshot
|
|
89
|
+
success_count = completed.count { |t| t.status == TaskStatus::SUCCESS }
|
|
90
|
+
failed_count = completed.count { |t| t.status == TaskStatus::FAILED }
|
|
91
|
+
cancelled_count = completed.count { |t| t.status == TaskStatus::CANCELLED }
|
|
92
|
+
|
|
93
|
+
# 获取失败和取消的任务
|
|
94
|
+
failed_tasks = completed.select { |t| t.status == TaskStatus::FAILED }
|
|
95
|
+
cancelled_tasks = completed.select { |t| t.status == TaskStatus::CANCELLED }
|
|
96
|
+
|
|
97
|
+
# 计算总耗时
|
|
98
|
+
total_time = completed.map(&:execution_time).compact.sum
|
|
99
|
+
minutes = (total_time / 60).to_i
|
|
100
|
+
seconds = (total_time % 60).to_i
|
|
101
|
+
|
|
102
|
+
puts "\n"
|
|
103
|
+
puts "\e[34m" + "=" * 60
|
|
104
|
+
puts "\e[34m" + " 任务执行完成"
|
|
105
|
+
puts "\e[34m" + "=" * 60
|
|
106
|
+
|
|
107
|
+
# 显示统计信息
|
|
108
|
+
puts "\e[34m" + " 成功: #{success_count} 个任务" + "\e[0m" if success_count > 0
|
|
109
|
+
|
|
110
|
+
if failed_count > 0
|
|
111
|
+
puts "\e[31m" + " 失败: #{failed_count} 个任务" + "\e[0m"
|
|
112
|
+
failed_tasks.each do |task|
|
|
113
|
+
puts "\e[31m" + " - #{task.name}" + "\e[0m"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if cancelled_count > 0
|
|
118
|
+
puts "\e[33m" + " 取消: #{cancelled_count} 个任务" + "\e[0m"
|
|
119
|
+
cancelled_tasks.each do |task|
|
|
120
|
+
puts "\e[33m" + " - #{task.name}" + "\e[0m"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if minutes > 0
|
|
125
|
+
puts "\e[34m" + " 总耗时: #{minutes}分#{seconds}秒" + "\e[0m"
|
|
126
|
+
else
|
|
127
|
+
puts "\e[34m" + " 总耗时: #{seconds}秒" + "\e[0m"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
puts "\e[34m" + "=" * 60 + "\e[0m"
|
|
131
|
+
puts "\n"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require_relative 'resource_type'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
module TaskSystem
|
|
5
|
+
module TaskResources
|
|
6
|
+
# 资源实例 - 表示一个具体的资源占用
|
|
7
|
+
#
|
|
8
|
+
# 职责:
|
|
9
|
+
# - 包含资源类型和上下文信息
|
|
10
|
+
# - 提供资源冲突检查接口
|
|
11
|
+
#
|
|
12
|
+
# 示例:
|
|
13
|
+
# xcode_resource = ResourceInstance.new(xcode_type, { directory: "/path/to/project" })
|
|
14
|
+
# network_resource = ResourceInstance.new(network_type, {})
|
|
15
|
+
class ResourceInstance
|
|
16
|
+
attr_reader :type, :context
|
|
17
|
+
|
|
18
|
+
# @param type [ResourceType] 资源类型对象
|
|
19
|
+
# @param context [Hash] 上下文信息(如 { directory: "/path/to/project" })
|
|
20
|
+
def initialize(type, context = {})
|
|
21
|
+
raise ArgumentError, "type must be a ResourceType" unless type.is_a?(ResourceType)
|
|
22
|
+
@type = type
|
|
23
|
+
@context = context || {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# 检查是否与另一个资源实例冲突
|
|
27
|
+
# @param other [ResourceInstance] 另一个资源实例
|
|
28
|
+
# @return [Boolean] 是否冲突
|
|
29
|
+
def conflicts_with?(other)
|
|
30
|
+
return false unless other.is_a?(ResourceInstance)
|
|
31
|
+
return false unless @type.name == other.type.name # 不同类型不冲突
|
|
32
|
+
|
|
33
|
+
# 委托给资源类型检查冲突逻辑
|
|
34
|
+
@type.conflicts?(self, other)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 检查是否匹配另一个资源实例(用于释放资源时查找)
|
|
38
|
+
# @param other [ResourceInstance] 另一个资源实例
|
|
39
|
+
# @return [Boolean] 是否匹配
|
|
40
|
+
def matches?(other)
|
|
41
|
+
return false unless other.is_a?(ResourceInstance)
|
|
42
|
+
return false unless @type.name == other.type.name
|
|
43
|
+
|
|
44
|
+
# 上下文必须完全匹配
|
|
45
|
+
@context == other.context
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 检查是否匹配资源规格(Hash 格式)
|
|
49
|
+
# @param spec [Hash] 资源规格,如 { type: :xcode, directory: "/path" }
|
|
50
|
+
# @return [Boolean] 是否匹配
|
|
51
|
+
def matches_spec?(spec)
|
|
52
|
+
return false unless spec.is_a?(Hash)
|
|
53
|
+
return false unless spec[:type] == @type.name
|
|
54
|
+
|
|
55
|
+
# 提取上下文(除了 :type 之外的所有键)
|
|
56
|
+
spec_context = spec.reject { |k, _| k == :type }
|
|
57
|
+
@context == spec_context
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 资源实例的字符串表示
|
|
61
|
+
# @return [String]
|
|
62
|
+
def to_s
|
|
63
|
+
if @context.empty?
|
|
64
|
+
"#{@type.name}"
|
|
65
|
+
else
|
|
66
|
+
"#{@type.name}(#{@context.inspect})"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 资源实例的详细描述
|
|
71
|
+
# @return [String]
|
|
72
|
+
def inspect
|
|
73
|
+
"#<ResourceInstance type=#{@type.name} strategy=#{@type.strategy} context=#{@context.inspect}>"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 获取目录路径(如果有)
|
|
77
|
+
# @return [String, nil]
|
|
78
|
+
def directory
|
|
79
|
+
@context[:directory]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# 检查是否有目录上下文
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def has_directory?
|
|
85
|
+
!@context[:directory].nil?
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require_relative 'types/directory_based_resource'
|
|
2
|
+
require_relative 'types/global_exclusive_resource'
|
|
3
|
+
require_relative 'types/global_shared_resource'
|
|
4
|
+
|
|
5
|
+
module Pindo
|
|
6
|
+
module TaskSystem
|
|
7
|
+
module TaskResources
|
|
8
|
+
# 资源类型注册表
|
|
9
|
+
#
|
|
10
|
+
# 职责:
|
|
11
|
+
# - 管理所有已注册的资源类型
|
|
12
|
+
# - 提供资源类型查询接口
|
|
13
|
+
# - 注册内置资源类型
|
|
14
|
+
class ResourceRegistry
|
|
15
|
+
def initialize
|
|
16
|
+
@types = {}
|
|
17
|
+
register_builtin_types
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 注册资源类型
|
|
21
|
+
# @param name [Symbol] 资源名称
|
|
22
|
+
# @param type [ResourceType] 资源类型对象
|
|
23
|
+
def register(name, type)
|
|
24
|
+
raise ArgumentError, "type must be a ResourceType" unless type.is_a?(ResourceType)
|
|
25
|
+
@types[name] = type
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 获取资源类型
|
|
29
|
+
# @param name [Symbol] 资源名称
|
|
30
|
+
# @return [ResourceType] 资源类型对象
|
|
31
|
+
# @raise [ArgumentError] 如果资源类型不存在
|
|
32
|
+
def get(name)
|
|
33
|
+
@types[name] or raise ArgumentError, "Unknown resource type: #{name}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 检查资源类型是否已注册
|
|
37
|
+
# @param name [Symbol] 资源名称
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def registered?(name)
|
|
40
|
+
@types.key?(name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 获取所有已注册的资源名称
|
|
44
|
+
# @return [Array<Symbol>]
|
|
45
|
+
def all_names
|
|
46
|
+
@types.keys
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 获取所有资源类型对象
|
|
50
|
+
# @return [Array<ResourceType>]
|
|
51
|
+
def all_types
|
|
52
|
+
@types.values
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# 注册内置资源类型
|
|
58
|
+
def register_builtin_types
|
|
59
|
+
# ========================================
|
|
60
|
+
# 基于目录的资源(相同目录互斥,不同目录可并行)
|
|
61
|
+
# ========================================
|
|
62
|
+
|
|
63
|
+
# 通用构建资源(锁定 git 仓库或项目目录)
|
|
64
|
+
# 确保同一项目下的所有构建任务串行执行
|
|
65
|
+
register(:build, DirectoryBasedResourceType.new(:build))
|
|
66
|
+
|
|
67
|
+
# Xcode 构建工具
|
|
68
|
+
register(:xcode, DirectoryBasedResourceType.new(:xcode))
|
|
69
|
+
|
|
70
|
+
# Git 版本控制
|
|
71
|
+
register(:git, DirectoryBasedResourceType.new(:git))
|
|
72
|
+
|
|
73
|
+
# Unity 编辑器
|
|
74
|
+
register(:unity, DirectoryBasedResourceType.new(:unity))
|
|
75
|
+
|
|
76
|
+
# Gradle 构建工具
|
|
77
|
+
register(:gradle, DirectoryBasedResourceType.new(:gradle))
|
|
78
|
+
|
|
79
|
+
# ========================================
|
|
80
|
+
# 全局互斥资源(任何时候只能一个任务使用)
|
|
81
|
+
# ========================================
|
|
82
|
+
|
|
83
|
+
# Keychain 操作
|
|
84
|
+
register(:keychain, GlobalExclusiveResourceType.new(:keychain))
|
|
85
|
+
|
|
86
|
+
# 证书操作
|
|
87
|
+
register(:certificate, GlobalExclusiveResourceType.new(:certificate))
|
|
88
|
+
|
|
89
|
+
# ========================================
|
|
90
|
+
# 全局共享资源(无限制并发)
|
|
91
|
+
# ========================================
|
|
92
|
+
|
|
93
|
+
# 网络操作
|
|
94
|
+
register(:network, GlobalSharedResourceType.new(:network))
|
|
95
|
+
|
|
96
|
+
# App Store Connect API
|
|
97
|
+
register(:appstore_connect, GlobalSharedResourceType.new(:appstore_connect))
|
|
98
|
+
|
|
99
|
+
# 文件系统操作
|
|
100
|
+
register(:file_system, GlobalSharedResourceType.new(:file_system))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Pindo
|
|
2
|
+
module TaskSystem
|
|
3
|
+
module TaskResources
|
|
4
|
+
# 资源类型基类
|
|
5
|
+
#
|
|
6
|
+
# 职责:
|
|
7
|
+
# - 定义资源的冲突检查逻辑
|
|
8
|
+
# - 子类实现不同的冲突策略
|
|
9
|
+
#
|
|
10
|
+
# 资源冲突策略:
|
|
11
|
+
# - :directory_based - 基于目录的资源(相同目录互斥,不同目录可并行)
|
|
12
|
+
# - :global_exclusive - 全局互斥资源(任何时候只能一个任务使用)
|
|
13
|
+
# - :global_shared - 全局共享资源(无限制并发)
|
|
14
|
+
class ResourceType
|
|
15
|
+
attr_reader :name, :strategy
|
|
16
|
+
|
|
17
|
+
# @param name [Symbol] 资源名称
|
|
18
|
+
# @param strategy [Symbol] 冲突策略
|
|
19
|
+
def initialize(name, strategy)
|
|
20
|
+
@name = name
|
|
21
|
+
@strategy = strategy
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# 检查两个资源实例是否冲突(抽象方法,子类必须实现)
|
|
25
|
+
# @param instance1 [ResourceInstance] 资源实例1
|
|
26
|
+
# @param instance2 [ResourceInstance] 资源实例2
|
|
27
|
+
# @return [Boolean] 是否冲突
|
|
28
|
+
# @raise [NotImplementedError] 如果子类未实现
|
|
29
|
+
def conflicts?(instance1, instance2)
|
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #conflicts?"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 资源类型的字符串表示
|
|
34
|
+
# @return [String]
|
|
35
|
+
def to_s
|
|
36
|
+
"#{@name}(#{@strategy})"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 检查是否为基于目录的资源
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def directory_based?
|
|
42
|
+
@strategy == :directory_based
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 检查是否为全局互斥资源
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def global_exclusive?
|
|
48
|
+
@strategy == :global_exclusive
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 检查是否为全局共享资源
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def global_shared?
|
|
54
|
+
@strategy == :global_shared
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require_relative '../resource_type'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
module TaskSystem
|
|
5
|
+
module TaskResources
|
|
6
|
+
# 基于目录的资源类型
|
|
7
|
+
#
|
|
8
|
+
# 特性:
|
|
9
|
+
# - 相同目录的资源互斥(不能并发)
|
|
10
|
+
# - 不同目录的资源可以并行
|
|
11
|
+
#
|
|
12
|
+
# 适用资源:
|
|
13
|
+
# - Xcode(相同项目目录互斥)
|
|
14
|
+
# - Git(相同仓库目录互斥)
|
|
15
|
+
# - Unity(相同项目目录互斥)
|
|
16
|
+
# - Gradle(相同项目目录互斥)
|
|
17
|
+
class DirectoryBasedResourceType < ResourceType
|
|
18
|
+
# @param name [Symbol] 资源名称
|
|
19
|
+
def initialize(name)
|
|
20
|
+
super(name, :directory_based)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 检查两个资源实例是否冲突
|
|
24
|
+
# 规则:
|
|
25
|
+
# 1. 如果任一资源未指定目录,视为全局冲突(为了安全起见,防止漏锁)
|
|
26
|
+
# 2. 相同目录冲突
|
|
27
|
+
# 3. 不同目录不冲突
|
|
28
|
+
#
|
|
29
|
+
# @param instance1 [ResourceInstance]
|
|
30
|
+
# @param instance2 [ResourceInstance]
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
def conflicts?(instance1, instance2)
|
|
33
|
+
dir1 = instance1.context[:directory]
|
|
34
|
+
dir2 = instance2.context[:directory]
|
|
35
|
+
|
|
36
|
+
# 修改:如果任一资源没有指定目录,视为冲突(Fail-safe 策略)
|
|
37
|
+
# 这确保了如果某个任务忘记或者无法指定目录,它会独占该类型资源,而不是并发执行
|
|
38
|
+
return true if dir1.nil? || dir2.nil?
|
|
39
|
+
|
|
40
|
+
# 规范化路径后比较
|
|
41
|
+
normalize_path(dir1) == normalize_path(dir2)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# 规范化路径(展开相对路径、软链接等)
|
|
47
|
+
# @param path [String] 原始路径
|
|
48
|
+
# @return [String] 规范化后的绝对路径
|
|
49
|
+
def normalize_path(path)
|
|
50
|
+
return nil if path.nil?
|
|
51
|
+
|
|
52
|
+
# 尝试获取真实路径(解决软链接问题)
|
|
53
|
+
begin
|
|
54
|
+
File.realpath(File.expand_path(path))
|
|
55
|
+
rescue
|
|
56
|
+
# 如果文件不存在,回退到 expand_path
|
|
57
|
+
File.expand_path(path)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require_relative '../resource_type'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
module TaskSystem
|
|
5
|
+
module TaskResources
|
|
6
|
+
# 全局互斥资源类型
|
|
7
|
+
#
|
|
8
|
+
# 特性:
|
|
9
|
+
# - 任何时候只能有一个任务占用
|
|
10
|
+
# - 任意两个实例都冲突
|
|
11
|
+
#
|
|
12
|
+
# 适用资源:
|
|
13
|
+
# - Keychain(系统级密钥链,全局唯一)
|
|
14
|
+
# - Certificate(证书操作,全局互斥)
|
|
15
|
+
class GlobalExclusiveResourceType < ResourceType
|
|
16
|
+
# @param name [Symbol] 资源名称
|
|
17
|
+
def initialize(name)
|
|
18
|
+
super(name, :global_exclusive)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 检查两个资源实例是否冲突
|
|
22
|
+
# 规则:永远冲突(全局互斥)
|
|
23
|
+
#
|
|
24
|
+
# @param instance1 [ResourceInstance]
|
|
25
|
+
# @param instance2 [ResourceInstance]
|
|
26
|
+
# @return [Boolean] 永远返回 true
|
|
27
|
+
def conflicts?(instance1, instance2)
|
|
28
|
+
true # 全局互斥,任意两个实例都冲突
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative '../resource_type'
|
|
2
|
+
|
|
3
|
+
module Pindo
|
|
4
|
+
module TaskSystem
|
|
5
|
+
module TaskResources
|
|
6
|
+
# 全局共享资源类型
|
|
7
|
+
#
|
|
8
|
+
# 特性:
|
|
9
|
+
# - 无限制并发
|
|
10
|
+
# - 任意两个实例都不冲突
|
|
11
|
+
#
|
|
12
|
+
# 适用资源:
|
|
13
|
+
# - Network(网络操作,可以并发)
|
|
14
|
+
# - AppStore Connect(API 调用,可以并发)
|
|
15
|
+
# - File System(文件系统读取,可以并发)
|
|
16
|
+
class GlobalSharedResourceType < ResourceType
|
|
17
|
+
# @param name [Symbol] 资源名称
|
|
18
|
+
def initialize(name)
|
|
19
|
+
super(name, :global_shared)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# 检查两个资源实例是否冲突
|
|
23
|
+
# 规则:永不冲突(全局共享)
|
|
24
|
+
#
|
|
25
|
+
# @param instance1 [ResourceInstance]
|
|
26
|
+
# @param instance2 [ResourceInstance]
|
|
27
|
+
# @return [Boolean] 永远返回 false
|
|
28
|
+
def conflicts?(instance1, instance2)
|
|
29
|
+
false # 全局共享,永不冲突
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -452,14 +452,18 @@ module Pindo
|
|
|
452
452
|
# 修复 Xcode 16 链接器兼容性问题
|
|
453
453
|
fix_xcode16_linker_flags(project_dir: project_dir)
|
|
454
454
|
|
|
455
|
-
# 清理旧的 build 目录
|
|
455
|
+
# 清理旧的 build 目录 (使用绝对路径)
|
|
456
456
|
build_dir = File.join(project_dir, "build")
|
|
457
457
|
FileUtils.rm_rf(build_dir) if File.exist?(build_dir)
|
|
458
|
+
|
|
459
|
+
# 手动创建输出目录
|
|
460
|
+
FileUtils.mkdir_p(build_dir)
|
|
458
461
|
|
|
459
|
-
# 获取构建参数
|
|
462
|
+
# 获取构建参数 (传入 output_directory)
|
|
460
463
|
gym_options = get_gym_build_options(
|
|
461
464
|
project_fullname: project_fullname,
|
|
462
|
-
icloud_id: icloud_id
|
|
465
|
+
icloud_id: icloud_id,
|
|
466
|
+
output_directory: build_dir
|
|
463
467
|
)
|
|
464
468
|
|
|
465
469
|
# 执行构建
|
|
@@ -468,7 +472,7 @@ module Pindo
|
|
|
468
472
|
Gym::Manager.new.work(config)
|
|
469
473
|
|
|
470
474
|
# 查找生成的 IPA 文件
|
|
471
|
-
build_path = File.join(
|
|
475
|
+
build_path = File.join(build_dir, "*.ipa")
|
|
472
476
|
ipa_file = Dir.glob(build_path).max_by { |f| File.mtime(f) }
|
|
473
477
|
|
|
474
478
|
ipa_file
|
|
@@ -477,14 +481,20 @@ module Pindo
|
|
|
477
481
|
# 获取 Gym 构建参数
|
|
478
482
|
# @param project_fullname [String] 工程文件完整路径
|
|
479
483
|
# @param icloud_id [String, nil] iCloud ID(可选)
|
|
484
|
+
# @param output_directory [String] 输出目录绝对路径
|
|
480
485
|
# @return [Hash] Gym 构建参数
|
|
481
|
-
def get_gym_build_options(project_fullname:, icloud_id: nil)
|
|
486
|
+
def get_gym_build_options(project_fullname:, icloud_id: nil, output_directory: nil)
|
|
482
487
|
if project_fullname.nil?
|
|
483
488
|
raise "请指定要编译的工程"
|
|
484
489
|
end
|
|
485
490
|
|
|
486
491
|
project_path = File.dirname(project_fullname)
|
|
487
492
|
proj_name = File.basename(project_fullname, ".xcodeproj")
|
|
493
|
+
|
|
494
|
+
# 使用绝对路径
|
|
495
|
+
project_fullname_abs = File.expand_path(project_fullname)
|
|
496
|
+
project_path_abs = File.dirname(project_fullname_abs)
|
|
497
|
+
|
|
488
498
|
project_obj = Xcodeproj::Project.open(project_fullname)
|
|
489
499
|
|
|
490
500
|
project_build_platform = project_obj.root_object.build_configuration_list.get_setting("SDKROOT")["Release"]
|
|
@@ -533,14 +543,22 @@ module Pindo
|
|
|
533
543
|
app_ipaname_temp = app_ipaname_temp.gsub(/ /, '')
|
|
534
544
|
app_ipaname_temp = app_ipaname_temp.gsub(/\'/, '')
|
|
535
545
|
|
|
546
|
+
# 确保输出目录是绝对路径
|
|
547
|
+
output_directory ||= File.join(project_path_abs, "build")
|
|
548
|
+
output_directory = File.expand_path(output_directory)
|
|
549
|
+
|
|
536
550
|
# 构建基本参数
|
|
537
551
|
values = {
|
|
538
|
-
|
|
552
|
+
# 虽然 gym 支持 absolute path,但是为了保险起见,如果提供了 abs path project_fullname,
|
|
553
|
+
# 我们这里还是传 relative 给 gym,但是我们在调用 gym 之前不 chdir。
|
|
554
|
+
# 实际上 gym 的 behavior: 如果 project 是 relative,它相对于 cwd。
|
|
555
|
+
# 所以我们必须给 gym 传入 ABSOLUTE PATH 的 project 才能避免 chdir 问题。
|
|
556
|
+
project: project_fullname_abs,
|
|
539
557
|
scheme: "#{proj_name}",
|
|
540
558
|
configuration: "Release",
|
|
541
559
|
clean: true,
|
|
542
560
|
export_method: build_type,
|
|
543
|
-
output_directory:
|
|
561
|
+
output_directory: output_directory,
|
|
544
562
|
output_name: "#{app_ipaname_temp}.ipa"
|
|
545
563
|
}
|
|
546
564
|
|
|
@@ -552,7 +570,7 @@ module Pindo
|
|
|
552
570
|
|
|
553
571
|
# 如果使用 CocoaPods
|
|
554
572
|
if File.exist?(File.join(project_path, "Podfile"))
|
|
555
|
-
values[:workspace] = "#{proj_name}.xcworkspace"
|
|
573
|
+
values[:workspace] = File.join(project_path_abs, "#{proj_name}.xcworkspace")
|
|
556
574
|
values[:project] = nil
|
|
557
575
|
end
|
|
558
576
|
|
|
@@ -69,6 +69,16 @@ module Pindo
|
|
|
69
69
|
env_name: 'PINDO_CERT_ID',
|
|
70
70
|
optional: true,
|
|
71
71
|
example: 'pindo jps resign --certid=com.test.bundleid'
|
|
72
|
+
),
|
|
73
|
+
|
|
74
|
+
media: OptionItem.new(
|
|
75
|
+
key: :media,
|
|
76
|
+
name: '上传媒体附件',
|
|
77
|
+
description: '上传完成后,从 JPSMedia/ 目录上传媒体附件(图片、视频)',
|
|
78
|
+
type: OptionItem::Boolean,
|
|
79
|
+
env_name: 'PINDO_UPLOAD_MEDIA',
|
|
80
|
+
optional: true,
|
|
81
|
+
example: 'pindo ios autobuild --media'
|
|
72
82
|
)
|
|
73
83
|
}
|
|
74
84
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'pindo/options/core/option_item'
|
|
2
|
+
require 'pindo/options/groups/option_group'
|
|
3
|
+
|
|
4
|
+
module Pindo
|
|
5
|
+
module Options
|
|
6
|
+
# Task 任务参数组
|
|
7
|
+
# 定义任务系统特有的参数
|
|
8
|
+
module TaskOptions
|
|
9
|
+
extend OptionGroup
|
|
10
|
+
|
|
11
|
+
def self.all_options
|
|
12
|
+
@all_options ||= {
|
|
13
|
+
multi: OptionItem.new(
|
|
14
|
+
key: :multi,
|
|
15
|
+
name: '并发执行',
|
|
16
|
+
description: '使用并发任务系统执行构建(提高构建速度)',
|
|
17
|
+
type: OptionItem::Boolean,
|
|
18
|
+
env_name: 'PINDO_TASK_MULTI',
|
|
19
|
+
default_value: false,
|
|
20
|
+
optional: true,
|
|
21
|
+
cache: false, # 不缓存该参数,每次都需要显式指定
|
|
22
|
+
example: 'pindo unity autobuild --multi'
|
|
23
|
+
),
|
|
24
|
+
|
|
25
|
+
select: OptionItem.new(
|
|
26
|
+
key: :select,
|
|
27
|
+
name: '选择提交',
|
|
28
|
+
description: '交互式选择历史 git commit(而不是使用 HEAD)',
|
|
29
|
+
type: OptionItem::Boolean,
|
|
30
|
+
env_name: 'PINDO_TASK_SELECT',
|
|
31
|
+
default_value: false,
|
|
32
|
+
optional: true,
|
|
33
|
+
example: 'pindo jps media --select'
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/pindo/version.rb
CHANGED
|
@@ -6,13 +6,13 @@ require 'time'
|
|
|
6
6
|
|
|
7
7
|
module Pindo
|
|
8
8
|
|
|
9
|
-
VERSION = "5.13.
|
|
9
|
+
VERSION = "5.13.13"
|
|
10
10
|
|
|
11
11
|
class VersionCheck
|
|
12
12
|
RUBYGEMS_API = 'https://rubygems.org/api/v1/gems/pindo.json'
|
|
13
13
|
VERSION_INFO_FILE = File.expand_path('~/.pindo/version_info.yml')
|
|
14
14
|
CHECK_INTERVAL = 5 * 60 * 60 # 5小时检查一次
|
|
15
|
-
CONFIG_MIN_VERSION = '1.
|
|
15
|
+
CONFIG_MIN_VERSION = '1.2.0' # 硬编码的配置版本要求
|
|
16
16
|
|
|
17
17
|
class << self
|
|
18
18
|
# 主版本检查方法(保持向后兼容)
|
|
@@ -187,6 +187,7 @@ module Pindo
|
|
|
187
187
|
# 创建 HTTP 连接,禁用代理
|
|
188
188
|
http = Net::HTTP.new(uri.host, uri.port, nil, nil)
|
|
189
189
|
http.use_ssl = true
|
|
190
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # 忽略 SSL 证书验证
|
|
190
191
|
http.open_timeout = 3 # 3秒超时
|
|
191
192
|
http.read_timeout = 3
|
|
192
193
|
|