Jenkins + Xcode9 持续集成环境搭建

最近搭建了一套 Jenkins + Xcode9 持续集成环境,记录一下。

安装 Jenkins

推荐通过 homebrew 安装,也可以通过下载 pkg 包安装(Jenkins OSX Installer),不过还是 brew 方便些。

1
brew install jenkins

如果没安装 Java,需要先装一下:

1
brew cask install caskroom/versions/java8

初始化配置

这里提一下关键的点,就不一一截图细说了。

浏览器中打开 http://localhost:8080

第一次打开会提示去取 initialAdminPassword(一行红字标明路径,很明显)里的密码。cat path/to/initialAdminPassword 拿到后贴到输入框中 next。

→ 安装推荐的插件 → 创建用户。完成后重新登录 Jenkins。

接下来在 Manage Jenkins -> Manage Plugins -> Available 安装一些需要的插件:

  • Xcode integration

  • Keychains and Provisioning Profiles Management

    可能一些教程推荐装这个插件,然后配置一下 login.keychain 填一些证书信息什么的就很方便导出 .ipa 了。but,很遗憾 Xcode9 无法读取 login.keychain 的信息,现在导出需要提供一份 ExportOptions.plist,后面再具体说怎么操作。

  • GIT plugin

  • Git Parameter

局域网内访问 Jenkins

使用 brew 安装 Jenkins 默认将 httpListenAddress 设置为 127.0.0.1,本机可以通过 localhost:8080 访问,但局域网内无法通过 本机 ip:8080 访问。

1
2
~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist
/usr/local/opt/jenkins/homebrew.mxcl.jenkins.plist

将两个 plist 文件中 httpListenAddress 改为 0.0.0.0 重启 Jenkins 即可。

重启 Jenkins 可以执行:

1
brew services restart jenkins

或者在浏览器里:

1
http://localhost:8080/restart

创建/配置项目

Net Item → 输入 item name → Freestyle project → OK

Source Code Management 选择 Git,并填入相应的信息。

Git 信息

切换到 Build 栏,按顺序添加 build step。

由于我们使用 Cocoapods,首先安装 pod 的第三方库。

点击 Add build step → Execute shell

add execute shell

切换的 Podfile 文件目录下,执行 pod install 命令

1
2
3
4
#!/bin/bash -l
cd ${JOB_NAME}
export LANG=en_US.UTF-8
pod install --verbose --no-repo-update

pod install

接下来添加另一个 build step,这次选择的是 Xcode。

使用 Cocoapods,Target 这栏不用填,点击右侧的 Settings,按截图设置下。

Xcode general settings

Code signing & OS X keychain options 可以不设置。

Xcode advanced options

这里有个注意的地方,在 Xcode9 以前,可以勾选底下的 Pack application, build and sign .ipa?,并设置些相关的信息就能导出 .ipa。但是使用 Xcode9 会报这样的错误:

1
"Error Domain=IDEProvisioningErrorDomain Code=9 \"\"MyApp.app\" requires a provisioning profile.\" UserInfo={NSLocalizedDescription=\"MyApp.app\" requires a provisioning profile., NSLocalizedRecoverySuggestion=Add a profile to the \"provisioningProfiles\" dictionary in your Export Options property list.}"

大概意思就是需要指定 ExportOptions.plist,这个文件内容大概是这样的:

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>compileBitcode</key>
<false/>
<key>method</key>
<string>ad-hoc</string>
<key>provisioningProfiles</key>
<dict>
<key>⚠️ Bundle ID </key>
<string>⚠️ Provisioning Profile </string>
</dict>
<key>signingCertificate</key>
<string>iPhone Distribution</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>⚠️ teamID </string>
<key>thinning</key>
<string>&lt;none&gt;</string>
</dict>
</plist>

可以拷贝然后修改一下相应的 key value。不过一个简单获取这个文件的方式是:手动使用 Xcode9 打包,在导出 .ipa 的文件夹里应该有 4 个文件,其中一个就是 ExportOptions.plist。可以把它拷贝到 workspace 目录下。

☕️ 补充:如果 Signing 勾选了 Automatically manage signing,就简单多了,它的 exportOptons.plist 是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>compileBitcode</key>
<false/>
<key>method</key>
<string>ad-hoc</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>⚠️ teamID</string>
<key>thinning</key>
<string>&lt;none&gt;</string>
</dict>
</plist>

image-20180531155611396

在 Xcode step 下面再添加一个 Execute shell

1
xcodebuild -exportArchive -archivePath ${WORKSPACE}/build/${JOB_NAME}.xcarchive -exportPath ${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_NUMBER}/archive -exportOptionsPlist ${WORKSPACE}/ExportOptions.plist

-exportOptionsPlist ${WORKSPACE}/ExportOptions.plist 这个选项就是指定刚刚拷贝到 workspace 的 plist。

到这里配置就完成了。回到 item 页面,点击 Build Now。

Build Now

左侧就能看到 building 的项目了,点击 Console Output 可以看到 log 记录,如无意外最后输出 Finished: SUCCESS

可以在两个目录找到 .xcarchive.ipa :

1
2
/Users/yourname/.jenkins/workspace/jobName/build/jobName.xcarchive
/Users/yourname/.jenkins/jobs/jobName/builds/buildNumber/archive

配置 Git Parameter

有时候想指定打某个分支的包,用 Git Parameter 就很方便了。

回到配置页最顶部,勾选 This project is parameterized,选择 Git Parameter

add parameter

Git Parameter 配置

Name 后面需要用到,Type 这里就选 Branch 了。

再到 Source Code Management,修改一下设置。

Source code management

红框部分修改为刚刚设置的 Name,以 $ 开头。

Build with Parameters

这时 Build Now 就变成 Build with Parameters 了。右侧选择分支,然后开 build。

这个插件适合手动构建,如果设置了轮询自动构建,会因为找不到分支而构建失败,有什么好的方案告诉我啊。

image-20180703103936525

点击右侧的 Advanced,有一栏「Default Value」可以指定默认的分支。这样轮询构建的时候就会自动选择这个分支。

获取 Bundle ID 和 Provisioning Profile 生成 ExportOptions.plist

☕️ 补充:Signing 勾选了 Automatically manage signing 可以不操作这步。

现在可以指定任意的分支打包了,但可能每个分支的 Bundle ID 或者 Provisioning Profile 跟之前准备的 exportOptions 不一致,这样就得准备多一份 ExportOptions.plist。就写个脚本来获取吧。

方案是这样的,先准备一份 exportOptions 模版,通过 xcodebuild -showBuildSettings 获取 Bundle ID 和 Provisioning Profile,替换模版生成一个新的 .plist,export 命令指定这个新的 .plist 就好了。

xcodebuild -exportArchive 前 Add execute shell

这里就玩玩 Swift 脚本了,在文本开头加上 !/usr/bin/env swift

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
#!/usr/bin/env swift

// 这是一段 Swift 写的脚本(好玩)
// 作用是从 xcodebuild 中获取 bundle id 和 provisioning profile
// 输出新的 export options plist,在导出 ipa 时需要用到。

import Foundation

func executeShell(_ arguments: String...) -> (status: Int32, output: String) {
let process = Process()
process.launchPath = "/usr/bin/env"
process.arguments = arguments

let pipe = Pipe()
process.standardOutput = pipe

process.launch()
process.waitUntilExit()

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
return (process.terminationStatus, output ?? "")
}

let profiles = executeShell("xcodebuild", "-showBuildSettings").output
.components(separatedBy: "\n")
.filter {
$0.range(of: "PRODUCT_BUNDLE_IDENTIFIER") != nil || $0.range(of: "PROVISIONING_PROFILE_SPECIFIER") != nil
}
.map {
$0.replacingOccurrences(of: " ", with: "").components(separatedBy: "=").last ?? ""
}
print("profiles: \(profiles)")

let directoryPath = "/Users/yourname/Documents/ExportOptions/"
let options = NSMutableDictionary(contentsOfFile: directoryPath + "ExportOptions_sample.plist")
options?["provisioningProfiles"] = [profiles[0] : profiles[1]]
options?.write(toFile: directoryPath + "ExportOptions.plist", atomically: true)

后面的 -exportOptionsPlist 指定 directoryPath/ExportOptions.plist

单元测试

采用 xcodebuild test + xcpretty 方案。

xcpretty 可以将 xcodebuild test 输出内容格式化,并且可以导出 xml、json 或者 html。

格式化的内容大概是这样子的:

1
2
3
4
5
6
7
8
9
10
   ✓ testExample (0.001 seconds)
◷ testPerformanceExample measured (0.000 seconds)
✓ testPerformanceExample (0.362 seconds)
✗ testString, XCTAssertTrue failed - 返回不是「你好世界」,测试不通过

testString, XCTAssertTrue failed - 返回不是「你好世界」,测试不通过
xx.swift:47
func testString() {
XCTAssert(viewCtrl.returnAString() == "你好世界", "返回不是「你好世界」,测试不通过")
}

首先安装 xcpretty :

1
gem install xcpretty

Add execute shell:

1
2
3
4
5
6
7
xcodebuild test \
-workspace ${JOB_NAME}.xcworkspace \
-scheme ${JOB_NAME} \
-destination 'platform=iOS Simulator,name=iPhone 7' \
| xcpretty -s -r html \
--output ${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_NUMBER}/tests.html \
&& exit ${PIPESTATUS[0]}

&& exit ${PIPESTATUS[0]} 使用 exit 参数帮助 Jenkins 确定是否失败。

可以把这段 shell 放在 pod install 之后,测试失败后 build 失败而不继续 archive。

上传 ipa 到测试平台

你可以找到平台提供的集成文档,例如 fir.im Jenkins 插件使用方法 或者 蒲公英 - 使用 Jenkins 实现持续集成 (iOS)

这里演示使用 ios-ipa-server 搭建「本地自签名 https 服务器,快速安装 ipa」。

安装 ios-ipa-server

1
npm install -g ios-ipa-server

开启 ios-ipa-server

1
2
3
4
5
6
7
8
$ cd /path/of/ipa
$ ios-ipa-server

# or

$ ios-ipa-server /path/of/ipa

# open https://ip:port/download on your iphone

将导出的 ipa 文件复制到 ios-ipa-server 目录

add Execute Shell:

1
2
3
4
5
#!/bin/bash -l
cd ${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_NUMBER}/archive
DATE=$(date +%m%d-%H:%M)
DISPLAY_NAME=$(/usr/libexec/PlistBuddy -c "print CFBundleDisplayName" ${WORKSPACE}/${JOB_NAME}/Info.plist)
cp ${JOB_NAME}.ipa ~/Documents/ipa/${DISPLAY_NAME}${DATE}.ipa

关于证书问题,iOS 10.3 (不确定版本了) 以上除了需要安装描述文件外,还需要到「关于本机」- 「证书信任设置」将 ios-ipa-server 开启。

Build Triggers

可以设置一些触发构建条件。

Trigger builds remotely

image-20180703105112845

在远程终端就可以通过以下指令来触发构建:

1
curl http://username:password@ipAddr:8080/job/jobName/build\?token\=tokenName

或者 /buildWithParameters?,指定 Git Parameter 的分支 &Branches=master

1
curl http://username:password@ipAddr:8080/job/jobName/buildWithParameters\?token\=tokenName\&Branches\=master
Poll SCM

Poll SCM 可以设置一个轮询周期,当 Git 上有代码更新才会触发构建:

image-20180703110056338

可能出现的错误和解决

pod: command not found

Execute shell 第一行加上 !/bin/bash -l

1
2
3
4
#!/bin/bash -l
cd FirstiOSItem
export LANG=en_US.UTF-8
pod install --verbose --no-repo-update

export 语句将控制台语言环境设置为 UTF-8

xcode-select: error: tool ‘xcodebuild’ requires Xcode

xcode 路径错误,在终端输入以下:

1
2
3
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/
xcodebuild -showsdks
xcrun --sdk iphoneos --show-sdk-path

error: exportArchive: “xx.app” requires a provisioning profile Jenkins

正文已解决,参考链接

最后,备份 Jenkins 配置

理论上,把 Jenkins Home,也就是用户目录下 .jenkins 文件夹,打个 .zip 保存起来就可以了。thinBackup 就能做到这些,可以完全备份或者差异备份。

不过这里用的是 GitHub 上的一个脚本(Backup Jenkins home periodicallly with git.),将 *.xml 配置文件保存到 git。

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
#!/bin/bash

# Setup
#
# - Create a new Jenkins Job
# - Mark "None" for Source Control Management
# - Select the "Build Periodically" build trigger
# - configure to run as frequently as you like
# - Add a new "Execute Shell" build step
# - Paste the contents of this file as the command
# - Save
#
# NOTE: before this job will work, you'll need to manually navigate to the $JENKINS_HOME directory
# and do the initial set up of the git repository.
# Make sure the appropriate remote is added and the default remote/branch set up.
#

# Jenkins Configuraitons Directory
cd $JENKINS_HOME
pwd

# Add general configurations, job configurations, and user content
git add -- *.xml jobs/*/*.xml userContent/*

# only add user configurations if they exist
if [ -d users ]; then
user_configs=`ls users/*/config.xml`

if [ -n "$user_configs" ]; then
git add $user_configs
fi
fi

# mark as deleted anything that's been, well, deleted
to_remove=`git status | grep "deleted" | awk '{print $3}'`

if [ -n "$to_remove" ]; then
git rm --ignore-unmatch $to_remove
fi

git commit -m "Automated Jenkins commit"

git push -q -u origin master

backup jenkins

新建一个 Item,设置一个周期性 Build Trigger。H 18 * * 5 表示每周五 18 点…

Add Execute shell,将上面脚本填进去就行了。