Fork me on GitHub

使用 Swift 脚本进行自动化打包

前言

  做 iOS 项目开发,日常开发中总少不了打测试包以及发布上线等操作,小的团队比较灵活,一般有专人负责上线,Debug 的时候测试同学直接找到开发要测试包。大的团队就要考虑沟通成本问题,产品同学和测试同学想要测试包的时候,都不知道该找谁,而且让开发同学每天频繁的打包上传测试平台也是十分消磨人的耐性,这个时候就需要一个内部的平台,由开发和运维负责维护并提供操作说明,最好由测试同学操作平台,执行打包测试、或者发布上线的操作。

  解决这个问题,最常用的办法就是使用 Jenkins 进行持续集成,关于 Jenkins 的配置,这里不做说明,网上已经有很多的教程。使用 Jenkins 进行打包的时候其实还是要调用 Xcode 的命令行工具进行编译打包导出操作,那么这个过程就需要脚本来执行任务。

Shell 脚本

  今天我们来说说使用脚本进行自动化打包。说到脚本,最常接触的应该是 shell 命令(文件名后缀为 sh),使用 shell 脚本打包的例子如下(适用于多 xcconfig 配置下自动化打包):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
################## config ##################

# 开发者账号
appid="" # 你的 AppleID
appid_pwd="" # 你的 AppleID 密码

# fir.im token
fir_token="" # 你的 fir.im token

# SVN,用来做备份,没有的可以不配置
svn_URL="" # 自己公司的私有 SVN 地址
svn_username="" # SVN 账号
svn_password="" # SVN 密码

################## config end ##################


# 工程绝对路径,这里取的 shell 脚本的相对路径,可以根据项目实际情况进行改动
project_path=$(cd `dirname $0`;cd ..;cd ..; pwd)

# shell绝对路径
shell_path=$(cd `dirname $0`; pwd)

# 工程名 将XXX替换成自己的工程名,这里只考虑了 xcworkspace 项目,如果项目是 xcodeproj 可自行更改
project_name=$(basename $(find ${project_path} -name *.xcworkspace ) .xcworkspace)
echo "------project_name=${project_name}------"

# scheme名 将XXX替换成自己的sheme名
scheme_name=${project_name}
# 打包模式 Release/Debug/Product/PreProduct/Test
development_mode=Release

# build文件夹路径
build_path=${project_path}/build

# plist文件所在路径
exportOptionsPlistPath=${shell_path}/exportAppstore.plist

# info.plist路径
info_Plist=${project_path}/${project_name}/Info.plist
echo "====info_Plist:${info_Plist}"

# bundleVersion
shortVersion=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" ${info_Plist})
echo "\n====shortVersion:${shortVersion}===="

bundleVersion=$(date "+%m%d%H%M")
bundleVersion="$shortVersion.$bundleVersion"

/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $bundleVersion" ${info_Plist}
bundleVersion=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" ${info_Plist})
echo "====bundleVersion:${bundleVersion}====\n"

# 导出.ipa文件所在路径
export_DirName=${project_name}_IPADir
export_desktop=$(cd `~`;pwd)/Desktop
export_IPAPath=${export_desktop}/${export_DirName}
exportIpaPath=${export_IPAPath}/${development_mode}/${bundleVersion}

if [ ! -d ${export_IPAPath} ];
then
mkdir -p ${export_IPAPath};
fi

echo "------请输入发布的环境------\n 1:app-store\n 2:ad-hoc-fir.im\n 3:development-fir.im\n 4:enterprise-fir.im\n"

##
read number
while([[ $number != 1 ]] && [[ $number != 2 ]] && [[ $number != 3 ]] && [[ $number != 4 ]])
do
echo "错误!只能输入 1 or 2 or 3 or 4"
echo "------请输入发布的环境------\n 1:app-store\n 2:ad-hoc-fir.im\n 3:development-fir.im\n 4:enterprise-fir.im\n"
read number
done

if [ $number == 1 ];then
development_mode=Release
exportOptionsPlistPath=${shell_path}/exportAppstore.plist
else

echo "请输入你需要编译的环境 \n 1:Release(AppStore发布)\n 2:Debug(调试)\n 3:PreProduct(预生产)\n 4:Product(生产)\n 5:Test(测试)\n \
6:TestBugly(bugly)✈--------------------------✈"
read environment
if [ $environment == 1 ];then
development_mode=Release
elif [ $environment == 2 ];then
development_mode=Debug
elif [ $environment == 3 ];then
development_mode=PreProduct
elif [ $environment == 4 ];then
development_mode=Product
elif [ $environment == 5 ];then
development_mode=Test
elif [ $environment == 6 ];then
development_mode=TestBugly
else
development_mode=Release
fi

echo "///Environment:${development_mode}"
exportIpaPath=${export_IPAPath}/${development_mode}/${bundleVersion}

if [ $number == 2 ];then
exportOptionsPlistPath=${shell_path}/exportAdHoc.plist
elif [ $number == 3 ];then
exportOptionsPlistPath=${shell_path}/exportDevelop.plist
elif [ $number == 4 ];then
exportOptionsPlistPath=${shell_path}/exportEnterprise.plist
fi

fi

if [ ! -d ${exportIpaPath} ];
rm -rf ${exportIpaPath};
mkdir -p ${exportIpaPath};
then
mkdir -p ${exportIpaPath};
fi

exportSvnPaht=${export_DirName}/${development_mode}/${bundleVersion}

echo '///-----------'
echo '/// 正在清理工程'
echo '///-----------'
xcodebuild clean -configuration ${development_mode} -quiet || exit


echo '///--------'
echo '/// 清理完成'
echo '///--------'
echo ''

echo '///-----------'
echo '/// 正在编译工程:'
echo '///-----------'
echo "xcodebuild archive -workspace ${project_path}/${project_name}.xcworkspace -scheme ${scheme_name} -configuration ${development_mode} -archivePath ${exportIpaPath}/${project_name}.xcarchive -quiet"
xcodebuild archive -workspace ${project_path}/${project_name}.xcworkspace -scheme ${scheme_name} -configuration ${development_mode} -archivePath ${exportIpaPath}/${project_name}.xcarchive -quiet || exit

echo '///--------'
echo '/// 编译完成'
echo '///--------'
echo ''

echo '///----------'
echo '/// 开始ipa打包'
echo '///----------'


# -allowProvisioningUpdates -allowProvisioningDeviceRegistration
echo "xcodebuild -exportArchive -allowProvisioningUpdates \
-archivePath ${exportIpaPath}/${project_name}.xcarchive \
-configuration ${development_mode} \
-exportPath ${exportIpaPath} \
-exportOptionsPlist ${exportOptionsPlistPath} \
-quiet"
xcodebuild -exportArchive -allowProvisioningUpdates \
-archivePath ${exportIpaPath}/${project_name}.xcarchive \
-configuration ${development_mode} \
-exportPath ${exportIpaPath} \
-exportOptionsPlist ${exportOptionsPlistPath} \
-quiet || exit

if [ -e $exportIpaPath/$scheme_name.ipa ]; then
echo '///----------'
echo '/// ipa包已导出'
echo '///----------'

else
echo '///-------------'
echo '/// ipa包导出失败 '
echo '///-------------'
fi
echo '///------------'
echo '/// 打包ipa完成 '
echo '///------------'

echo '///----------'
echo '/// ipa 图片检查'
echo '///----------'

tar xvf ${exportIpaPath}/${project_name}.ipa -C ${exportIpaPath}
find ${exportIpaPath}/Payload/${project_name}.app -name Assets.car
xcrun --sdk iphoneos assetutil --info ${exportIpaPath}/Payload/${project_name}.app/Assets.car > ${exportIpaPath}/Assets.json

if [ `grep -c "P3" ${exportIpaPath}/Assets.json` -eq '1' ]; then
echo '///----------'
echo '/// ipa P3 图片检查 error!'
echo '///----------'
open ${exportIpaPath}/Assets.json
exit 0
fi

if [ `grep -c "ARGB-16" ${exportIpaPath}/Assets.json` -eq '1' ]; then
echo '///----------'
echo '/// error! ARGB-16 图片检查 error!'
echo '///----------'
open ${exportIpaPath}/Assets.json
exit 0
fi

echo '///----------'
echo '/// ipa 图片检查 Success!'
echo '///----------'

rm -rf ${exportIpaPath}/Payload;

echo '///-------------'
echo '/// 开始发布ipa包 '
echo '///-------------'

if [ $number == 1 ];then

# 验证并上传到 App Store
altoolPath="/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Versions/A/Support/altool"
"$altoolPath" --validate-app -f ${exportIpaPath}/${scheme_name}.ipa -u ${appid} -p ${appid_pwd} -t ios --output-format xml
"$altoolPath" --upload-app -f ${exportIpaPath}/${scheme_name}.ipa -u ${appid} -p ${appid_pwd} -t ios --output-format xml

# 上传 svn
svn import ${exportIpaPath} ${svn_URL}/${exportSvnPaht} --username=${svn_username} --password=${svn_password} -m ${bundleVersion}

else

# 上传到 fir.im
fir login -T ${fir_token}
fir publish $exportIpaPath/$scheme_name.ipa

fi

open $exportIpaPath

exit 0

Swift 脚本

  其实除了常规的 shell 命令、Python 脚本这些,Swift 也是支持脚本化编程的,而且每台能打包的机器上肯定都安装了 Xcode,天然支持 Swift 的运行环境,Swift 的版本还会跟着 Xcode 的升级自动提升,使用到最新版本的改进。

  我这这里抛砖引玉,将我目前在用的脚本分享出来,希望大家能够提出改进意见,或者更加实用的方案。当前在用的方案示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
#!/usr/bin/env swift

import Foundation

/******************************* Shell Environment **************************************/

struct ShellEnvironment {

@discardableResult
static func shell(launchPath: String, arguments: [String]) -> String {
let task = Process()
task.launchPath = launchPath
task.arguments = arguments

let pipe = Pipe()
task.standardOutput = pipe
task.launch()

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: String.Encoding.utf8)!
if output.count > 0 {
// remove newline character.
let lastIndex = output.index(before: output.endIndex)
return String(output[output.startIndex ..< lastIndex])
}
return output
}
}

@discardableResult
func bash(command: String, arguments: [String]) -> String {

let result = ShellEnvironment.shell(launchPath: "/usr/bin/which", arguments: [ command ])
if result.count == 0 {
print("\n\n\n")
print("warning: bash_command: \(command), not found!")
print("\n\n\n")
}

let whichPathForCommand = ShellEnvironment.shell(launchPath: "/bin/bash", arguments: [ "-l", "-c", "which \(command)" ])
return ShellEnvironment.shell(launchPath: whichPathForCommand, arguments: arguments)
}

/******************************* Utils **************************************/

enum ExitCode: Int32 {
case cannotFindPath = 100
case noPodfile = 101
case podfileWriteError = 102
case ipaExportFailed = 103
case p3ImageError = 104
case argb_16ImageError = 105
}

struct TimeManager {

static let shared = TimeManager()

fileprivate let matter = DateFormatter()

private init() {
matter.dateFormat = "yyyy-MM-dd HH:mm:ss"
matter.timeZone = TimeZone(identifier: "Asia/Shanghai")
}

func current() -> String {
return matter.string(from: Date())
}
}

extension TimeManager {

// Static func
static func current() -> String {
return self.shared.matter.string(from: Date())
}

static func currentTimeInterval() -> TimeInterval {
return Date().timeIntervalSince1970
}

static func timeStrToInterval(_ str: String) -> TimeInterval {
let date = self.shared.matter.date(from: str)
return date?.timeIntervalSince1970 ?? 0
}
}

extension String {

public var toInt: Int? {
return Int(self)
}

public var toFloat: Float? {
return Float(self)
}

public var toDouble: Double? {
return Double(self)
}

/// Remove the blank characters at both ends of the string
public func trim() -> String {
return self.trimmingCharacters(in: CharacterSet.whitespaces)
}

/// compatibility API for NSString
public var length: Int {
return self.count
}

public func indexOf(_ target: Character) -> Int? {
#if swift(>=5.0)
return self.firstIndex(of: target)?.utf16Offset(in: self)
#else
return self.firstIndex(of: target)?.encodedOffset
#endif
}

public func subString(to: Int) -> String {
#if swift(>=5.0)
let endIndex = String.Index(utf16Offset: to, in: self)
#else
let endIndex = String.Index.init(encodedOffset: to)
#endif
let subStr = self[self.startIndex..<endIndex]
return String(subStr)
}

public func subString(from: Int) -> String {
#if swift(>=5.0)
let startIndex = String.Index(utf16Offset: from, in: self)
#else
let startIndex = String.Index.init(encodedOffset: from)
#endif
let subStr = self[startIndex..<self.endIndex]
return String(subStr)
}

public func subString(range: Range<String.Index>) -> String {
return String(self[range.lowerBound..<range.upperBound])
}

public func subString(start: Int, end: Int) -> String {
#if swift(>=5.0)
let startIndex = String.Index(utf16Offset: start, in: self)
let endIndex = String.Index(utf16Offset: end, in: self)
#else
let startIndex = String.Index.init(encodedOffset: start)
let endIndex = String.Index.init(encodedOffset: end)
#endif
return String(self[startIndex..<endIndex])
}

public func subString(withNSRange range: NSRange) -> String {

return subString(start: range.location, end: range.location + range.length)
}

/// NSRange 转化为 Range
public func range(from nsRange: NSRange) -> Range<String.Index>? {
guard
let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self)
else { return nil }

return from ..< to
}
}

enum ANSIColors: String {
case black = "\u{001B}[0;30m"
case red = "\u{001B}[0;31m" // error
case green = "\u{001B}[0;32m" // success
case yellow = "\u{001B}[0;33m" // warning
case blue = "\u{001B}[0;34m" // info
case magenta = "\u{001B}[0;35m" // important
case cyan = "\u{001B}[0;36m" // tips
case white = "\u{001B}[0;37m" // unimportant
case `default` = "\u{001B}[0;0m"

func name() -> String {
switch self {
case .black: return "Black"
case .red: return "Red"
case .green: return "Green"
case .yellow: return "Yellow"
case .blue: return "Blue"
case .magenta: return "Magenta"
case .cyan: return "Cyan"
case .white: return "White"
case .default: return "Default"
}
}

static func all() -> [ANSIColors] {
return [.black, .red, .green, .yellow, .blue, .magenta, .cyan, .white, .default]
}

static func + (_ left: ANSIColors, _ right: String) -> String {
return left.rawValue + right
}
}

print(ANSIColors.default + "")
print(ANSIColors.cyan + TimeManager.current())
print(ANSIColors.default + "")

/******************************* config **************************************/

// 开发者账号
let appid = "" // 你的 AppleID
let appid_pwd = "" // 你的 AppleID 密码

// fir.im token
let fir_token = "" // 你的 fir.im token

/******************************* config end **************************************/

// 获取项目绝对路径,这个可以根据需要灵活改动,也可以直接写死
let shell_file = URL(fileURLWithPath: #file).path

var last = shell_file.components(separatedBy: "/").last ?? ""
let shell_path = shell_file.subString(start: 0, end: shell_file.count - last.count)

last = shell_path.components(separatedBy: "/").reversed()[0...2].joined(separator: "/")
let project_path = shell_path.subString(start: 0, end: shell_path.count - last.count)
print(ANSIColors.blue + "shell绝对路径: \(shell_path)")
print(ANSIColors.blue + "工程绝对路径: \(project_path)")

// 工程名 将XXX替换成自己的工程名,这里只考虑了 xcworkspace 项目,如果项目是 xcodeproj 可自行更改
var project_name = ""
let manager = FileManager.default
do {
let path = project_path
let url = URL(fileURLWithPath: path)

guard let contentsOfPath = try? manager.contentsOfDirectory(atPath: url.path) else {
print("error: can't find path, please check the relative path")
exit(0)
}
// print("contentsOfPath: \(contentsOfPath)")

for fileName in contentsOfPath {
if fileName.hasSuffix(".xcworkspace") {
project_name = fileName.components(separatedBy: ".xcworkspace").first ?? ""
break
}
}
}

print(ANSIColors.blue + "------ project_name: \(project_name) ------\n")

// scheme名 将XXX替换成自己的sheme名
var scheme_name = project_name
// 打包模式 Release/Debug/Product/PreRelease/Test
var development_mode = "Release"

// build文件夹路径
var build_path = "\(project_path)build"

// plist文件所在路径
var exportOptionsPlistPath = "\(shell_path)exportAppstore.plist"

// info.plist路径
var info_Plist = "\(project_path)\(project_name)/Info.plist"
print(ANSIColors.blue + "==== info_Plist: \(info_Plist) ====\n")
// bundleVersion
let shortVersion = bash(command: "/usr/libexec/PlistBuddy", arguments: ["-c", "Print :CFBundleShortVersionString", info_Plist])
print(ANSIColors.blue + "\n==== shortVersion: \(shortVersion) ====\n")

let bundleVersion = bash(command: "/usr/libexec/PlistBuddy", arguments: ["-c", "Print :CFBundleVersion", info_Plist])
print(ANSIColors.blue + "==== bundleVersion: \(bundleVersion) ====\n")

// 导出.ipa文件所在路径
let export_DirName = "\(project_name)_IPADir"

let documentUrls = manager.urls(for: .desktopDirectory, in:.userDomainMask)
let export_desktop = documentUrls[0].path
let export_IPAPath = "\(export_desktop)/\(export_DirName)"

if !manager.fileExists(atPath: export_IPAPath) {
bash(command: "mkdir", arguments: ["-p", export_IPAPath])
}

let publishEnvironmentTip = "------请输入发布的环境------\n 1: app-store\n 2: ad-hoc-fir.im\n 3: development-fir.im\n 4: enterprise-fir.im\n"
print(ANSIColors.magenta + publishEnvironmentTip)

func checkPublishEnvironment(_ number: String) -> Bool {
return (number != "1" && number != "2" && number != "3" && number != "4")
}

var number = ""
repeat {
if let num = readLine() {
number = num
if checkPublishEnvironment(number) {
print(ANSIColors.red + "错误!只能输入 1 or 2 or 3 or 4")
}
} else {
print(ANSIColors.magenta + publishEnvironmentTip)
}
} while(checkPublishEnvironment(number))

print(ANSIColors.green + "输入成功:\(number)")
print(ANSIColors.default + "")

if number == "1" {
development_mode = "Release"
exportOptionsPlistPath = "\(shell_path)exportAppstore.plist"
} else {
let buildEnvironmentTip = "请输入你需要编译的环境 \n 1: Release (AppStore发布)\n 2: Debug(调试)\n 3: PreRelease(预生产)\n 4: Product(生产) ✈--------------------------✈"
print(ANSIColors.magenta + buildEnvironmentTip)

func checkBuildEnvironment(_ environment: String) -> Bool {
return (environment != "1" && environment != "2" && environment != "3" && environment != "4")
}

var environment = ""
repeat {
if let num = readLine() {
environment = num
if checkBuildEnvironment(environment) {
print(ANSIColors.red + "错误!只能输入 1 or 2 or 3 or 4")
}
} else {
print(ANSIColors.magenta + buildEnvironmentTip)
}
} while(checkBuildEnvironment(environment))

print(ANSIColors.green + "输入成功:\(environment)")
print("")

if environment == "1" {
development_mode = "Release"
} else if environment == "2" {
development_mode = "Debug"
} else if environment == "3" {
development_mode = "PreRelease"
} else if environment == "4" {
development_mode = "Product"
}
}

print(ANSIColors.blue + "Environment: \(development_mode)")
var exportIpaPath = "\(export_IPAPath)/\(development_mode)/\(bundleVersion)"
print(ANSIColors.blue + "==== exportIpaPath: \(exportIpaPath) ====\n")

if number == "2" {
exportOptionsPlistPath = "\(shell_path)exportAdHoc.plist"
} else if number == "3" {
exportOptionsPlistPath = "\(shell_path)exportDevelop.plist"
} else if number == "4" {
exportOptionsPlistPath = "\(shell_path)exportEnterprise.plist"
}


if manager.fileExists(atPath: export_IPAPath) {
bash(command: "rm", arguments: ["-rf", export_IPAPath])
}
bash(command: "mkdir", arguments: ["-p", export_IPAPath])


/************************************ Xcode clean **************************************/


print(ANSIColors.default + "-----------")
print(ANSIColors.blue + "正在清理工程")
print(ANSIColors.default + "-----------")
bash(command: "xcodebuild", arguments: ["clean", "-configuration", development_mode, "-quiet"])


print(ANSIColors.default + "--------")
print(ANSIColors.green + "清理完成")
print(ANSIColors.default + "--------")
print("")

/************************************ cocoapods **************************************/


print(ANSIColors.default + "--------")
print(ANSIColors.blue + "正在更新 Cocoapods")
print(ANSIColors.default + "--------")
print("")


let podfilePath = "\(project_path)Podfile"
print(ANSIColors.blue + "podfilePath: \(podfilePath)")

guard let text = try? String(contentsOfFile: podfilePath, encoding: String.Encoding.utf8) else {
exit(ExitCode.cannotFindPath.rawValue)
}

var array = text.components(separatedBy: "\n")

guard array.count > 11 else {
print(ANSIColors.red + "array.count is too short!")
exit(0)
}

let buglyStatusSuffix = "pod 'Bugly'"
let buglyStatus = array[9]

let buglyStatusTrue = " pod 'Bugly'"
let buglyStatusFalse = " # pod 'Bugly'"

print(ANSIColors.blue + "current bugly status: \(buglyStatus)")

guard buglyStatus.hasSuffix(buglyStatusSuffix) else {
print(ANSIColors.red + "error there has no bugly pod")
exit(0)
}

let bugtagsTrue = " pod 'Bugtags'"
let bugtagsStatusSuffix = "pod 'Bugtags'"
let bugtagsStatus = array[10]

let bugtagsStatusTrue = " pod 'Bugtags'"
let bugtagsStatusFalse = " # pod 'Bugtags'"

print(ANSIColors.blue + "current bugtags status: \(bugtagsStatus)")

guard bugtagsStatus.hasSuffix(bugtagsStatusSuffix) else {
print(ANSIColors.red + "error there has no bugtags pod")
exit(0)
}

let hasBugly = (buglyStatus == buglyStatusTrue)
let hasBugtags = (bugtagsStatus == bugtagsStatusTrue)

let isDebug = (development_mode == "Debug" || development_mode == "PreRelease")

if (isDebug != hasBugly || isDebug != hasBugtags) {
print(ANSIColors.default + "")
print(ANSIColors.magenta + "Cocoapods 需要更新。。。")
print(ANSIColors.default + "")
if (isDebug) {
array[9] = buglyStatusTrue
array[10] = bugtagsStatusTrue
} else {
array[9] = buglyStatusFalse
array[10] = bugtagsStatusFalse
}
let finalStr = array.joined(separator: "\n")
print(finalStr)

do {
try finalStr.write(toFile: podfilePath, atomically: true, encoding: .utf8)
print(ANSIColors.green + "写入成功")
} catch {
print(ANSIColors.red + "写入失败")
print(error as Error)
exit(ExitCode.podfileWriteError.rawValue)
}

let l = bash(command: "pod", arguments: [ "install" ])
print(l)
} else {
print(ANSIColors.default + "")
print(ANSIColors.magenta + "Cocoapods 不需要更新")
print(ANSIColors.default + "")
}

print(ANSIColors.default + "--------")
print(ANSIColors.green + "Cocoapods 更新完成")
print(ANSIColors.default + "--------")
print("")


/************************************ archive **************************************/


print(ANSIColors.default + "-----------")
print(ANSIColors.blue + "正在编译工程:")
print(ANSIColors.default + "-----------")
print(ANSIColors.default + "xcodebuild archive -workspace \(project_path)(project_name).xcworkspace -scheme \(scheme_name) -configuration \(development_mode) -archivePath \(exportIpaPath)/\(project_name).xcarchive -quiet")

var log = bash(command: "xcodebuild", arguments: ["archive", "-workspace", "\(project_path)\(project_name).xcworkspace", "-scheme", scheme_name, "-configuration", development_mode, "-archivePath", "\(exportIpaPath)/\(project_name).xcarchive", "-quiet"])

print(ANSIColors.default + "")
//print(log)
print(ANSIColors.magenta + "the last line with log: \(log.components(separatedBy: ".").last ?? "")")
print(ANSIColors.default + "")

print(ANSIColors.default + "--------")
print(ANSIColors.green + "编译完成")
print(ANSIColors.default + "--------")
print(ANSIColors.default + "")

print(ANSIColors.default + "----------")
print(ANSIColors.blue + "开始ipa打包")
print(ANSIColors.default + "----------")


//# -allowProvisioningUpdates -allowProvisioningDeviceRegistration
print(ANSIColors.default + ["xcodebuild", "-exportArchive", "-allowProvisioningUpdates", "-archivePath", "\(exportIpaPath)/\(project_name).xcarchive", "-configuration", development_mode, "-exportPath", exportIpaPath, "-exportOptionsPlist", exportOptionsPlistPath, "-quiet"].joined(separator: " "))

log = bash(command: "xcodebuild", arguments: ["-exportArchive", "-allowProvisioningUpdates", "-archivePath", "\(exportIpaPath)/\(project_name).xcarchive", "-configuration", development_mode, "-exportPath", exportIpaPath, "-exportOptionsPlist", exportOptionsPlistPath, "-quiet"])

print(ANSIColors.default + "")
print(log)
print(ANSIColors.magenta + "the last line with log: \(log.components(separatedBy: ".").last ?? "")")
print(ANSIColors.default + "")

if manager.fileExists(atPath: "\(exportIpaPath)/\(scheme_name).ipa") {
print(ANSIColors.default + "----------")
print(ANSIColors.green + "ipa包已导出")
print(ANSIColors.default + "----------")
} else {
print(ANSIColors.default + "-------------")
print(ANSIColors.red + "ipa包导出失败 ")
print(ANSIColors.default + "-------------")
exit(ExitCode.ipaExportFailed.rawValue)
}


print(ANSIColors.default + "------------")
print(ANSIColors.green + "打包ipa完成 ")
print(ANSIColors.default + "------------")

/******************************* p3 image check **************************************/

print(ANSIColors.default + "----------")
print(ANSIColors.blue + "ipa 图片检查")
print(ANSIColors.default + "----------")

bash(command: "tar", arguments: ["xvf", "\(exportIpaPath)/\(project_name).ipa", "-C", exportIpaPath])
bash(command: "find", arguments: ["\(exportIpaPath)/Payload/\(project_name).app", "-name", "Assets.car"])
log = bash(command: "xcrun", arguments: ["--sdk", "iphoneos", "assetutil", "--info", "\(exportIpaPath)/Payload/\(project_name).app/Assets.car"])
let assetsFilePath = "\(exportIpaPath)/Assets.json"
try! log.write(toFile: assetsFilePath, atomically: true, encoding: .utf8)

let p3Result = bash(command: "grep", arguments: ["-c", "P3", "\(exportIpaPath)/Assets.json"])
if p3Result == "1" {
print(ANSIColors.default + "----------")
print(ANSIColors.red + "ipa P3 图片检查 error!")
print(ANSIColors.default + "----------")
bash(command: "open", arguments: ["\(exportIpaPath)/Assets.json"])
exit(ExitCode.p3ImageError.rawValue)
}

let res = bash(command: "grep", arguments: ["-c", "ARGB-16", "\(exportIpaPath)/Assets.json"])
if res == "1" {
print(ANSIColors.default + "----------")
print(ANSIColors.red + "error! ARGB-16 图片检查 error!")
print(ANSIColors.default + "----------")
bash(command: "open", arguments: ["\(exportIpaPath)/Assets.json"])
exit(ExitCode.argb_16ImageError.rawValue)
}

print(ANSIColors.default + "----------")
print(ANSIColors.green + "ipa 图片检查 Success!")
print(ANSIColors.default + "----------")

bash(command: "rm", arguments: ["-rf", "\(exportIpaPath)/Payload"])

print(ANSIColors.default + "-------------")
print(ANSIColors.blue + "开始发布ipa包 ")
print(ANSIColors.default + "-------------")

if number == "1" {

// 验证并上传到App Store
let altoolPath = "/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Versions/A/Support/altool"
// "$altoolPath" --validate-app -f ${exportIpaPath}/${scheme_name}.ipa -u ${appid} -p ${appid_pwd} -t ios --output-format xml
log = bash(command: altoolPath, arguments: ["--validate-app", "-f", "\(exportIpaPath)/\(scheme_name).ipa", "-u", appid, "-p", appid_pwd, "-t", "ios", "--output-format", "xml"])

print(ANSIColors.default + "")
print(ANSIColors.magenta + log)
print(ANSIColors.default + "")

// "$altoolPath" --upload-app -f ${exportIpaPath}/${scheme_name}.ipa -u ${appid} -p ${appid_pwd} -t ios --output-format xml
log = bash(command: altoolPath, arguments: ["--upload-app", "-f", "\(exportIpaPath)/\(scheme_name).ipa", "-u", appid, "-p", appid_pwd, "-t", "ios", "--output-format", "xml"])

print(ANSIColors.default + "")
print(ANSIColors.magenta + log)
print(ANSIColors.default + "")

} else {

// 上传到Fir
log = bash(command: "fir", arguments: ["login", "-T", fir_token])

print(ANSIColors.default + "")
print(ANSIColors.magenta + log)
print(ANSIColors.default + "")

log = bash(command: "fir", arguments: ["publish", "\(exportIpaPath)/\(scheme_name).ipa"])

print(ANSIColors.default + "")
print(ANSIColors.magenta + log)
print(ANSIColors.default + "")
}

bash(command: "open", arguments: [exportIpaPath])

print(ANSIColors.default + "")
print(ANSIColors.cyan + TimeManager.current())
print(ANSIColors.default + "")

脚本编写完成后,文件后缀可以是 .sh,也可以是 .swift,记得使用 chmod +x 命令给脚本赋予可执行权限。

存在的一些问题

  目前使用 Swift 进行脚本编程,还有一些问题。Swift 脚本并没有 sh 脚本与终端结合的那么好,在日志输出方面表现的比较明显, sh 脚本能够做到实时输出,目前 Swift 脚本需要搭配 shell 命令使用,执行 shell 的时候只能等一条命令执行完毕后才能够进行输出,而且输出的内容没有颜色,这是一个弱势,希望后续能够得到改善。

------------- 本文结束感谢您的阅读 -------------