pindo 5.18.13 → 5.18.20
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 +17 -29
- data/lib/pindo/command/appstore/adhocbuild.rb +13 -2
- data/lib/pindo/command/appstore/autobuild.rb +2 -1
- data/lib/pindo/command/appstore/iap.rb +58 -54
- data/lib/pindo/config/build_info_manager.rb +31 -0
- data/lib/pindo/config/ios_config_parser.rb +212 -140
- data/lib/pindo/module/android/android_build_helper.rb +70 -15
- data/lib/pindo/module/android/gradle_helper.rb +13 -2
- data/lib/pindo/module/android/workflow_gradle_injector.rb +48 -12
- data/lib/pindo/module/appstore/appstore_in_app_purchase.rb +80 -56
- data/lib/pindo/module/task/model/build/ios_build_adhoc_task.rb +4 -30
- data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +14 -4
- data/lib/pindo/module/task/model/git/git_commit_task.rb +15 -1
- data/lib/pindo/module/task/model/git/git_tag_task.rb +90 -75
- data/lib/pindo/module/task/model/unity/unity_export_task.rb +44 -0
- data/lib/pindo/module/task/task_reporter.rb +45 -0
- data/lib/pindo/module/utils/git_repo_helper.rb +55 -29
- data/lib/pindo/module/xcode/res/xcode_res_handler.rb +25 -4
- data/lib/pindo/module/xcode/xcode_build_config.rb +7 -1
- data/lib/pindo/module/xcode/xcode_build_helper.rb +36 -4
- data/lib/pindo/module/xcode/xcode_res_helper.rb +8 -0
- data/lib/pindo/options/helpers/bundleid_selector.rb +23 -6
- data/lib/pindo/version.rb +1 -1
- metadata +2 -2
|
@@ -51,93 +51,108 @@ module Pindo
|
|
|
51
51
|
|
|
52
52
|
root_dir = @git_root_path
|
|
53
53
|
git_repo_helper = Pindo::GitRepoHelper.share_instance
|
|
54
|
-
stash_saved = false
|
|
55
54
|
stash_name = "pindo_stash_#{Time.now.strftime('%Y%m%d%H%M%S')}_#{rand(1000)}"
|
|
56
55
|
coding_branch = Pindo::GitHandler.git!(%W(-C #{root_dir} rev-parse --abbrev-ref HEAD)).strip
|
|
56
|
+
git_commit_data = get_data_param_by_key(:git_commit)
|
|
57
|
+
stash_saved = false
|
|
57
58
|
|
|
58
59
|
begin
|
|
59
|
-
# 1.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@tag_pre = param[:tag_pre] if param[:tag_pre]
|
|
73
|
-
@tag_type = param[:tag_type] if param[:tag_type]
|
|
74
|
-
@ver_inc = param[:ver_inc] if param[:ver_inc]
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# 3. 合并到发布分支(使用已回填的 @release_branch)
|
|
78
|
-
if coding_branch != @release_branch
|
|
79
|
-
Pindo::GitHandler.merge_branches(
|
|
80
|
-
project_dir: root_dir,
|
|
81
|
-
source_branch: coding_branch,
|
|
82
|
-
target_branch: @release_branch,
|
|
83
|
-
sync_branches: true # 双向合并,确保两分支完全同步
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# 4. 获取版本信息
|
|
88
|
-
if git_commit_data && git_commit_data[:task_param] &&
|
|
89
|
-
git_commit_data[:task_param][:build_version] && git_commit_data[:task_param][:build_number]
|
|
90
|
-
@build_version = git_commit_data[:task_param][:build_version]
|
|
91
|
-
@build_number = git_commit_data[:task_param][:build_number]
|
|
92
|
-
Funlog.instance.fancyinfo_success("使用 GitCommitTask 的版本号: #{@build_version}, Build: #{@build_number}, Tag前缀: #{@tag_pre}, Tag类型: #{@tag_type}")
|
|
93
|
-
|
|
94
|
-
# 降级:自己计算(向后兼容或 GitCommitTask 无有效数据)
|
|
95
|
-
else
|
|
96
|
-
@build_number, @commit_hash = git_repo_helper.get_build_number_from_commit(project_dir: root_dir)
|
|
97
|
-
@build_version = git_repo_helper.calculate_build_version(
|
|
98
|
-
project_dir: root_dir,
|
|
99
|
-
tag_prefix: @tag_pre,
|
|
100
|
-
create_tag_type: @tag_type,
|
|
101
|
-
version_increase_type: @ver_inc
|
|
102
|
-
)
|
|
103
|
-
commit_short = @commit_hash ? @commit_hash[0..7] : "unknown"
|
|
104
|
-
Funlog.instance.fancyinfo_warning("未找到 GitCommitTask 依赖,降级为自己计算版本号: #{@build_version}, Build: #{@build_number}, Commit: #{commit_short}")
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# 4. 创建并推送 Tag
|
|
60
|
+
# 1. 工作目录不干净则暂存
|
|
61
|
+
stash_saved = stash_if_dirty(root_dir, stash_name)
|
|
62
|
+
|
|
63
|
+
# 2. 从 GitCommitTask 回填参数(须在合并之前,确保 release_branch 等正确)
|
|
64
|
+
backfill_params_from_commit_task(git_commit_data)
|
|
65
|
+
|
|
66
|
+
# 3. 合并到发布分支
|
|
67
|
+
merge_to_release_branch(root_dir, coding_branch)
|
|
68
|
+
|
|
69
|
+
# 4. 解析版本号
|
|
70
|
+
resolve_build_version(root_dir, git_commit_data, git_repo_helper)
|
|
71
|
+
|
|
72
|
+
# 5. 创建并推送 Tag
|
|
108
73
|
tag_name = "#{@tag_pre}#{@build_version}"
|
|
109
74
|
git_repo_helper.create_and_push_tag(
|
|
110
75
|
project_dir: root_dir,
|
|
111
76
|
tag_name: tag_name,
|
|
112
77
|
tag_type: @tag_type
|
|
113
78
|
)
|
|
114
|
-
|
|
115
79
|
ensure
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
80
|
+
# 6. 还原 stash
|
|
81
|
+
restore_stash(root_dir, stash_name) if stash_saved
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# 工作目录有未提交更改则暂存,返回是否已暂存
|
|
88
|
+
def stash_if_dirty(root_dir, stash_name)
|
|
89
|
+
status = Pindo::GitHandler.git!(%W(-C #{root_dir} status --porcelain)).strip
|
|
90
|
+
return false if status.empty?
|
|
91
|
+
|
|
92
|
+
Funlog.instance.fancyinfo_update("工作目录有未提交更改,暂存到 stash: #{stash_name}")
|
|
93
|
+
Pindo::GitHandler.git!(%W(-C #{root_dir} stash push -u -m #{stash_name}))
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# 从 GitCommitTask 回填 release_branch / tag_pre / tag_type / ver_inc
|
|
98
|
+
def backfill_params_from_commit_task(git_commit_data)
|
|
99
|
+
return unless git_commit_data && git_commit_data[:task_param]
|
|
100
|
+
|
|
101
|
+
param = git_commit_data[:task_param]
|
|
102
|
+
@release_branch = param[:release_branch] if param[:release_branch]
|
|
103
|
+
@tag_pre = param[:tag_pre] if param[:tag_pre]
|
|
104
|
+
@tag_type = param[:tag_type] if param[:tag_type]
|
|
105
|
+
@ver_inc = param[:ver_inc] if param[:ver_inc]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# 将编码分支双向合并到发布分支,确保两分支同步到同一节点
|
|
109
|
+
def merge_to_release_branch(root_dir, coding_branch)
|
|
110
|
+
return if coding_branch == @release_branch
|
|
111
|
+
|
|
112
|
+
Pindo::GitHandler.merge_branches(
|
|
113
|
+
project_dir: root_dir,
|
|
114
|
+
source_branch: coding_branch,
|
|
115
|
+
target_branch: @release_branch,
|
|
116
|
+
sync_branches: true # 双向合并,确保两分支完全同步
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# 解析构建版本号:优先用 GitCommitTask 的,否则自己计算(向后兼容)
|
|
121
|
+
def resolve_build_version(root_dir, git_commit_data, git_repo_helper)
|
|
122
|
+
if git_commit_data && git_commit_data[:task_param] &&
|
|
123
|
+
git_commit_data[:task_param][:build_version] && git_commit_data[:task_param][:build_number]
|
|
124
|
+
@build_version = git_commit_data[:task_param][:build_version]
|
|
125
|
+
@build_number = git_commit_data[:task_param][:build_number]
|
|
126
|
+
Funlog.instance.fancyinfo_success("使用 GitCommitTask 的版本号: #{@build_version}, Build: #{@build_number}, Tag前缀: #{@tag_pre}, Tag类型: #{@tag_type}")
|
|
127
|
+
else
|
|
128
|
+
@build_number, @commit_hash = git_repo_helper.get_build_number_from_commit(project_dir: root_dir)
|
|
129
|
+
@build_version = git_repo_helper.calculate_build_version(
|
|
130
|
+
project_dir: root_dir,
|
|
131
|
+
tag_prefix: @tag_pre,
|
|
132
|
+
create_tag_type: @tag_type,
|
|
133
|
+
version_increase_type: @ver_inc
|
|
134
|
+
)
|
|
135
|
+
commit_short = @commit_hash ? @commit_hash[0..7] : "unknown"
|
|
136
|
+
Funlog.instance.fancyinfo_warning("未找到 GitCommitTask 依赖,降级为自己计算版本号: #{@build_version}, Build: #{@build_number}, Commit: #{commit_short}")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 还原之前暂存的 stash;冲突时保留 stash 并告警,不抛异常(tag 已创建成功)
|
|
141
|
+
def restore_stash(root_dir, stash_name)
|
|
142
|
+
stash_list = Pindo::GitHandler.git!(%W(-C #{root_dir} stash list)).strip
|
|
143
|
+
matched_line = stash_list.lines.find { |l| l.include?(stash_name) }
|
|
144
|
+
|
|
145
|
+
if matched_line && (stash_id = matched_line.match(/(stash@\{\d+\})/)&.[](1))
|
|
146
|
+
# 先应用 stash(不删除),成功后再删除
|
|
147
|
+
Pindo::GitHandler.git!(%W(-C #{root_dir} stash apply #{stash_id}))
|
|
148
|
+
Pindo::GitHandler.git!(%W(-C #{root_dir} stash drop #{stash_id}))
|
|
149
|
+
Funlog.instance.fancyinfo_success("已还原 stash: #{stash_name} (#{stash_id})")
|
|
150
|
+
else
|
|
151
|
+
Funlog.instance.fancyinfo_warning("未找到名为 #{stash_name} 的 stash,请手动检查")
|
|
140
152
|
end
|
|
153
|
+
rescue => e
|
|
154
|
+
Funlog.instance.fancyinfo_warning("stash 还原时发生冲突: #{e.message}")
|
|
155
|
+
Funlog.instance.fancyinfo_warning("stash 已保留,请手动执行 'git stash pop' 处理冲突")
|
|
141
156
|
end
|
|
142
157
|
|
|
143
158
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'pindo/module/task/model/unity_task'
|
|
2
2
|
require 'pindo/base/funlog'
|
|
3
|
+
require 'json'
|
|
3
4
|
|
|
4
5
|
module Pindo
|
|
5
6
|
module TaskSystem
|
|
@@ -18,6 +19,7 @@ module Pindo
|
|
|
18
19
|
# - :project_path [String] Unity 项目路径(默认当前目录)
|
|
19
20
|
# - :export_path [String] 导出路径(nil 表示自动检测)
|
|
20
21
|
# - :deploy_mode [String] 部署模式 ('dev', 'adhoc', 'release'),默认 'dev'
|
|
22
|
+
# - :app_config [Hash] 需写入工程 AppConfig 的键值(传入则在导出前先改写 AppConfig)
|
|
21
23
|
# - :context [Hash] 上下文信息(如 index_count)
|
|
22
24
|
def initialize(platform, options = {})
|
|
23
25
|
@platform = platform
|
|
@@ -26,6 +28,7 @@ module Pindo
|
|
|
26
28
|
@index_count = options[:context] ? options[:context][:index_count] : nil
|
|
27
29
|
@export_path = options[:export_path] # 可由参数传入,nil 表示自动检测
|
|
28
30
|
@deploy_mode = options[:deploy_mode] || 'dev' # 部署模式:dev/adhoc/release,默认 dev
|
|
31
|
+
@app_config = options[:app_config] # 需写入 AppConfig 的键值,nil/空表示跳过
|
|
29
32
|
|
|
30
33
|
name = case platform
|
|
31
34
|
when 'ios', 'ipa'
|
|
@@ -57,6 +60,9 @@ module Pindo
|
|
|
57
60
|
protected
|
|
58
61
|
|
|
59
62
|
def do_work
|
|
63
|
+
# 若传入了配置参数,先改写工程内的 AppConfig
|
|
64
|
+
update_app_config_file if @app_config && !@app_config.empty?
|
|
65
|
+
|
|
60
66
|
# 获取 Unity 版本
|
|
61
67
|
project_unity_version = get_unity_version
|
|
62
68
|
|
|
@@ -106,6 +112,44 @@ module Pindo
|
|
|
106
112
|
|
|
107
113
|
private
|
|
108
114
|
|
|
115
|
+
# 使用传入的配置改写 Unity 工程内的 AppConfig(TextAsset JSON)
|
|
116
|
+
# 仅覆盖 @app_config 中提供的键,其余键保持原值
|
|
117
|
+
def update_app_config_file
|
|
118
|
+
app_config_file = File.join(@unity_root_path, "Assets", "Resources", "AppConfig", "AppConfig.json")
|
|
119
|
+
unless File.exist?(app_config_file)
|
|
120
|
+
Funlog.warning("未找到 AppConfig 文件,跳过 AppConfig 更新:#{app_config_file}")
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
puts "更新 AppConfig: #{app_config_file}"
|
|
125
|
+
|
|
126
|
+
begin
|
|
127
|
+
app_config_json = JSON.parse(File.read(app_config_file))
|
|
128
|
+
rescue JSON::ParserError => e
|
|
129
|
+
Funlog.warning("AppConfig 不是合法 JSON,跳过更新:#{e.message}")
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
changed = false
|
|
134
|
+
@app_config.each do |key, value|
|
|
135
|
+
next if value.nil? || value.to_s.strip.empty?
|
|
136
|
+
new_value = value.to_s.strip
|
|
137
|
+
old_value = app_config_json[key]
|
|
138
|
+
next if old_value.to_s == new_value
|
|
139
|
+
|
|
140
|
+
app_config_json[key] = new_value
|
|
141
|
+
puts " ✓ #{key}: #{old_value} → #{new_value}"
|
|
142
|
+
changed = true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
if changed
|
|
146
|
+
File.open(app_config_file, "w") { |f| f.write(JSON.pretty_generate(app_config_json)) }
|
|
147
|
+
puts "✅ AppConfig 更新完成\n"
|
|
148
|
+
else
|
|
149
|
+
puts "AppConfig 无需更新(值一致)\n"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
109
153
|
# 导出 iOS
|
|
110
154
|
def export_ios
|
|
111
155
|
platform = 'iOS'
|
|
@@ -41,12 +41,57 @@ module Pindo
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# 按依赖拓扑排序展示真实执行顺序
|
|
45
|
+
ordered = topological_execution_order(@queue.pending_snapshot)
|
|
46
|
+
is_serial = strategy.respond_to?(:name) && strategy.name.include?("串行")
|
|
47
|
+
puts "\e[34m"
|
|
48
|
+
if is_serial
|
|
49
|
+
puts "\e[34m" + " 实际执行顺序:"
|
|
50
|
+
else
|
|
51
|
+
puts "\e[34m" + " 执行顺序预览(并发模式下实际顺序受运行时调度影响):"
|
|
52
|
+
end
|
|
53
|
+
ordered.each_with_index do |task, index|
|
|
54
|
+
puts "\e[34m" + " #{index + 1}. #{task.name}"
|
|
55
|
+
end
|
|
56
|
+
|
|
44
57
|
puts "\e[34m"
|
|
45
58
|
puts "\e[34m" + " 总计: #{@queue.pending_count} 个任务"
|
|
46
59
|
puts "\e[34m" + "=" * 60 + "\e[0m"
|
|
47
60
|
puts "\n"
|
|
48
61
|
end
|
|
49
62
|
|
|
63
|
+
# 按依赖关系生成执行顺序(贪心拓扑排序,模拟串行调度:
|
|
64
|
+
# 每轮取「入队顺序中第一个依赖已满足」的任务),与串行实际执行顺序一致
|
|
65
|
+
# @param tasks [Array<PindoTask>] 待排序任务(入队顺序)
|
|
66
|
+
# @return [Array<PindoTask>] 按执行顺序排列的任务
|
|
67
|
+
def topological_execution_order(tasks)
|
|
68
|
+
all_ids = tasks.map(&:id)
|
|
69
|
+
remaining = tasks.dup
|
|
70
|
+
completed_ids = []
|
|
71
|
+
ordered = []
|
|
72
|
+
|
|
73
|
+
until remaining.empty?
|
|
74
|
+
index = remaining.index do |task|
|
|
75
|
+
task.dependencies.all? do |dep_id|
|
|
76
|
+
# 依赖已完成,或依赖不在本批任务中(视为已提前完成)
|
|
77
|
+
completed_ids.include?(dep_id) || !all_ids.include?(dep_id)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 存在循环依赖或依赖缺失,剩余任务按原顺序追加并结束
|
|
82
|
+
if index.nil?
|
|
83
|
+
ordered.concat(remaining)
|
|
84
|
+
break
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
task = remaining.delete_at(index)
|
|
88
|
+
completed_ids << task.id
|
|
89
|
+
ordered << task
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
ordered
|
|
93
|
+
end
|
|
94
|
+
|
|
50
95
|
# 输出任务执行标题
|
|
51
96
|
# @param task [PindoTask] 任务对象
|
|
52
97
|
def print_task_title(task)
|
|
@@ -224,41 +224,29 @@ module Pindo
|
|
|
224
224
|
end
|
|
225
225
|
end
|
|
226
226
|
|
|
227
|
-
# 获取Build
|
|
227
|
+
# 获取Build号(基于 git 提交总数,单调递增)
|
|
228
|
+
# 对标 fastlane number_of_commits:versionCode = git rev-list --count HEAD
|
|
228
229
|
# @param project_dir [String] 项目目录路径
|
|
229
|
-
# @return [Integer, String]
|
|
230
|
-
# - build_number:
|
|
231
|
-
# - commit_hash: 完整的commit hash(40位)
|
|
230
|
+
# @return [Array(Integer, String)] build_number 和完整 commit_hash
|
|
231
|
+
# - build_number: 当前 HEAD 的 commit 总数(最小为 1)
|
|
232
|
+
# - commit_hash: 完整的 commit hash(40位)
|
|
232
233
|
def get_build_number_from_commit(project_dir: nil)
|
|
233
234
|
raise ArgumentError, "项目目录不能为空" if project_dir.nil?
|
|
234
235
|
|
|
235
|
-
# 获取git根目录
|
|
236
236
|
git_root = Pindo::GitHandler.git_root_directory(local_repo_dir: project_dir)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
begin
|
|
240
|
-
# 获取当前HEAD的完整commit hash
|
|
241
|
-
full_commit_hash = Pindo::GitHandler.git!(%W(-C #{git_root} rev-parse HEAD)).strip
|
|
242
|
-
|
|
243
|
-
# 取前6位计算build_number
|
|
244
|
-
short_hash = full_commit_hash[0..5]
|
|
237
|
+
raise Pindo::Informative, "非 Git 仓库,无法计算 versionCode: #{project_dir}" unless git_root
|
|
245
238
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
build_number = build_number % max_value if build_number > max_value
|
|
239
|
+
# shallow clone 下 rev-list --count 结果不完整,必须先 unshallow
|
|
240
|
+
shallow = Pindo::GitHandler.git!(%W(-C #{git_root} rev-parse --is-shallow-repository)).strip
|
|
241
|
+
if shallow == "true"
|
|
242
|
+
raise Pindo::Informative, "shallow clone 无法计算 versionCode,请执行 git fetch --unshallow 后重试"
|
|
243
|
+
end
|
|
252
244
|
|
|
253
|
-
|
|
254
|
-
|
|
245
|
+
count = Pindo::GitHandler.git!(%W(-C #{git_root} rev-list --count HEAD)).strip.to_i
|
|
246
|
+
full_commit_hash = Pindo::GitHandler.git!(%W(-C #{git_root} rev-parse HEAD)).strip
|
|
255
247
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
Funlog.instance.fancyinfo_error("获取commit hash失败: #{e.message}")
|
|
259
|
-
# 如果获取失败,返回基于时间戳的默认值
|
|
260
|
-
return Time.now.to_i % 1000000 + 1, nil
|
|
261
|
-
end
|
|
248
|
+
build_number = count < 1 ? 1 : count
|
|
249
|
+
[build_number, full_commit_hash]
|
|
262
250
|
end
|
|
263
251
|
|
|
264
252
|
|
|
@@ -364,7 +352,7 @@ module Pindo
|
|
|
364
352
|
if local_exists && Pindo::GitHandler.is_tag_at_head?(git_root_dir: project_dir, tag_name: tag_name)
|
|
365
353
|
if !remote_exists
|
|
366
354
|
# 本地存在且在 HEAD,但远程不存在,推送到远程
|
|
367
|
-
|
|
355
|
+
push_tag_with_verify(project_dir: project_dir, tag_name: tag_name)
|
|
368
356
|
Funlog.instance.fancyinfo_success("推送 Tag 到远程: #{tag_name}")
|
|
369
357
|
else
|
|
370
358
|
Funlog.instance.fancyinfo_success("Tag 已存在且在 HEAD: #{tag_name}")
|
|
@@ -386,10 +374,48 @@ module Pindo
|
|
|
386
374
|
|
|
387
375
|
# 创建并推送 tag
|
|
388
376
|
Pindo::GitHandler.git!(%W(-C #{project_dir} tag #{tag_name}))
|
|
389
|
-
|
|
377
|
+
push_tag_with_verify(project_dir: project_dir, tag_name: tag_name)
|
|
390
378
|
Funlog.instance.fancyinfo_success("创建并推送 Tag: #{tag_name}")
|
|
391
379
|
tag_name
|
|
392
380
|
end
|
|
393
381
|
|
|
382
|
+
# 推送 tag 到远程并校验是否真正推送成功,失败则重试
|
|
383
|
+
# 校验方式为「远程 tag 指向的 commit == 本地 tag commit」,
|
|
384
|
+
# 一次 ls-remote 同时覆盖「存在性」与「指向正确性」
|
|
385
|
+
# @param project_dir [String] 仓库目录
|
|
386
|
+
# @param tag_name [String] tag 名称
|
|
387
|
+
# @param max_retries [Integer] 最大重试次数(默认 3)
|
|
388
|
+
# @return [Boolean] 是否推送成功(失败到达上限会 raise)
|
|
389
|
+
def push_tag_with_verify(project_dir:, tag_name:, max_retries: 3)
|
|
390
|
+
local_commit = Pindo::GitHandler.git!(%W(-C #{project_dir} rev-parse #{tag_name}^{commit})).strip
|
|
391
|
+
|
|
392
|
+
attempt = 0
|
|
393
|
+
loop do
|
|
394
|
+
attempt += 1
|
|
395
|
+
begin
|
|
396
|
+
Pindo::GitHandler.git_remote!(project_dir, %W(-C #{project_dir} push origin #{tag_name}))
|
|
397
|
+
rescue => e
|
|
398
|
+
first_line = e.message.lines.first&.strip
|
|
399
|
+
Funlog.instance.fancyinfo_warning("推送 Tag 失败(第 #{attempt}/#{max_retries} 次): #{first_line}")
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# 校验远程 tag 指向的 commit 是否与本地一致,确认推送成功
|
|
403
|
+
remote_commit = Pindo::GitHandler.remote_tag_commit(local_repo_dir: project_dir, tag_name: tag_name)
|
|
404
|
+
if remote_commit && remote_commit == local_commit
|
|
405
|
+
Funlog.instance.fancyinfo_success("已确认 Tag 推送到远程: #{tag_name}")
|
|
406
|
+
return true
|
|
407
|
+
elsif remote_commit
|
|
408
|
+
Funlog.instance.fancyinfo_warning("远程 Tag 指向不同 commit(远程 #{remote_commit[0..7]} ≠ 本地 #{local_commit[0..7]}),疑似残留旧 Tag: #{tag_name}")
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
if attempt >= max_retries
|
|
412
|
+
raise Informative, "Tag 推送失败,已重试 #{max_retries} 次仍未在远程确认: #{tag_name}"
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
Funlog.instance.fancyinfo_update("远程未确认 Tag,#{attempt}s 后重试推送: #{tag_name}")
|
|
416
|
+
sleep(attempt)
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
394
420
|
end
|
|
395
421
|
end
|
|
@@ -12,13 +12,13 @@ module Pindo
|
|
|
12
12
|
@project_obj = Xcodeproj::Project.open(proj_fullname)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 环境配置目录
|