appship 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/exe/appship +6 -0
- data/lib/appship/badger.rb +175 -0
- data/lib/appship/builder.rb +295 -0
- data/lib/appship/cache.rb +207 -0
- data/lib/appship/cli.rb +800 -0
- data/lib/appship/config.rb +75 -0
- data/lib/appship/errors.rb +8 -0
- data/lib/appship/ipa.rb +51 -0
- data/lib/appship/runner.rb +55 -0
- data/lib/appship/uploader.rb +490 -0
- data/lib/appship/version.rb +5 -0
- data/lib/appship.rb +25 -0
- data/test/test_cache.rb +146 -0
- data/test/test_cli.rb +22 -0
- data/test/test_config.rb +22 -0
- data/test/test_fir_uploader.rb +93 -0
- data/test/test_multipart_body.rb +33 -0
- metadata +61 -0
data/lib/appship/cli.rb
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appship
|
|
4
|
+
class CLI
|
|
5
|
+
def self.start(argv)
|
|
6
|
+
new.start(argv)
|
|
7
|
+
rescue Error => e
|
|
8
|
+
warn "❌ #{e.message}"
|
|
9
|
+
exit 1
|
|
10
|
+
rescue OptionParser::ParseError => e
|
|
11
|
+
warn "❌ #{e.message}"
|
|
12
|
+
exit 2
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def start(argv)
|
|
16
|
+
args = argv.dup
|
|
17
|
+
global = extract_global_options!(args)
|
|
18
|
+
command = args.shift
|
|
19
|
+
|
|
20
|
+
case command
|
|
21
|
+
when "init"
|
|
22
|
+
init(args)
|
|
23
|
+
when "doctor"
|
|
24
|
+
doctor
|
|
25
|
+
when "build"
|
|
26
|
+
build(args, global)
|
|
27
|
+
when "package"
|
|
28
|
+
package(args, global)
|
|
29
|
+
when "upload"
|
|
30
|
+
upload(args, global)
|
|
31
|
+
when "pgyer"
|
|
32
|
+
pgyer(args, global)
|
|
33
|
+
when "fir"
|
|
34
|
+
fir(args, global)
|
|
35
|
+
when "version", "--version", "-v"
|
|
36
|
+
puts "appship #{VERSION}"
|
|
37
|
+
when nil, "help", "--help", "-h"
|
|
38
|
+
puts help
|
|
39
|
+
else
|
|
40
|
+
raise ConfigurationError, "未知命令: #{command}\n\n#{help}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def extract_global_options!(args)
|
|
47
|
+
config_path = nil
|
|
48
|
+
verbose = false
|
|
49
|
+
loop do
|
|
50
|
+
case args.first
|
|
51
|
+
when "--verbose"
|
|
52
|
+
verbose = true
|
|
53
|
+
args.shift
|
|
54
|
+
when "--config"
|
|
55
|
+
args.shift
|
|
56
|
+
config_path = args.shift || raise(OptionParser::MissingArgument, "--config")
|
|
57
|
+
when ->(value) { value&.start_with?("--config=") }
|
|
58
|
+
config_path = args.shift.split("=", 2).last
|
|
59
|
+
else
|
|
60
|
+
break
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
{ config_path: config_path, verbose: verbose }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize_provider_short_options!(args, provider)
|
|
67
|
+
key_option = provider.to_s == "fir" ? "--fir-api-key" : "--api-key"
|
|
68
|
+
password_option = provider.to_s == "fir" ? "--fir-password" : "--password"
|
|
69
|
+
args.map! do |arg|
|
|
70
|
+
case arg
|
|
71
|
+
when "-key" then key_option
|
|
72
|
+
when "-password" then password_option
|
|
73
|
+
else
|
|
74
|
+
arg = "#{key_option}=#{arg.delete_prefix("-key=")}" if arg.start_with?("-key=")
|
|
75
|
+
arg = "#{password_option}=#{arg.delete_prefix("-password=")}" if arg.start_with?("-password=")
|
|
76
|
+
arg
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def init(args)
|
|
82
|
+
path = File.join(Dir.pwd, ".appship.yml")
|
|
83
|
+
force = false
|
|
84
|
+
parser = OptionParser.new do |opts|
|
|
85
|
+
opts.banner = "用法: appship init [选项]"
|
|
86
|
+
opts.on("--path PATH", "配置文件路径(默认 .appship.yml)") { |value| path = value }
|
|
87
|
+
opts.on("--force", "覆盖已有配置文件") { force = true }
|
|
88
|
+
opts.on("-h", "--help", "显示帮助") { puts opts; exit 0 }
|
|
89
|
+
end
|
|
90
|
+
parser.parse!(args)
|
|
91
|
+
puts "✅ 已创建配置文件: #{Config.write_template(path, force: force)}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def doctor
|
|
95
|
+
checks = {
|
|
96
|
+
"ruby" => "运行 CLI",
|
|
97
|
+
"xcodebuild" => "归档和导出",
|
|
98
|
+
"xcrun" => "actool 和代码工具",
|
|
99
|
+
"codesign" => "重签名角标后的 App",
|
|
100
|
+
"security" => "读取代码签名身份",
|
|
101
|
+
"zip" => "生成 IPA",
|
|
102
|
+
"unzip" => "修改已有 IPA",
|
|
103
|
+
"magick/convert" => "生成图标角标"
|
|
104
|
+
}
|
|
105
|
+
puts "appship doctor"
|
|
106
|
+
checks.each do |command, purpose|
|
|
107
|
+
available = if command == "magick/convert"
|
|
108
|
+
Runner.which("magick") || Runner.which("convert")
|
|
109
|
+
else
|
|
110
|
+
Runner.which(command)
|
|
111
|
+
end
|
|
112
|
+
puts "#{available ? "✅" : "⚠️"} #{command.ljust(16)} #{available || "未安装"} - #{purpose}"
|
|
113
|
+
end
|
|
114
|
+
config = Config.find
|
|
115
|
+
puts "#{config.path ? "✅" : "ℹ️"} 配置文件 #{config.path || "当前目录没有 .appship.yml"}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build(args, global)
|
|
119
|
+
config = Config.find(global[:config_path])
|
|
120
|
+
options = build_options(config)
|
|
121
|
+
parser = build_parser(options)
|
|
122
|
+
parser.parse!(args)
|
|
123
|
+
Builder.new(runner: Runner.new(verbose: global[:verbose])).build!(options)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def package(args, global)
|
|
127
|
+
config = Config.find(global[:config_path])
|
|
128
|
+
options = upload_options(config).merge(
|
|
129
|
+
app: nil,
|
|
130
|
+
output: nil,
|
|
131
|
+
badge: config.get("badge"),
|
|
132
|
+
assets: config.get("assets"),
|
|
133
|
+
app_icon: config.get("app_icon", default: "AppIcon")
|
|
134
|
+
)
|
|
135
|
+
parser = package_parser(options)
|
|
136
|
+
parser.parse!(args)
|
|
137
|
+
Builder.new(runner: Runner.new(verbose: global[:verbose])).package!(options)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def upload(args, global)
|
|
141
|
+
config = Config.find(global[:config_path])
|
|
142
|
+
options = upload_options(config).merge(file: nil)
|
|
143
|
+
parser = upload_parser(options)
|
|
144
|
+
parser.parse!(args)
|
|
145
|
+
options[:file] ||= args.shift
|
|
146
|
+
raise ConfigurationError, "请指定 IPA/APK 文件路径" unless options[:file]
|
|
147
|
+
raise ConfigurationError, "upload 不接受多余参数: #{args.join(" ")}" unless args.empty?
|
|
148
|
+
|
|
149
|
+
Builder.new(runner: Runner.new(verbose: global[:verbose])).upload!(options)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def pgyer(args, global)
|
|
153
|
+
options = {
|
|
154
|
+
project_dir: Dir.pwd,
|
|
155
|
+
env_file: ENV.fetch("APPSHIP_ENV_FILE", Cache::DEFAULT_PATH),
|
|
156
|
+
project_name: nil,
|
|
157
|
+
app_name: nil,
|
|
158
|
+
workspace: nil,
|
|
159
|
+
project: nil,
|
|
160
|
+
scheme: nil,
|
|
161
|
+
configuration: nil,
|
|
162
|
+
configuration_explicit: false,
|
|
163
|
+
sdk: "iphoneos",
|
|
164
|
+
output: nil,
|
|
165
|
+
archive_path: nil,
|
|
166
|
+
export_path: nil,
|
|
167
|
+
export_options_plist: nil,
|
|
168
|
+
export_method: "development",
|
|
169
|
+
signing_style: "automatic",
|
|
170
|
+
team_id: nil,
|
|
171
|
+
build_mode: "rebuild",
|
|
172
|
+
build_mode_explicit: false,
|
|
173
|
+
badge: nil,
|
|
174
|
+
badge_explicit: false,
|
|
175
|
+
assets: nil,
|
|
176
|
+
app_icon: "AppIcon",
|
|
177
|
+
upload: true,
|
|
178
|
+
api_key: nil,
|
|
179
|
+
api_key_env: "PGYER_API_KEY",
|
|
180
|
+
install_type: nil,
|
|
181
|
+
password: nil,
|
|
182
|
+
password_env: nil,
|
|
183
|
+
description: nil,
|
|
184
|
+
description_explicit: false,
|
|
185
|
+
channel: nil,
|
|
186
|
+
poll_attempts: 60,
|
|
187
|
+
clean: true,
|
|
188
|
+
keep_temporary: false,
|
|
189
|
+
allow_provisioning_updates: true,
|
|
190
|
+
skip_package_plugin_validation: true,
|
|
191
|
+
skip_macro_validation: true
|
|
192
|
+
}
|
|
193
|
+
parser = pgyer_parser(options)
|
|
194
|
+
normalize_provider_short_options!(args, "pgyer")
|
|
195
|
+
parser.parse!(args)
|
|
196
|
+
raise ConfigurationError, "pgyer 不接受多余参数: #{args.join(" ")}" unless args.empty?
|
|
197
|
+
|
|
198
|
+
project_dir = File.expand_path(options[:project_dir])
|
|
199
|
+
raise ConfigurationError, "项目目录不存在: #{project_dir}" unless File.directory?(project_dir)
|
|
200
|
+
env_file = File.expand_path(options[:env_file], Dir.pwd)
|
|
201
|
+
|
|
202
|
+
Dir.chdir(project_dir) do
|
|
203
|
+
cache = Cache.load(env_file, project_dir: project_dir, interactive: true, provider: "pgyer")
|
|
204
|
+
profile = cache.resolve(
|
|
205
|
+
project_name: options[:project_name],
|
|
206
|
+
project_dir: project_dir,
|
|
207
|
+
provider: "pgyer",
|
|
208
|
+
credential_overrides: { "pgyer_api_key" => options[:api_key], "pgyer_password" => options[:password] }.compact
|
|
209
|
+
)
|
|
210
|
+
apply_cached_profile!(options, profile, provider: "pgyer")
|
|
211
|
+
infer_pgyer_target!(options)
|
|
212
|
+
options[:output] ||= File.join(Cache::DEFAULT_BUILD_DIR, "#{options[:app_name]}.ipa")
|
|
213
|
+
|
|
214
|
+
runner = Runner.new(verbose: global[:verbose])
|
|
215
|
+
builder = Builder.new(runner: runner)
|
|
216
|
+
prepare_pgyer_flow!(options, builder)
|
|
217
|
+
if options[:build_mode] == "cache"
|
|
218
|
+
options[:app] ||= builder.find_cached_app!(options)
|
|
219
|
+
builder.package!(options)
|
|
220
|
+
else
|
|
221
|
+
builder.build!(options)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def fir(args, global)
|
|
227
|
+
options = {
|
|
228
|
+
project_dir: Dir.pwd,
|
|
229
|
+
env_file: ENV.fetch("APPSHIP_ENV_FILE", Cache::DEFAULT_PATH),
|
|
230
|
+
project_name: nil,
|
|
231
|
+
app_name: nil,
|
|
232
|
+
workspace: nil,
|
|
233
|
+
project: nil,
|
|
234
|
+
scheme: nil,
|
|
235
|
+
configuration: nil,
|
|
236
|
+
configuration_explicit: false,
|
|
237
|
+
sdk: "iphoneos",
|
|
238
|
+
output: nil,
|
|
239
|
+
archive_path: nil,
|
|
240
|
+
export_path: nil,
|
|
241
|
+
export_options_plist: nil,
|
|
242
|
+
export_method: "adhoc",
|
|
243
|
+
signing_style: "automatic",
|
|
244
|
+
team_id: nil,
|
|
245
|
+
build_mode: "rebuild",
|
|
246
|
+
build_mode_explicit: false,
|
|
247
|
+
badge: nil,
|
|
248
|
+
badge_explicit: false,
|
|
249
|
+
assets: nil,
|
|
250
|
+
app_icon: "AppIcon",
|
|
251
|
+
upload: true,
|
|
252
|
+
provider: "fir",
|
|
253
|
+
fir_api_token: nil,
|
|
254
|
+
fir_api_token_env: "FIR_API_TOKEN",
|
|
255
|
+
fir_password: nil,
|
|
256
|
+
fir_password_env: "FIR_PASSWORD",
|
|
257
|
+
bundle_id: nil,
|
|
258
|
+
app_version: nil,
|
|
259
|
+
build_number: nil,
|
|
260
|
+
release_type: "Adhoc",
|
|
261
|
+
icon: nil,
|
|
262
|
+
description: nil,
|
|
263
|
+
description_explicit: false,
|
|
264
|
+
clean: true,
|
|
265
|
+
keep_temporary: false,
|
|
266
|
+
allow_provisioning_updates: true,
|
|
267
|
+
skip_package_plugin_validation: true,
|
|
268
|
+
skip_macro_validation: true
|
|
269
|
+
}
|
|
270
|
+
parser = fir_parser(options)
|
|
271
|
+
normalize_provider_short_options!(args, "fir")
|
|
272
|
+
parser.parse!(args)
|
|
273
|
+
raise ConfigurationError, "fir 不接受多余参数: #{args.join(" ")}" unless args.empty?
|
|
274
|
+
|
|
275
|
+
project_dir = File.expand_path(options[:project_dir])
|
|
276
|
+
raise ConfigurationError, "项目目录不存在: #{project_dir}" unless File.directory?(project_dir)
|
|
277
|
+
env_file = File.expand_path(options[:env_file], Dir.pwd)
|
|
278
|
+
|
|
279
|
+
Dir.chdir(project_dir) do
|
|
280
|
+
cache = Cache.load(env_file, project_dir: project_dir, interactive: true, provider: "fir")
|
|
281
|
+
profile = cache.resolve(
|
|
282
|
+
project_name: options[:project_name],
|
|
283
|
+
project_dir: project_dir,
|
|
284
|
+
provider: "fir",
|
|
285
|
+
credential_overrides: { "fir_api_key" => options[:fir_api_token], "fir_password" => options[:fir_password] }.compact
|
|
286
|
+
)
|
|
287
|
+
apply_cached_profile!(options, profile, provider: "fir")
|
|
288
|
+
infer_pgyer_target!(options)
|
|
289
|
+
options[:output] ||= File.join(Cache::DEFAULT_BUILD_DIR, "#{options[:app_name]}.ipa")
|
|
290
|
+
|
|
291
|
+
runner = Runner.new(verbose: global[:verbose])
|
|
292
|
+
builder = Builder.new(runner: runner)
|
|
293
|
+
prepare_pgyer_flow!(options, builder)
|
|
294
|
+
if options[:build_mode] == "cache"
|
|
295
|
+
options[:app] ||= builder.find_cached_app!(options)
|
|
296
|
+
builder.package!(options)
|
|
297
|
+
else
|
|
298
|
+
builder.build!(options)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def apply_cached_profile!(options, profile, provider: "pgyer")
|
|
304
|
+
options[:project_name] ||= profile["project_name"]
|
|
305
|
+
options[:app_name] ||= profile["app_name"]
|
|
306
|
+
if provider.to_s == "fir"
|
|
307
|
+
options[:fir_api_token] ||= profile["fir_api_key"] || profile["fir_api_token"]
|
|
308
|
+
options[:fir_password] ||= profile["fir_password"]
|
|
309
|
+
options[:fir_password] ||= ENV[options[:fir_password_env]] if options[:fir_password_env]
|
|
310
|
+
options[:bundle_id] ||= profile["bundle_id"]
|
|
311
|
+
options[:app_version] ||= profile["app_version"]
|
|
312
|
+
options[:build_number] ||= profile["build_number"]
|
|
313
|
+
options[:release_type] ||= profile["fir_release_type"] || "Adhoc"
|
|
314
|
+
options[:description] ||= profile["fir_update_description"]
|
|
315
|
+
else
|
|
316
|
+
options[:api_key] ||= profile["pgyer_api_key"]
|
|
317
|
+
end
|
|
318
|
+
options[:workspace] ||= profile["workspace"]
|
|
319
|
+
options[:project] ||= profile["project"]
|
|
320
|
+
options[:scheme] ||= profile["scheme"] || options[:project_name]
|
|
321
|
+
options[:configuration] ||= profile["configuration"] || "Debug"
|
|
322
|
+
options[:assets] ||= profile["assets"]
|
|
323
|
+
options[:app_icon] ||= profile["app_icon"] || "AppIcon"
|
|
324
|
+
options[:badge] ||= profile["badge"]
|
|
325
|
+
if provider.to_s != "fir"
|
|
326
|
+
options[:password_env] ||= profile["pgyer_password_env"]
|
|
327
|
+
options[:password] ||= profile["pgyer_password"]
|
|
328
|
+
options[:password] ||= ENV[options[:password_env]] if options[:password_env]
|
|
329
|
+
options[:password] ||= ENV["PGYER_INSTALL_PASSWORD"]
|
|
330
|
+
options[:install_type] ||= profile["pgyer_install_type"] || 2
|
|
331
|
+
if options[:password].to_s.empty? && options[:install_type].to_i == 2
|
|
332
|
+
options[:install_type] = 1
|
|
333
|
+
puts "ℹ️ pgyer_password 为空,将使用公开安装模式(不设置密码)"
|
|
334
|
+
end
|
|
335
|
+
options[:description] ||= profile["pgyer_update_description"]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
raise ConfigurationError, "缓存缺少 project_name" if options[:project_name].to_s.empty?
|
|
339
|
+
raise ConfigurationError, "缓存缺少 app_name" if options[:app_name].to_s.empty?
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def infer_pgyer_target!(options)
|
|
343
|
+
return if options[:workspace] || options[:project]
|
|
344
|
+
|
|
345
|
+
workspace = "#{options[:project_name]}.xcworkspace"
|
|
346
|
+
project = "#{options[:project_name]}.xcodeproj"
|
|
347
|
+
if File.exist?(workspace)
|
|
348
|
+
options[:workspace] = workspace
|
|
349
|
+
elsif File.exist?(project)
|
|
350
|
+
options[:project] = project
|
|
351
|
+
else
|
|
352
|
+
candidates = Dir["*.xcworkspace", "*.xcodeproj"]
|
|
353
|
+
if candidates.length == 1
|
|
354
|
+
options[File.extname(candidates.first) == ".xcworkspace" ? :workspace : :project] = candidates.first
|
|
355
|
+
else
|
|
356
|
+
raise ConfigurationError, "找不到 #{options[:project_name]}.xcworkspace 或 #{options[:project_name]}.xcodeproj"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
if options[:assets].to_s.empty?
|
|
361
|
+
icon_set = Dir["**/AppIcon.appiconset"].reject { |path| path.include?("Pods/") || path.include?("build/") || path.include?("DerivedData/") }.first
|
|
362
|
+
options[:assets] = File.dirname(icon_set) if icon_set
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def default_description(options)
|
|
367
|
+
branch = git_value(["git", "rev-parse", "--abbrev-ref", "HEAD"], "unknown")
|
|
368
|
+
user = git_value(["git", "config", "user.name"], "unknown")
|
|
369
|
+
content = options[:interactive_content].to_s
|
|
370
|
+
"自动编译打包 | 分支:#{branch} | 构建人:#{user} | 配置:#{options[:configuration]} | 更新内容:#{content}"
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def prepare_pgyer_flow!(options, builder)
|
|
374
|
+
interactive = $stdin.tty? && $stdout.tty?
|
|
375
|
+
unless options[:build_mode_explicit]
|
|
376
|
+
options[:build_mode] = choose_build_mode if interactive
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
if options[:build_mode] == "rebuild" && !options[:configuration_explicit] && interactive
|
|
380
|
+
options[:configuration] = choose_configuration
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
if options[:build_mode] == "cache"
|
|
384
|
+
options[:badge] ||= badge_text(options[:configuration]) unless options[:badge_explicit]
|
|
385
|
+
elsif !options[:badge_explicit] && interactive
|
|
386
|
+
options[:badge] = choose_badge ? badge_text(options[:configuration]) : nil
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
unless options[:description_explicit] || !interactive
|
|
390
|
+
print "请输入更新内容(输入完回车):"
|
|
391
|
+
options[:interactive_content] = $stdin.gets.to_s.chomp
|
|
392
|
+
puts "✅ 更新内容: #{options[:interactive_content]}"
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
if options[:build_mode] == "cache"
|
|
396
|
+
begin
|
|
397
|
+
puts ""
|
|
398
|
+
puts "📦 使用缓存模式"
|
|
399
|
+
options[:app] ||= with_spinner("正在查找编译缓存中的 .app 文件") do
|
|
400
|
+
builder.find_cached_app!(options)
|
|
401
|
+
end
|
|
402
|
+
puts "✅ 找到缓存 App: #{options[:app]}"
|
|
403
|
+
rescue CommandError => e
|
|
404
|
+
puts "⚠️ #{e.message},将切换到重新构建模式"
|
|
405
|
+
options[:build_mode] = "rebuild"
|
|
406
|
+
options[:app] = nil
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
options[:description] ||= default_description(options)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def with_spinner(message)
|
|
414
|
+
return yield unless $stdout.tty?
|
|
415
|
+
|
|
416
|
+
frames = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏]
|
|
417
|
+
finished = false
|
|
418
|
+
spinner = Thread.new do
|
|
419
|
+
index = 0
|
|
420
|
+
until finished
|
|
421
|
+
print "\r#{frames[index % frames.length]} #{message}"
|
|
422
|
+
$stdout.flush
|
|
423
|
+
index += 1
|
|
424
|
+
sleep 0.12
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
yield
|
|
429
|
+
ensure
|
|
430
|
+
finished = true
|
|
431
|
+
spinner&.join
|
|
432
|
+
if $stdout.tty?
|
|
433
|
+
print "\r\e[2K"
|
|
434
|
+
$stdout.flush
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def choose_build_mode
|
|
439
|
+
choices = [
|
|
440
|
+
"使用缓存(从 DerivedData 读取已有的 .app,快速打包)",
|
|
441
|
+
"重新构建(archive + export 导出已签名 IPA)"
|
|
442
|
+
]
|
|
443
|
+
selected = gum_choose("请选择构建方式:", choices)
|
|
444
|
+
if selected == choices[0]
|
|
445
|
+
puts "✅ 已选择构建方式: 使用缓存"
|
|
446
|
+
return "cache"
|
|
447
|
+
end
|
|
448
|
+
if selected == choices[1]
|
|
449
|
+
puts "✅ 已选择构建方式: 重新构建"
|
|
450
|
+
return "rebuild"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
puts ""
|
|
454
|
+
puts "请选择构建方式:"
|
|
455
|
+
puts "1) #{choices[0]}"
|
|
456
|
+
puts "2) #{choices[1]}"
|
|
457
|
+
print "请输入选项 [1/2,默认 2]: "
|
|
458
|
+
answer = $stdin.gets.to_s.strip
|
|
459
|
+
result = answer == "1" ? "cache" : "rebuild"
|
|
460
|
+
puts "✅ 已选择构建方式: #{result == "cache" ? "使用缓存" : "重新构建"}"
|
|
461
|
+
result
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def choose_badge
|
|
465
|
+
choices = [
|
|
466
|
+
"添加 Badge(显示环境标记)",
|
|
467
|
+
"不添加 Badge(使用原始图标)"
|
|
468
|
+
]
|
|
469
|
+
selected = gum_choose("请选择是否在 App Icon 上添加 Badge:", choices)
|
|
470
|
+
if selected == choices[0]
|
|
471
|
+
puts "✅ 已选择 Badge: 添加 Badge"
|
|
472
|
+
return true
|
|
473
|
+
end
|
|
474
|
+
if selected == choices[1]
|
|
475
|
+
puts "✅ 已选择 Badge: 不添加 Badge"
|
|
476
|
+
return false
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
puts ""
|
|
480
|
+
puts "请选择是否在 App Icon 上添加 Badge:"
|
|
481
|
+
puts "1) #{choices[0]}"
|
|
482
|
+
puts "2) #{choices[1]}"
|
|
483
|
+
print "请输入选项 [1/2,默认 2]: "
|
|
484
|
+
result = $stdin.gets.to_s.strip == "1"
|
|
485
|
+
puts "✅ 已选择 Badge: #{result ? "添加 Badge" : "不添加 Badge"}"
|
|
486
|
+
result
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def choose_configuration
|
|
490
|
+
choices = ["Debug", "Release"]
|
|
491
|
+
selected = gum_choose("请选择构建配置:", choices)
|
|
492
|
+
if choices.include?(selected)
|
|
493
|
+
puts "✅ 已选择构建配置: #{selected}"
|
|
494
|
+
return selected
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
puts ""
|
|
498
|
+
puts "请选择构建配置:"
|
|
499
|
+
puts "1) Debug"
|
|
500
|
+
puts "2) Release"
|
|
501
|
+
print "请输入选项 [1/2,默认 1]: "
|
|
502
|
+
result = $stdin.gets.to_s.strip == "2" ? "Release" : "Debug"
|
|
503
|
+
puts "✅ 已选择构建配置: #{result}"
|
|
504
|
+
result
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def gum_choose(header, choices)
|
|
508
|
+
gum = ensure_gum
|
|
509
|
+
return nil unless gum
|
|
510
|
+
|
|
511
|
+
puts ""
|
|
512
|
+
output = IO.popen([gum, "choose", "--header", header, *choices], "r", &:read)
|
|
513
|
+
selected = output.to_s.strip
|
|
514
|
+
selected unless selected.empty?
|
|
515
|
+
rescue Errno::ENOENT
|
|
516
|
+
warn "⚠️ gum 不可用,将使用普通终端选择"
|
|
517
|
+
nil
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def ensure_gum
|
|
521
|
+
existing = Runner.which("gum")
|
|
522
|
+
return existing if existing
|
|
523
|
+
|
|
524
|
+
brew = Runner.which("brew")
|
|
525
|
+
unless brew
|
|
526
|
+
warn "⚠️ 未找到 Homebrew,无法自动安装 gum,将使用普通终端选择"
|
|
527
|
+
return nil
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
puts "⚙️ gum 未安装,正在通过 Homebrew 安装..."
|
|
531
|
+
Runner.new.run!([brew, "install", "gum"])
|
|
532
|
+
installed = Runner.which("gum")
|
|
533
|
+
return installed if installed
|
|
534
|
+
|
|
535
|
+
warn "⚠️ gum 安装命令已完成,但仍找不到 gum,将使用普通终端选择"
|
|
536
|
+
nil
|
|
537
|
+
rescue Error => e
|
|
538
|
+
warn "⚠️ gum 安装失败:#{e.message}"
|
|
539
|
+
nil
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def badge_text(configuration)
|
|
543
|
+
configuration.to_s.casecmp("release").zero? ? "RELEASE" : "DEBUG"
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def git_value(argv, fallback)
|
|
547
|
+
output, status = Open3.capture2(*argv)
|
|
548
|
+
status.success? && !output.strip.empty? ? output.strip : fallback
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def build_options(config)
|
|
552
|
+
upload_options(config).merge(
|
|
553
|
+
workspace: config.get("workspace"),
|
|
554
|
+
project: config.get("project"),
|
|
555
|
+
scheme: config.get("scheme"),
|
|
556
|
+
configuration: config.get("configuration", default: "Debug"),
|
|
557
|
+
sdk: config.get("sdk", default: "iphoneos"),
|
|
558
|
+
output: config.get("output"),
|
|
559
|
+
archive_path: config.get("archive_path"),
|
|
560
|
+
export_path: config.get("export_path"),
|
|
561
|
+
export_options_plist: config.get("export_options_plist"),
|
|
562
|
+
export_method: config.get("export_method", default: "development"),
|
|
563
|
+
signing_style: config.get("signing_style", default: "automatic"),
|
|
564
|
+
team_id: config.get("team_id"),
|
|
565
|
+
badge: config.get("badge"),
|
|
566
|
+
assets: config.get("assets"),
|
|
567
|
+
app_icon: config.get("app_icon", default: "AppIcon"),
|
|
568
|
+
upload: false,
|
|
569
|
+
clean: true,
|
|
570
|
+
keep_temporary: false,
|
|
571
|
+
allow_provisioning_updates: true,
|
|
572
|
+
skip_package_plugin_validation: true,
|
|
573
|
+
skip_macro_validation: true
|
|
574
|
+
)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def upload_options(config)
|
|
578
|
+
{
|
|
579
|
+
provider: config.get("upload", "provider", default: "pgyer"),
|
|
580
|
+
api_key: nil,
|
|
581
|
+
api_key_env: config.get("upload", "api_key_env", default: "PGYER_API_KEY"),
|
|
582
|
+
fir_api_token: nil,
|
|
583
|
+
fir_api_token_env: config.get("upload", "fir_api_token_env", default: "FIR_API_TOKEN"),
|
|
584
|
+
fir_password: config.get("upload", "fir_password"),
|
|
585
|
+
fir_password_env: config.get("upload", "fir_password_env", default: "FIR_PASSWORD"),
|
|
586
|
+
bundle_id: config.get("upload", "bundle_id"),
|
|
587
|
+
app_name: config.get("upload", "app_name"),
|
|
588
|
+
app_version: config.get("upload", "app_version"),
|
|
589
|
+
build_number: config.get("upload", "build_number"),
|
|
590
|
+
release_type: config.get("upload", "release_type"),
|
|
591
|
+
icon: config.get("upload", "icon"),
|
|
592
|
+
install_type: config.get("upload", "install_type", default: 1),
|
|
593
|
+
password: nil,
|
|
594
|
+
password_env: config.get("upload", "password_env"),
|
|
595
|
+
description: config.get("upload", "description"),
|
|
596
|
+
install_date: config.get("upload", "install_date"),
|
|
597
|
+
start_date: config.get("upload", "start_date"),
|
|
598
|
+
end_date: config.get("upload", "end_date"),
|
|
599
|
+
channel: config.get("upload", "channel"),
|
|
600
|
+
poll_attempts: config.get("upload", "poll_attempts", default: 60),
|
|
601
|
+
upload: false
|
|
602
|
+
}
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def build_parser(options)
|
|
606
|
+
OptionParser.new do |opts|
|
|
607
|
+
opts.banner = "用法: appship [--config FILE] build [选项]"
|
|
608
|
+
opts.separator ""
|
|
609
|
+
opts.separator "工程与构建"
|
|
610
|
+
opts.on("--workspace PATH", "Xcode workspace 路径") { |value| options[:workspace] = value }
|
|
611
|
+
opts.on("--project PATH", "Xcode project 路径") { |value| options[:project] = value }
|
|
612
|
+
opts.on("--scheme NAME", "Scheme 名称") { |value| options[:scheme] = value }
|
|
613
|
+
opts.on("--configuration NAME", "构建配置(默认 Debug)") { |value| options[:configuration] = value }
|
|
614
|
+
opts.on("--sdk NAME", "SDK(默认 iphoneos)") { |value| options[:sdk] = value }
|
|
615
|
+
opts.on("--output PATH", "最终 IPA 路径") { |value| options[:output] = value }
|
|
616
|
+
opts.on("--archive-path PATH", "自定义 xcarchive 路径") { |value| options[:archive_path] = value }
|
|
617
|
+
opts.on("--export-path PATH", "自定义 export 目录") { |value| options[:export_path] = value }
|
|
618
|
+
opts.on("--export-options-plist PATH", "使用已有 ExportOptions.plist") { |value| options[:export_options_plist] = value }
|
|
619
|
+
opts.on("--export-method NAME", "development/adhoc/app-store 等") { |value| options[:export_method] = value }
|
|
620
|
+
opts.on("--team-id ID", "签名 Team ID") { |value| options[:team_id] = value }
|
|
621
|
+
opts.on("--no-clean", "不清理已有 archive/export 路径") { options[:clean] = false }
|
|
622
|
+
opts.on("--keep-temporary", "保留工具生成的临时目录") { options[:keep_temporary] = true }
|
|
623
|
+
opts.on("--no-allow-provisioning-updates", "不允许 xcodebuild 自动更新签名") { options[:allow_provisioning_updates] = false }
|
|
624
|
+
opts.separator ""
|
|
625
|
+
opts.separator "图标角标"
|
|
626
|
+
opts.on("--badge TEXT", "添加角标,例如 DEBUG 或 RELEASE") { |value| options[:badge] = value }
|
|
627
|
+
opts.on("--no-badge", "禁用配置文件里的角标") { options[:badge] = nil }
|
|
628
|
+
opts.on("--assets PATH", "Assets.xcassets 路径") { |value| options[:assets] = value }
|
|
629
|
+
opts.on("--app-icon NAME", "AppIcon 名称(默认 AppIcon)") { |value| options[:app_icon] = value }
|
|
630
|
+
opts.separator ""
|
|
631
|
+
add_upload_options(opts, options)
|
|
632
|
+
opts.on("-h", "--help", "显示帮助") { puts opts; exit 0 }
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def package_parser(options)
|
|
637
|
+
OptionParser.new do |opts|
|
|
638
|
+
opts.banner = "用法: appship [--config FILE] package [选项]"
|
|
639
|
+
opts.on("--app PATH", "已编译 .app 路径") { |value| options[:app] = value }
|
|
640
|
+
opts.on("--output PATH", "最终 IPA 路径") { |value| options[:output] = value }
|
|
641
|
+
opts.on("--badge TEXT", "添加角标") { |value| options[:badge] = value }
|
|
642
|
+
opts.on("--no-badge", "禁用角标") { options[:badge] = nil }
|
|
643
|
+
opts.on("--assets PATH", "Assets.xcassets 路径") { |value| options[:assets] = value }
|
|
644
|
+
opts.on("--app-icon NAME", "AppIcon 名称") { |value| options[:app_icon] = value }
|
|
645
|
+
add_upload_options(opts, options)
|
|
646
|
+
opts.on("-h", "--help", "显示帮助") { puts opts; exit 0 }
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def upload_parser(options)
|
|
651
|
+
OptionParser.new do |opts|
|
|
652
|
+
opts.banner = "用法: appship [--config FILE] upload IPA_OR_APK [选项]"
|
|
653
|
+
opts.on("--file PATH", "IPA/APK 路径,也可作为位置参数传入") { |value| options[:file] = value }
|
|
654
|
+
add_upload_options(opts, options)
|
|
655
|
+
opts.on("-h", "--help", "显示帮助") { puts opts; exit 0 }
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def pgyer_parser(options)
|
|
660
|
+
OptionParser.new do |opts|
|
|
661
|
+
opts.banner = "用法: appship pgyer [选项]"
|
|
662
|
+
opts.separator ""
|
|
663
|
+
opts.separator "项目与缓存"
|
|
664
|
+
opts.on("--project-dir PATH", "项目目录(默认当前目录)") { |value| options[:project_dir] = value }
|
|
665
|
+
opts.on("--env-file PATH", "本机缓存文件(默认 ~/.appship/.config)") { |value| options[:env_file] = value }
|
|
666
|
+
opts.on("--project-name NAME", "缓存中的 project_name") { |value| options[:project_name] = value }
|
|
667
|
+
opts.on("--app-name NAME", "缓存中的 app_name") { |value| options[:app_name] = value }
|
|
668
|
+
opts.on("--workspace PATH", "Xcode workspace 路径") { |value| options[:workspace] = value }
|
|
669
|
+
opts.on("--project PATH", "Xcode project 路径") { |value| options[:project] = value }
|
|
670
|
+
opts.on("--scheme NAME", "Scheme 名称") { |value| options[:scheme] = value }
|
|
671
|
+
opts.on("--configuration NAME", "构建配置(Debug/Release)") { |value| options[:configuration] = value; options[:configuration_explicit] = true }
|
|
672
|
+
opts.on("--output PATH", "最终 IPA 路径") { |value| options[:output] = value }
|
|
673
|
+
opts.on("--cache", "复用 DerivedData 中已有的 .app") { options[:build_mode] = "cache"; options[:build_mode_explicit] = true }
|
|
674
|
+
opts.on("--rebuild", "重新 archive/export 构建(默认)") { options[:build_mode] = "rebuild"; options[:build_mode_explicit] = true }
|
|
675
|
+
opts.separator ""
|
|
676
|
+
opts.separator "角标与导出"
|
|
677
|
+
opts.on("--badge TEXT", "添加图标角标,例如 DEBUG") { |value| options[:badge] = value; options[:badge_explicit] = true }
|
|
678
|
+
opts.on("--no-badge", "不添加图标角标") { options[:badge] = nil; options[:badge_explicit] = true }
|
|
679
|
+
opts.on("--assets PATH", "Assets.xcassets 路径") { |value| options[:assets] = value }
|
|
680
|
+
opts.on("--app-icon NAME", "AppIcon 名称") { |value| options[:app_icon] = value }
|
|
681
|
+
opts.on("--export-method NAME", "development/adhoc/app-store 等") { |value| options[:export_method] = value }
|
|
682
|
+
opts.on("--team-id ID", "签名 Team ID") { |value| options[:team_id] = value }
|
|
683
|
+
opts.on("--no-allow-provisioning-updates", "不允许自动更新签名") { options[:allow_provisioning_updates] = false }
|
|
684
|
+
opts.separator ""
|
|
685
|
+
opts.separator "蒲公英"
|
|
686
|
+
opts.separator "简写:-key KEY、-password PASSWORD 会同时更新本机缓存"
|
|
687
|
+
opts.on("--api-key KEY", "覆盖并保存缓存中的 pgyer_api_key") { |value| options[:api_key] = value }
|
|
688
|
+
opts.on("--install-type N", Integer, "1=公开,2=密码,3=邀请") { |value| options[:install_type] = value }
|
|
689
|
+
opts.on("--password PASSWORD", "覆盖并保存缓存中的 pgyer_password") { |value| options[:password] = value }
|
|
690
|
+
opts.on("--password-env NAME", "安装密码环境变量名") { |value| options[:password_env] = value }
|
|
691
|
+
opts.on("--description TEXT", "更新描述") { |value| options[:description] = value; options[:description_explicit] = true }
|
|
692
|
+
opts.on("--channel NAME", "渠道短链接") { |value| options[:channel] = value }
|
|
693
|
+
opts.on("--poll-attempts N", Integer, "轮询次数,默认 60") { |value| options[:poll_attempts] = value }
|
|
694
|
+
opts.on("-h", "--help", "显示帮助") { puts opts; exit 0 }
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def fir_parser(options)
|
|
699
|
+
OptionParser.new do |opts|
|
|
700
|
+
opts.banner = "用法: appship fir [选项]"
|
|
701
|
+
opts.separator ""
|
|
702
|
+
opts.separator "项目与缓存"
|
|
703
|
+
opts.on("--project-dir PATH", "项目目录(默认当前目录)") { |value| options[:project_dir] = value }
|
|
704
|
+
opts.on("--env-file PATH", "本机缓存文件(默认 ~/.appship/.config)") { |value| options[:env_file] = value }
|
|
705
|
+
opts.on("--project-name NAME", "缓存中的 project_name") { |value| options[:project_name] = value }
|
|
706
|
+
opts.on("--app-name NAME", "fir.im 应用名称") { |value| options[:app_name] = value }
|
|
707
|
+
opts.on("--workspace PATH", "Xcode workspace 路径") { |value| options[:workspace] = value }
|
|
708
|
+
opts.on("--project PATH", "Xcode project 路径") { |value| options[:project] = value }
|
|
709
|
+
opts.on("--scheme NAME", "Scheme 名称") { |value| options[:scheme] = value }
|
|
710
|
+
opts.on("--configuration NAME", "构建配置(Debug/Release)") { |value| options[:configuration] = value; options[:configuration_explicit] = true }
|
|
711
|
+
opts.on("--output PATH", "最终 IPA 路径") { |value| options[:output] = value }
|
|
712
|
+
opts.on("--cache", "复用 DerivedData 中已有的 .app") { options[:build_mode] = "cache"; options[:build_mode_explicit] = true }
|
|
713
|
+
opts.on("--rebuild", "重新 archive/export 构建(默认)") { options[:build_mode] = "rebuild"; options[:build_mode_explicit] = true }
|
|
714
|
+
opts.separator ""
|
|
715
|
+
opts.separator "角标与导出"
|
|
716
|
+
opts.on("--badge TEXT", "添加图标角标,例如 DEBUG") { |value| options[:badge] = value; options[:badge_explicit] = true }
|
|
717
|
+
opts.on("--no-badge", "不添加角标") { options[:badge] = nil; options[:badge_explicit] = true }
|
|
718
|
+
opts.on("--assets PATH", "Assets.xcassets 路径") { |value| options[:assets] = value }
|
|
719
|
+
opts.on("--app-icon NAME", "AppIcon 名称") { |value| options[:app_icon] = value }
|
|
720
|
+
opts.on("--export-method NAME", "adhoc/inhouse 等") { |value| options[:export_method] = value }
|
|
721
|
+
opts.on("--team-id ID", "签名 Team ID") { |value| options[:team_id] = value }
|
|
722
|
+
opts.on("--no-allow-provisioning-updates", "不允许自动更新签名") { options[:allow_provisioning_updates] = false }
|
|
723
|
+
opts.separator ""
|
|
724
|
+
opts.separator "fir.im"
|
|
725
|
+
opts.separator "简写:-key KEY、-password PASSWORD 会同时更新本机缓存"
|
|
726
|
+
opts.on("--fir-api-key KEY", "覆盖并保存缓存中的 fir_api_key") { |value| options[:fir_api_token] = value }
|
|
727
|
+
opts.on("--fir-api-token TOKEN", "fir.im API Token(不建议写入配置文件)") { |value| options[:fir_api_token] = value }
|
|
728
|
+
opts.on("--fir-api-token-env NAME", "fir.im API Token 环境变量名") { |value| options[:fir_api_token_env] = value }
|
|
729
|
+
opts.on("--fir-password PASSWORD", "覆盖并保存缓存中的 fir_password") { |value| options[:fir_password] = value }
|
|
730
|
+
opts.on("--fir-password-env NAME", "fir.im 访客密码环境变量名") { |value| options[:fir_password_env] = value }
|
|
731
|
+
opts.on("--bundle-id ID", "应用 Bundle ID(IPA 可自动读取)") { |value| options[:bundle_id] = value }
|
|
732
|
+
opts.on("--app-version VERSION", "应用版本号") { |value| options[:app_version] = value }
|
|
733
|
+
opts.on("--build-number NUMBER", "Build 号") { |value| options[:build_number] = value }
|
|
734
|
+
opts.on("--release-type TYPE", "iOS 打包类型(Adhoc 或 Inhouse)") { |value| options[:release_type] = value }
|
|
735
|
+
opts.on("--icon PATH", "应用图标路径(可选)") { |value| options[:icon] = value }
|
|
736
|
+
opts.on("--description TEXT", "更新描述") { |value| options[:description] = value; options[:description_explicit] = true }
|
|
737
|
+
opts.on("--no-clean", "不清理已有 archive/export 路径") { options[:clean] = false }
|
|
738
|
+
opts.on("--keep-temporary", "保留工具生成的临时目录") { options[:keep_temporary] = true }
|
|
739
|
+
opts.on("-h", "--help", "显示帮助") { puts opts; exit 0 }
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def add_upload_options(opts, options)
|
|
744
|
+
opts.separator ""
|
|
745
|
+
opts.separator "分发平台"
|
|
746
|
+
opts.on("--provider NAME", "分发平台(pgyer 或 fir,默认 pgyer)") { |value| options[:provider] = value }
|
|
747
|
+
opts.separator "蒲公英上传"
|
|
748
|
+
opts.on("--upload", "构建/打包后上传到所选分发平台") { options[:upload] = true }
|
|
749
|
+
opts.on("--api-key KEY", "蒲公英 API Key(不建议写入配置文件)") { |value| options[:api_key] = value }
|
|
750
|
+
opts.on("--api-key-env NAME", "API Key 环境变量名") { |value| options[:api_key_env] = value }
|
|
751
|
+
opts.on("--install-type N", Integer, "1=公开,2=密码,3=邀请") { |value| options[:install_type] = value }
|
|
752
|
+
opts.on("--password PASSWORD", "安装密码(建议使用 --password-env)") { |value| options[:password] = value }
|
|
753
|
+
opts.on("--password-env NAME", "安装密码环境变量名") { |value| options[:password_env] = value }
|
|
754
|
+
opts.on("--description TEXT", "更新描述") { |value| options[:description] = value }
|
|
755
|
+
opts.on("--channel NAME", "渠道短链接") { |value| options[:channel] = value }
|
|
756
|
+
opts.on("--poll-attempts N", Integer, "轮询次数,默认 60") { |value| options[:poll_attempts] = value }
|
|
757
|
+
opts.separator ""
|
|
758
|
+
opts.separator "fir.im 上传"
|
|
759
|
+
opts.on("--fir-api-token TOKEN", "fir.im API Token(不建议写入配置文件)") { |value| options[:fir_api_token] = value }
|
|
760
|
+
opts.on("--fir-api-token-env NAME", "fir.im API Token 环境变量名") { |value| options[:fir_api_token_env] = value }
|
|
761
|
+
opts.on("--fir-password PASSWORD", "fir.im 访客密码") { |value| options[:fir_password] = value }
|
|
762
|
+
opts.on("--fir-password-env NAME", "fir.im 访客密码环境变量名") { |value| options[:fir_password_env] = value }
|
|
763
|
+
opts.on("--bundle-id ID", "fir.im 应用 Bundle ID(IPA 可自动读取)") { |value| options[:bundle_id] = value }
|
|
764
|
+
opts.on("--app-name NAME", "fir.im 应用名称") { |value| options[:app_name] = value }
|
|
765
|
+
opts.on("--app-version VERSION", "fir.im 应用版本号") { |value| options[:app_version] = value }
|
|
766
|
+
opts.on("--build-number NUMBER", "fir.im Build 号") { |value| options[:build_number] = value }
|
|
767
|
+
opts.on("--release-type TYPE", "iOS 打包类型(Adhoc 或 Inhouse)") { |value| options[:release_type] = value }
|
|
768
|
+
opts.on("--icon PATH", "fir.im 应用图标路径(可选)") { |value| options[:icon] = value }
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def help
|
|
772
|
+
<<~HELP
|
|
773
|
+
appship #{VERSION} - 通用移动应用构建、打包和分发工具
|
|
774
|
+
|
|
775
|
+
用法:
|
|
776
|
+
appship init [--path .appship.yml]
|
|
777
|
+
appship doctor
|
|
778
|
+
appship pgyer
|
|
779
|
+
appship fir
|
|
780
|
+
appship build [选项]
|
|
781
|
+
appship package --app path/to/App.app [选项]
|
|
782
|
+
appship upload path/to/App.ipa [选项]
|
|
783
|
+
|
|
784
|
+
常用示例:
|
|
785
|
+
appship init
|
|
786
|
+
cd /path/to/project && appship pgyer
|
|
787
|
+
FIR_API_TOKEN=xxx appship fir --workspace MyApp.xcworkspace --scheme MyApp
|
|
788
|
+
appship build --workspace MyApp.xcworkspace --scheme MyApp --configuration Debug
|
|
789
|
+
appship build --project MyApp.xcodeproj --scheme MyApp --badge DEBUG --upload
|
|
790
|
+
appship package --app build/Debug-iphoneos/MyApp.app --output build/MyApp.ipa
|
|
791
|
+
PGYER_API_KEY=xxx appship upload build/MyApp.ipa --install-type 2 --password-env PGYER_INSTALL_PASSWORD
|
|
792
|
+
FIR_API_TOKEN=xxx appship upload build/MyApp.ipa --provider fir --bundle-id com.example.MyApp
|
|
793
|
+
|
|
794
|
+
全局选项必须放在命令前:
|
|
795
|
+
--config FILE 使用指定配置文件
|
|
796
|
+
--verbose 输出执行的底层命令
|
|
797
|
+
HELP
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
end
|