使用 Jenkins 来进行 Android/iOS 项目的持续集成,可以提高开发效率、优化开发流程。
AppX @ifeegoo https://www.ifeegoo.com/appx.html。
由于本部门以开发移动端 Android/iOS App 为主,为了提高开发效率、优化开发流程,准备引入 Jenkins 来进行 Android/iOS 项目的持续集成。
Tips:
1.由于 iOS 开发环境的特殊性:很多编译需要在 macOS 环境下处理。所以本文里面的 Jenkins 是搭建在 macOS 环境中的,虽然在其他平台(Windows/Linux)可以安装 Jenkins 并解决 iOS 开发环境的问题(例如:反向远程调用 macOS 系统环境),但是这样做,还是比较麻烦,当然更不建议使用“黑苹果”技术,同时 Android 在 macOS 下的开发环境也没有什么问题,所以我们直接选用 macOS 来作为 Jenkins 环境。
2.实际的生产环境中,我们可以采用一台性能较好的 Mac mini 或者 Mac Pro 来作为局域网服务器,专门用于 Jenkins 结合 CI/CD 这块的服务。
3.强烈推荐在 macOS 上采用 Homebrew 命令来安装 Jenkins,而不是通过官方网站上下载的 .pkg 包来安装,实际实践过程中,发现通过 .pkg 包来安装的 Jenkins 存在各种各样的问题,比如 SSH 配置问题,Git Clone 超时问题,Provisioning Profile 读取权限问题等,很难去解决!而通过 Homebrew 命令来安装的 Jenkins 就没有这些问题!
环境:
macOS: Mojave version 10.14
Jenkins: 2.138.3
安装方式:
Homebrew 命令安装
无论通过哪种方式安装的 Jenkins,都是需要 macOS 上已经安装了 JDK 环境的,如果没有请先自行安装。
Homebrew 是 macOS 上的包管理器,我们首先确认当前 macOS 是否已经安装了 Homebrew,通过 brew help 命令就可以简单确认当期那电脑是否安装了 Homebrew,如果安装了,就有相关信息提示,如果没有就会提示错误!
没有安装 Homebrew 的情况:
ifeegoo$ brew help shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory Error: The current working directory doesn't exist, cannot proceed.
安装了 Homebrew 的情况:
ifeegoo:~ ifeegoo$ brew help Example usage: brew search [TEXT|/REGEX/] brew info [FORMULA...] brew install FORMULA... brew update brew upgrade [FORMULA...] brew uninstall FORMULA... brew list [FORMULA...] Troubleshooting: brew config brew doctor brew install --verbose --debug FORMULA Contributing: brew create [URL [--no-fetch]] brew edit [FORMULA...] Further help: brew commands brew help [COMMAND] man brew https://docs.brew.sh
如果没有安装 Homebrew,我们可以通过以下命令来先安装 Homebrew:/usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”
ifeegoo:~ ifeegoo$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" /System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/universal-darwin18/rbconfig.rb:215: warning: Insecure world writable dir /Users/ifeegoo/Library in PATH, mode 040722 ==> This script will install: /usr/local/bin/brew /usr/local/share/doc/homebrew /usr/local/share/man/man1/brew.1 /usr/local/share/zsh/site-functions/_brew /usr/local/etc/bash_completion.d/brew /usr/local/Homebrew ==> The following new directories will be created: /usr/local/sbin ==> The Xcode Command Line Tools will be installed. Press RETURN to continue or any other key to abort ==> /usr/bin/sudo /bin/mkdir -p /usr/local/sbin Password: ==> /usr/bin/sudo /bin/chmod g+rwx /usr/local/sbin ==> /usr/bin/sudo /bin/chmod 755 /usr/local/share/zsh /usr/local/share/zsh/site-functions ==> /usr/bin/sudo /usr/sbin/chown ifeegoo /usr/local/sbin ==> /usr/bin/sudo /usr/bin/chgrp admin /usr/local/sbin ==> /usr/bin/sudo /bin/mkdir -p /Users/ifeegoo/Library/Caches/Homebrew ==> /usr/bin/sudo /bin/chmod g+rwx /Users/ifeegoo/Library/Caches/Homebrew ==> /usr/bin/sudo /usr/sbin/chown ifeegoo /Users/ifeegoo/Library/Caches/Homebrew ==> /usr/bin/sudo /bin/mkdir -p /Library/Caches/Homebrew ==> /usr/bin/sudo /bin/chmod g+rwx /Library/Caches/Homebrew ==> /usr/bin/sudo /usr/sbin/chown ifeegoo /Library/Caches/Homebrew ==> Searching online for the Command Line Tools ==> /usr/bin/sudo /usr/bin/touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress ==> Installing Command Line Tools (macOS Mojave version 10.14) for Xcode-10.1 ==> /usr/bin/sudo /usr/sbin/softwareupdate -i Command\ Line\ Tools\ (macOS\ Mojave\ version\ 10.14)\ for\ Xcode-10.1 Software Update Tool Downloading Command Line Tools (macOS Mojave version 10.14) for Xcode Downloaded Command Line Tools (macOS Mojave version 10.14) for Xcode Installing Command Line Tools (macOS Mojave version 10.14) for Xcode Done with Command Line Tools (macOS Mojave version 10.14) for Xcode Done. ==> /usr/bin/sudo /bin/rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress ==> /usr/bin/sudo /usr/bin/xcode-select --switch /Library/Developer/CommandLineTools ==> Downloading and installing Homebrew... remote: Enumerating objects: 3335, done. remote: Counting objects: 100% (3335/3335), done. remote: Compressing objects: 100% (12/12), done. remote: Total 8620 (delta 3325), reused 3327 (delta 3323), pack-reused 5285 Receiving objects: 100% (8620/8620), 2.45 MiB | 1.48 MiB/s, done. Resolving deltas: 100% (6406/6406), completed with 797 local objects. From https://github.com/Homebrew/brew ddbefee44..4021aa80d master -> origin/master * [new tag] 1.7.2 -> 1.7.2 * [new tag] 1.7.3 -> 1.7.3 * [new tag] 1.7.4 -> 1.7.4 * [new tag] 1.7.5 -> 1.7.5 * [new tag] 1.7.6 -> 1.7.6 * [new tag] 1.7.7 -> 1.7.7 * [new tag] 1.8.0 -> 1.8.0 * [new tag] 1.8.1 -> 1.8.1 * [new tag] 1.8.2 -> 1.8.2 HEAD is now at 4021aa80d Merge pull request #5310 from sjackman/openjdk ==> Homebrew is run entirely by unpaid volunteers. Please consider donating: https://github.com/Homebrew/brew#donations
安装好 Homebrew 之后,可以通过 brew install jenkins-lts 来安装 LTS 版本的 Jenkins。
ifeegoo:~ ifeegoo$ brew install jenkins-lts Updating Homebrew... ==> Auto-updated Homebrew! Updated 2 taps (homebrew/core and homebrew/cask). ==> Migrating /Library/Caches/Homebrew to /Users/ifeegoo/Library/Caches/Homebrew ==> Deleting /Library/Caches/Homebrew... ==> Updated Formulae ace armor gmsh logstash tomcat ==> Downloading http://mirrors.jenkins.io/war-stable/2.138.2/jenkins.war ==> Downloading from http://mirrors.tuna.tsinghua.edu.cn/jenkins/war-stable/2.13 ######################################################################## 100.0% ==> jar xvf jenkins.war ==> Caveats Note: When using launchctl the port will be 8080. To have launchd start jenkins-lts now and restart at login: brew services start jenkins-lts Or, if you don't want/need a background service you can just run: jenkins-lts ==> Summary 🍺 /usr/local/Cellar/jenkins-lts/2.138.2: 7 files, 75.4MB, built in 17 seconds
安装好 Jenkins 之后,直接访问默认的地址:http://localhost:8080/ 就可以启动默认设置页面了。如果访问这个默认地址不能加载 Jenkins 的,我们需要先通过 brew services start jenkins-lts 来启动 Jenkins。
Tips:
1.通过解锁 Jenkins 页面来看,Homebrew 命令安装的 Jenkins 的目录在:/Users/ifeegoo/.jenkins,而通过 .pkg 包安装的 Jenkins 目录在:/Users/Shared/Jenkins,这个就是为什么 .pkg 安装的 Jenkins 相对于 Homebrew 安装的 Jenkins 问题更多的根本原因!
2.如果你不想用默认的端口号:8080,或者这个端口号已经被占用,可以通过以下位置的文件来修改默认端口号:/usr/local/Cellar/jenkins-lts/2.138.2/homebrew.mxcl.jenkins-lts.plist,当然这个位置的文件根据你的版本号不同而不同。修改第 18 行的 –httpPort= 后面的值。然后通过 brew services start jenkins-lts 命令来重启生效。
<?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>Label</key> <string>homebrew.mxcl.jenkins-lts</string> <key>ProgramArguments</key> <array> <string>/usr/libexec/java_home</string> <string>-v</string> <string>1.8</string> <string>--exec</string> <string>java</string> <string>-Dmail.smtp.starttls.enable=true</string> <string>-jar</string> <string>/usr/local/opt/jenkins-lts/libexec/jenkins.war</string> <string>--httpListenAddress=127.0.0.1</string> <string>--httpPort=8080</string> </array> <key>RunAtLoad</key> <true/> </dict> </plist>
如果你发现在使用 Homebrew 命令安装的 Jenkins 过程中,有很多不容易解决的问题,那么你也可以尝试先卸载 Homebrew 安装的 Jenkins,然后通过 .pkg 包形式来安装!
如果你想要卸载 Jenkins 的话,可以通过 brew uninstall jenkins-lts 命令来卸载 Jenkins。
ifeegoo:~ ifeegoo$ brew uninstall jenkins-lts Uninstalling /usr/local/Cellar/jenkins-lts/2.121.2... (7 files, 74.4MB)
环境:
macOS: Mojave version 10.14
Jenkins: 2.138.3
安装方式:
Jenkins 官方下载 .pkg 包安装
macOS 上 使用 .pkg 包 来Jenkins 的安装相对简单,先去 Jenkins 官方下载地址:https://jenkins.io/download,下载 macOS LTS 版本的 Jenkins。
双击下载的 .pkg 安装包来安装 Jenkins:
安装成功之后,会自动加载浏览器,进行初始化。
环境:
macOS: Mojave version 10.14
Jenkins: 2.138.3
配置问题:
1.初始密码与管理员设置。
2.插件安装。
3.语言设置。
进入到解锁 Jenkins 页面,我们可以直接通过命令行拿到存在相关路径底下的密码,填写然后下一步。
通过 Homebrew 安装的 Jenkins 操作:
ifeegoo:jenkins-lts ifeegoo$ sudo cat /Users/ifeegoo/.jenkins/secrets/initialAdminPassword e1f6b0a66d914c0aa718f034b85a0eb1
通过 .pkg 包安装的 Jenkins 操作:
ifeegoo:~ ifeegoo$ sudo cat /Users/Shared/Jenkins/Home/secrets/initialAdminPassword Password: d0ff3b6dff704f5ca9c188fff0643d82
选择“安装推荐的插件”,然后下一步。
进入到管理员设置页面,设置好相关信息,然后设置 URL,保持默认就好。
进入到以下页面,就表明 macOS 上 Jenkins 的安装与配置成功!
将系统语言修改成英文。这个操作很重要!你可以认为这个操作是为了逼迫自己学习英语,但这个不是主要原因。你在英文环境下,很多专业词语,是有利于你以英文的方式思考和解决问题。我们去到 Jenkins -> 系统管理 -> 插件管理 -> 可选插件,搜索 Locale 这个插件,勾选然后点击直接安装。
去到 Jenkins -> 系统管理 -> 系统设置,搜索 Locale,然后填入 en-US,并且勾选 Ignore browser preference and force this language to all users。然后保存,就可以将系统语言设置成英文了!
环境:
macOS: Mojave version 10.14
Jenkins: 2.138.3
辅助环境:
Git + GitLab
在开始使用 Jenkins 自动化构建 iOS 项目之前,我强烈建议你将 iOS 项目以 Git 形式托管到类似 GitLab 的代码管理系统中!通过 http://localhost:8080/ 可以访问我们已经搭建好的 Jenkins。在开始之前,我们先做一下几个操作:
1.安装几个插件。GitLab 相关插件:GitLab/Gitlab Hook,Xcode 相关插件:Xcode integration / Keychains and Provisioning Profiles Management。Jenkins -> Manage Jenkins -> Manage Plugins -> Available,安装好了之后,重启系统。其他的诸如 Git 这种重要的插件,默认推荐安装里面就包含了。
如果你通过这种方式安装插件出现问题,也是可以在官方插件下载页面,下载 .hpi 格式的 Jenkins 插件,然后手动上传安装插件:Jenkins -> Manage Jenkins -> Manage Plugins -> Advanced -> Upload Plugin。
保持你的 macOS 系统隐藏文件夹可见性,方便后续操作查看某些文件夹,可以在命令行工具中输入:
defaults write com.apple.finder AppleShowAllFiles -bool true
我们还需要通过刚才安装的 keychains and Provisioning Profiles Management 插件,Jenkins -> Manage Jenkins -> Keychains and Provisioning Profiles Management,来配置 Keychains 和 Provisioning Profiles。
去 macOS 以下地址:/Users/{username}/Library/Keychains。我的路径是:/Users/ifeegoo/Library/Keychains,找到 login.keychain 文件,有的时候你可能看到的是 login.keychain-db 文件,没关系,将 login.keychain-db 文件修改成 login.keychain。然后通过 Upload Keychain or Provisioning Profile File -> Choose File -> Upload 上传:
上传好了之后,就会出现以下配置:
Password 一栏就填写当前 macOS 用户的登录密码。Code Signing Identity 这个地方我们填写相关证书的标识符,我目前电脑上配置了一个调试证书和一个 Ad Hoc 发布证书,当然后面你还需要一个 App Store 发布证书,这个操作流程都是一样的,后面自己处理就好。我们去到 Keychain Access -> login,找到你的 iPhone Developer 和 iPhone Distribution 证书,选择其中一个,然后复制标识符,填写到此位置,保存即可!
说明:这个操作步骤,可以理解为 Jenkins 有权以你这台电脑(login.keychain 文件)来获取调试和发布 iOS 应用的证书(证书标识符)。
然后我们拿到调试证书和 Ad Hoc 证书对应的 .mobileprovision 文件,建议去 Apple 开发者后台去下载,也可以去到 /Users/{username}/Library/MobileDevice/Provisioning Profiles 这个路径下去找,但是如果你的配置文件太多的话,你很难分辨是哪个文件,而且这个地方的名称都是 UUID 值标记的。我们拿到对应的 .mobileprovision 文件,同样的位置点击上传,出现以下页面:
会自动识别出 .mobileprovision 文件的 UUID 值。对于 Provisioning Profiles Directory Path,根据大家 macOS 当前登录的用户名的不同,我们填写:/Users/{username}/Library/MobileDevice/Provisioning Profiles。
说明:这个操作步骤,可以理解为 Jenkins 可以读取你位于 /Users/{username}/Library/MobileDevice/Provisioning Profiles 目录下的配置文件,同时知道应该读取那个对应的配置文件(识别 UUID 值)。
保存好了之后,整个 iOS 工程项目相关的证书和配置文件都配置好了。
为了方便访问远程的 Git 仓库,我们需要配置用于登录验证的信息。Jenkins -> Manage Jenkins -> Configure Credentials -> Credentials -> System -> Global Credentials -> Add Credentials。
说明:我们可以通过两种方式来访问远程 Git 仓库,一个是用户名 + 密码的形式,另外一个是 SSH 形式,关于 SSH 的公钥、私钥的制作与关联使用,请参见《在 Mac OS X 上关联 GitHub》。我们这里就追加两种方式的登录信息,以防止一种出现问题,另外一种还可以使用。
用户名和密码形式,Kind 选择:Username with password。Scope 选择 Global,Username 为你登录 GitLab 的用户名,Password 为你登录 GitLab 的密码。ID 默认不用填写,Description 我们这里写一个可以区分当前登录信息的字符串即可。
SSH 形式,Kind 选择:SSH Username with private key,Scope 还是选择 Global,Username 还是 GitLab 登录的用户名,Private Key 我们去到通过我以上文章方式生成的用于 SSH 的公钥和私钥文件夹:/Users/{username}/.ssh:打开 id_rsa,然后复制内容到 Key 里面,Description 还是和上面一样,写一个可以区分当前登录信息的字符串即可。
ifeegoo:.ssh ifeegoo$ ls id_rsa id_rsa.pub known_hosts ifeegoo:.ssh ifeegoo$ cat id_rsa -----BEGIN RSA PRIVATE KEY----- ******************************** ******************************** ******************************** -----END RSA PRIVATE KEY-----
保存好这两种登录方式的信息之后,我们在列表中可以看到以下内容,说明保存成功了!
接下来,我们关联一个远程 GitLab 的 iOS 工程项目:Jenkins -> New Item -> 输入名称 -> Freestyle project -> OK
下一步,我们进行最重要的一个步骤,配置 Jenkins 针对 iOS 项目的相关信息:
相关的配置如下:
1.Description:项目的一个简要的描述。我们的项目是:iOS 蓝牙语音助手。
2.Source Code Management:项目代码版本管理方式。我们的项目是使用 Git 托管在 GitLab 上面,所以我们选择 Git。Repository URL 我们填写 Git 仓库的地址。请注意:如果你选择的是 http:// 打头的 Git 仓库地址,那么 Credentials 就要选择:用户名和密码形式。如果你选择的是 git@ 打头的 Git 仓库地址,那么 Credentials 就要选择:SSH 形式。需要 Build 的分支我们就默认 master。其他参数默认即可。
说明:你选择 git@ SSH 形式访问远程 Git 仓库,有可能会出现以下提示:
你可以参考这篇文章尝试解决:https://www.jianshu.com/p/ed0edb93e234,我个人由于没有解决掉这个问题,就采用 http:// 方式访问了。另外我猜测这个是由于通过 .pkg 包安装的方式来安装的,会生成一个共享的 Jenkins 账户,而通过 Brew 命令来安装的则是会安装到当前用户目录底下,账户之间的权限访问问题,比如你配置的是当前用户底下的 .ssh 信息,但是实际上 Jenkins 尝试去 /Users/Shared/Jenkins 底下获取相关的 .ssh 信息。
3.Build Triggers:构建触发器。就是希望以什么样的机制来触发项目的构建。由于是独立的单个项目,一般采用的方式有两种:Build periodically,无论远程仓库代码是否变化,周期性构建。Poll SCM,当远程仓库代码有变化时,才进行周期性构建。我们目前采用 Poll SCM 形式。大家要注意的就是如何去写这个配置参数,在配置这个参数之前,我们需要了解一个非常重要的概念:Crontab,还有一个非常好的针对 Crontab 的工具网站。了解清楚之后,我们就可以先配置一个测试的 Crontab:*/5 * * * *。表示每五分钟构建一次,我上图中填写的是:H/5 * * * *,两个意思是一样的。当你填写 */5 * * * * 的时候,Jenkins 会提醒你写成:H/5 * * * * 形式更好。
4.Build Environment:构建环境。这个地方主要配置的是针对 iOS 项目的证书和配置文件。勾选上:Keychains and Code Signing Identities,这个时候出现 login.keychain 的信息。但是可能会出现 Code Signing Identity 没有可以选择的情况,你需要先点击保存一下,让相关的数据加载,不然你是无法选择这个 Code Signing Identity。这是一个小坑!我们选择 iPhone Developer 的证书,那么底下勾选上:Mobile Provisioning Profiles 之后,选择的配置文件是要和 iPhone Developer 证书对应。当你选择 iPhone Distribution 证书之后,Mobile Provisioning Profiles 要选择和 iPhone Distribution 证书对应的配置文件。
5.Build:构建。我们选择 Execute shell,执行一段 Shell 命令。我们这边执行 Shell 命令,需要注意针对 iOS 项目是否有引入 CocoaPods 而不同。有经验的 iOS 开发者都知道,如果你引入了 CocoaPods,那么你需要通过 .xcworkspace 文件载入工程,而不是 . xcodeproj 文件载入工程。
引入了 CocoaPods 的项目编译的 Shell 命令示例:
# 项目名称:iOS 项目工程中的名称 PROJECT_NAME="VoiceAssistant" # Scheme 名称:iOS 项目工程中 Scheme 名称 SCHEME_NAME="VoiceAssistant" # Info.Plist 文件的路径,请注意有时候 Info.Plist 的文件名和路径可能不同 SCHEME_INFO_PLIST_PATH="./${PROJECT_NAME}/Info.plist" # 从 Info.Plist 文件中读取版本号 VERSION=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${SCHEME_INFO_PLIST_PATH}") # 从 Info.Plist 文件中读取构建号 BUILD=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${SCHEME_INFO_PLIST_PATH}") # 获取当前日期 DATE="$(date +%Y%m%d)" # 编译之后的 .ipa 文件名称 IPA_NAME="${SCHEME_NAME}_v${VERSION}_${DATE}" # 编译之后的 .xcarchive 文件名称 ARCHIVE_NAME="${SCHEME_NAME}_v${VERSION}_${DATE}" # 编译之后的 .ipa 文件导出路径 IPA_EXPORT_PATH="/Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME}/${VERSION}/" # 存档打包导出 .ipa 文件 xcodebuild archive -scheme ${SCHEME_NAME} -archivePath ${IPA_EXPORT_PATH}${IPA_NAME}.xcarchive -workspace ${PROJECT_NAME}.xcworkspace xcodebuild -exportArchive -archivePath ${IPA_EXPORT_PATH}${ARCHIVE_NAME}.xcarchive -exportPath ${IPA_EXPORT_PATH}${IPA_NAME}.ipa -exportOptionsPlist /Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME}/ExportOptions.plist
未引入 CocoaPods 的项目编译的 Shell 命令示例:
# 项目名称:iOS 项目工程中的名称 PROJECT_NAME="VoiceAssistant" # Scheme 名称:iOS 项目工程中 Scheme 名称 SCHEME_NAME="VoiceAssistant" # Info.Plist 文件的路径,请注意有时候 Info.Plist 的文件名和路径可能不同 SCHEME_INFO_PLIST_PATH="./${PROJECT_NAME}/Info.plist" # 从 Info.Plist 文件中读取版本号 VERSION=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${SCHEME_INFO_PLIST_PATH}") # 从 Info.Plist 文件中读取构建号 BUILD=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${SCHEME_INFO_PLIST_PATH}") # 获取当前日期 DATE="$(date +%Y%m%d)" # 编译之后的 .ipa 文件名称 IPA_NAME="${SCHEME_NAME}_v${VERSION}_${DATE}" # 编译之后的 .xcarchive 文件名称 ARCHIVE_NAME="${SCHEME_NAME}_v${VERSION}_${DATE}" # 编译之后的 .ipa 文件导出路径 IPA_EXPORT_PATH="/Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME}/${VERSION}/" # 存档打包导出 .ipa 文件 xcodebuild archive -scheme ${SCHEME_NAME} -archivePath ${IPA_EXPORT_PATH}${IPA_NAME}.xcarchive -workspace ${PROJECT_NAME}.xcodeproj xcodebuild -exportArchive -archivePath ${IPA_EXPORT_PATH}${ARCHIVE_NAME}.xcarchive -exportPath ${IPA_EXPORT_PATH}${IPA_NAME}.ipa -exportOptionsPlist /Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME}/ExportOptions.plist
当然,执行以上命令的前提是当前 macOS 已经安装了 Xcode!
你一定注意到了,上面针对于是否引入 CocoaPods 不同的 Shell 命令,唯一的区别就是在 23 行最后面载入不同格式的工程文件。另外你也可能注意到了,编译的 Shell 命令里面有一个 ExportOptions.plist 的文件,有些疑惑这个文件是哪里来的,第一次的时候,我们可以通过导出对应的 Development/Ad Hoc/App Store 包,里面有这几个文件:
DistributionSummary.plist ExportOptions.plist VoiceAssistant.ipa Packaging.log
通过 Xcode 导出相对应的包,产生的文件夹中就有这个 ExportOptions.plist 文件,如果你想要 Build Development 的包,你就需要通过 Development 证书和对应的配置文件来导出包,获得这个文件。如果你想要 Build Ad Hoc 的包,你就需要通过 Ad Hoc 证书和对应的配置文件来导出包,获得这个文件。ExportOptions.plist 文件内容示例:
<?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> <true/> <key>destination</key> <string>export</string> <key>method</key> <string>ad-hoc</string> <key>provisioningProfiles</key> <dict> <key>com.chipsguide.app.ble.voicecontroller</key> <string>ad_hot_distribution</string> </dict> <key>signingCertificate</key> <string>FB8D60D89AFC74B44B4F7315F1C9F632CAB8B2D3</string> <key>signingStyle</key> <string>manual</string> <key>stripSwiftSymbols</key> <true/> <key>teamID</key> <string>GR3FK25RUR</string> <key>thinning</key> <string><none></string> </dict> </plist>
以上是通过 Xcode 导出的 Ad Hoc 包的 ExportOptions.plist 文件,这个记录当前工程导出的相关配置。注意:我们将以上配置文件中第六行修改成 false,因为 compileBitcode 默认的 true 可能导致编译失败。然后将此文件放置到:/Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/ 目录底下:/Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/ExportOptions.plist。
至此,所有的准备工作都已经就绪,去到工程页面,点击左侧的:Build Now,第一次通过手动触发构建。
左下角显示第一次构建的进度,最后变成了红色。很显然构建失败了,这个也很正常,一次成功的例子在开发中是不常见的。不要慌,点击编译失败的任务,弹出框选择:Console Output
可以看到有以下错误日志:
Console Output Started by user root Building in workspace /Users/Shared/Jenkins/Home/workspace/ios-bluetooth-voice-assistant Cloning the remote Git repository Cloning repository http://192.168.0.20/gitlab/chipsguide/ios-bluetooth-soundbox-ali-2819.git > git init /Users/Shared/Jenkins/Home/workspace/ios-bluetooth-voice-assistant # timeout=10 Fetching upstream changes from http://192.168.0.20/gitlab/chipsguide/ios-bluetooth-soundbox-ali-2819.git > git --version # timeout=10 using GIT_ASKPASS to set credentials gitlab-password-yugancheng > git fetch --tags --progress http://192.168.0.20/gitlab/chipsguide/ios-bluetooth-soundbox-ali-2819.git +refs/heads/*:refs/remotes/origin/* ERROR: Timeout after 10 minutes ERROR: Error cloning remote repo 'origin' hudson.plugins.git.GitException: Command "git fetch --tags --progress http://192.168.0.20/gitlab/chipsguide/ios-bluetooth-soundbox-ali-2819.git +refs/heads/*:refs/remotes/origin/*" returned status code 143: stdout: stderr: at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.launchCommandIn(CliGitAPIImpl.java:2016) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.launchCommandWithCredentials(CliGitAPIImpl.java:1735) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.access$300(CliGitAPIImpl.java:72) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl$1.execute(CliGitAPIImpl.java:420) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl$2.execute(CliGitAPIImpl.java:629) at hudson.plugins.git.GitSCM.retrieveChanges(GitSCM.java:1146) at hudson.plugins.git.GitSCM.checkout(GitSCM.java:1186) at hudson.scm.SCM.checkout(SCM.java:504) at hudson.model.AbstractProject.checkout(AbstractProject.java:1208) at hudson.model.AbstractBuild$AbstractBuildExecution.defaultCheckout(AbstractBuild.java:574) at jenkins.scm.SCMCheckoutStrategy.checkout(SCMCheckoutStrategy.java:86) at hudson.model.AbstractBuild$AbstractBuildExecution.run(AbstractBuild.java:499) at hudson.model.Run.execute(Run.java:1819) at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:43) at hudson.model.ResourceController.execute(ResourceController.java:97) at hudson.model.Executor.run(Executor.java:429) ERROR: Error cloning remote repo 'origin' Finished: FAILURE
很明显我们可以看到,上面提示 timeout=10,针对这种超时的提醒,对 Git 有经验的开发者来说,已经很熟悉了。意思就是 10 分钟的 Git Clone 动作已经超时了,一般会出现在初次 Clone 时网络不太好或者远程 Git 仓库过大造成的,我们现在要做的就是调大超时时间:
还是去到当前项目的配置页面:Source Code Management -> Additional Behaviours -> Advanced clone behaviours -> Timeout (In Minutes) for clone and fetch operations -> 设置成 60 (分钟),根据自己的网络实际情况和远程仓库的大小。保存之后,再次手动构建一下。
当你发现时间设置成 120 分钟这种超长时间,然后 Git Clone 还是一直卡在那里,同时我们也尝试了去 Clone 一个非常小的项目,确定还是同样的情况,这个就说明,不是超时时间设置的问题,网上有人说可能是因为防火墙设置,导致 Git Clone 出现问题,推荐使用 SSH 方式 Clone 项目,但是前面说了,通过 .pkg 文件安装的 Jenkins,是挂在 Jenkins 用户底下的,需要单独针对这个 Jenkins 来配置 SSH 信息,我也没有解决这个问题,所以,当你遇到这个问题之后,推荐采用 Brew 命令来安装 Jenkins,而且在实际的时间过程中,你会发现通过 .pkg 包安装的 Jenkins,要解决很多问题,有的问题还不好解决,而通过 Brew 命令安装的 Jenkins,相对来说,出问题的情况少些!!!
当你发现通过 .pkg 安装的 Jenkins 有太多问题解决不了的话,先卸载,然后通过 Homebrew 命令行来安装 Jenkins!
// 调用 Jenkins 卸载命令工具 ifeegoo:Jenkins ifeegoo$ '/Library/Application Support/Jenkins/Uninstall.command' Jenkins uninstallation script The following commands are executed using sudo, so you need to be logged in as an administrator. Please provide your password when prompted. + sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist Password: + sudo rm /Library/LaunchDaemons/org.jenkins-ci.plist + sudo rm -rf /Applications/Jenkins '/Library/Application Support/Jenkins' /Library/Documentation/Jenkins + sudo rm -rf /Users/Shared/Jenkins + sudo rm -rf /var/log/jenkins + sudo rm -f /etc/newsyslog.d/jenkins.conf + sudo dscl . -delete /Users/jenkins + sudo dscl . -delete /Groups/jenkins + pkgutil --pkgs + grep 'org\.jenkins-ci\.' + xargs -n 1 sudo pkgutil --forget Forgot package 'org.jenkins-ci.support.pkg' on '/'. Forgot package 'org.jenkins-ci.documentation.pkg' on '/'. Forgot package 'org.jenkins-ci.launchd-jenkins.pkg' on '/'. Forgot package 'org.jenkins-ci.jenkins21383.postflight.pkg' on '/'. Forgot package 'org.jenkins-ci.jenkins.osx.pkg' on '/'. + set +x Jenkins has been uninstalled. // 删除被忽略的文件 ifeegoo:Jenkins ifeegoo$ sudo rm -rf /var/root/.jenkins ~/.jenkins
通过 Homebrew 命令来安装 Jenkins 之后,前面的配置步骤都是一样的!我们同样尝试 SSH 形式访问远程 Git 仓库。再次 Build,发现没有 Git Clone 超时的问题!
Build 一次,失败了,查看失败原因:大致问题是 Shell command 有问题。
[ios-bluetooth-voice-assistant] $ /bin/sh -xe /var/folders/vl/nvypcrfj25n20fhnmtl0t5p40000gn/T/jenkins4816936349464943236.sh + 2 /var/folders/vl/nvypcrfj25n20fhnmtl0t5p40000gn/T/jenkins4816936349464943236.sh: line 3: 2: command not found Build step 'Execute shell' marked build as failure Finished: FAILURE
查了一下,发现自己在复制 Shell 命令的时候,把 Shell 命令代码前面的行号复制进去了,导致 Shell 编译出错!!!解决好错误再次运行!还是报错,不要紧,慢慢来!我们去查看错误日志,这次错误日志非常多,我们直接搜索 Failed 关键词,找到以下错误的地方:看起来应该是针对 Swift 编译配置的问题,因为本项目大部分源代码是 Swift 来写的。
** ARCHIVE FAILED ** The following build commands failed: CompileSwift normal arm64 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Voice/Controller/LXSkillSettingViewController.swift CompileSwiftSources normal arm64 com.apple.xcode.tools.swift.compiler CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Main/View/ESTabBarItemContentView.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Lib/VoiceLib/LXRecognizeResult.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Voice/Model/LXJsonDecoder.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Lib/VoiceLib/LXRecordData.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Device/Controller/LXRemoteViewController.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Device/Controller/LXSettingViewController.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Other/LXConst.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Device/View/LXEditTopView.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Main/View/LXGifView.swift CompileSwift normal armv7 /Users/ifeegoo/.jenkins/workspace/ios-bluetooth-voice-assistant/VoiceAssistant/Voice/Controller/LXSkillSettingViewController.swift (12 failures) Build step 'Execute shell' marked build as failure Finished: FAILURE
之前项目没有用到 Swift 的时候,一切都可以编译成功的,初步判定是由于 Swift 代码编译导致的问题!但是比较奇怪的就是,远程的 master 分支的代码是可以正常编译的!而现在却提示这几个 Swift 文件有问题,尝试移除其中一个 Swift 文件,然后编译,还是报错!但是报错的文件减少,再次移除,然后编译成功!重新重置到原来那个节点,然后编译居然通过了,比较奇怪!我猜测可能是因为一些配置环境的问题导致的!下图是最终编译成功的样子!
针对单个 iOS 工程有多个 Scheme (对应多个应用),这种情况下,根据不同的 Scheme 名称来做匹配,相关的编译代码示例如下。
# 项目名称:iOS 项目工程中的名称 PROJECT_NAME="BluetoothColorLamp24G" # Scheme 名称:iOS 项目工程中 Scheme 名称 SCHEME_NAME_A="ilight-lite" SCHEME_NAME_B="econ-light" SCHEME_NAME_C="lumi-light" SCHEME_NAME_D="maoqiu-light" # Info.Plist 文件的路径,请注意有时候 Info.Plist 的文件名和路径可能不同 SCHEME_INFO_PLIST_PATH_A="${SCHEME_NAME_A}.plist" SCHEME_INFO_PLIST_PATH_B="${SCHEME_NAME_B}.plist" SCHEME_INFO_PLIST_PATH_C="${SCHEME_NAME_C}.plist" SCHEME_INFO_PLIST_PATH_D="${SCHEME_NAME_D}.plist" # 从 Info.Plist 文件中读取版本号 VERSION_A=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${SCHEME_INFO_PLIST_PATH_A}") VERSION_B=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${SCHEME_INFO_PLIST_PATH_B}") VERSION_C=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${SCHEME_INFO_PLIST_PATH_C}") VERSION_D=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${SCHEME_INFO_PLIST_PATH_D}") # 从 Info.Plist 文件中读取构建号 BUILD_A=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${SCHEME_INFO_PLIST_PATH_A}") BUILD_B=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${SCHEME_INFO_PLIST_PATH_B}") BUILD_C=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${SCHEME_INFO_PLIST_PATH_C}") BUILD_D=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${SCHEME_INFO_PLIST_PATH_D}") # 获取当前日期 DATE="$(date +%Y%m%d)" # 编译之后的 .ipa 文件名称 IPA_NAME_A="${SCHEME_NAME_A}_v${VERSION_A}_${DATE}" IPA_NAME_B="${SCHEME_NAME_B}_v${VERSION_B}_${DATE}" IPA_NAME_C="${SCHEME_NAME_C}_v${VERSION_C}_${DATE}" IPA_NAME_D="${SCHEME_NAME_D}_v${VERSION_D}_${DATE}" # 编译之后的 .xcarchive 文件名称 ARCHIVE_NAME_A="${SCHEME_NAME_A}_v${VERSION_A}_${DATE}" ARCHIVE_NAME_B="${SCHEME_NAME_B}_v${VERSION_B}_${DATE}" ARCHIVE_NAME_C="${SCHEME_NAME_C}_v${VERSION_C}_${DATE}" ARCHIVE_NAME_D="${SCHEME_NAME_D}_v${VERSION_D}_${DATE}" # 编译之后的 .ipa 文件导出路径 IPA_EXPORT_PATH_A="/Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME_A}/${VERSION_A}/" IPA_EXPORT_PATH_B="/Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME_B}/${VERSION_B}/" IPA_EXPORT_PATH_C="/Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME_C}/${VERSION_C}/" IPA_EXPORT_PATH_D="/Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME_D}/${VERSION_D}/" # 存档打包导出 .ipa 文件 xcodebuild archive -scheme ${SCHEME_NAME_A} -archivePath ${IPA_EXPORT_PATH_A}${IPA_NAME_A}.xcarchive -workspace ${PROJECT_NAME}.xcworkspace xcodebuild -exportArchive -archivePath ${IPA_EXPORT_PATH_A}${ARCHIVE_NAME_A}.xcarchive -exportPath ${IPA_EXPORT_PATH_A}${IPA_NAME_A}.ipa -exportOptionsPlist /Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME_A}/ExportOptions.plist xcodebuild archive -scheme ${SCHEME_NAME_B} -archivePath ${IPA_EXPORT_PATH_B}${IPA_NAME_B}.xcarchive -workspace ${PROJECT_NAME}.xcworkspace xcodebuild -exportArchive -archivePath ${IPA_EXPORT_PATH_B}${ARCHIVE_NAME_B}.xcarchive -exportPath ${IPA_EXPORT_PATH_B}${IPA_NAME_B}.ipa -exportOptionsPlist /Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME_B}/ExportOptions.plist xcodebuild archive -scheme ${SCHEME_NAME_C} -archivePath ${IPA_EXPORT_PATH_C}${IPA_NAME_C}.xcarchive -workspace ${PROJECT_NAME}.xcworkspace xcodebuild -exportArchive -archivePath ${IPA_EXPORT_PATH_C}${ARCHIVE_NAME_C}.xcarchive -exportPath ${IPA_EXPORT_PATH_C}${IPA_NAME_C}.ipa -exportOptionsPlist /Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME_C}/ExportOptions.plist xcodebuild archive -scheme ${SCHEME_NAME_D} -archivePath ${IPA_EXPORT_PATH_D}${IPA_NAME_D}.xcarchive -workspace ${PROJECT_NAME}.xcworkspace xcodebuild -exportArchive -archivePath ${IPA_EXPORT_PATH_D}${ARCHIVE_NAME_D}.xcarchive -exportPath ${IPA_EXPORT_PATH_D}${IPA_NAME_D}.ipa -exportOptionsPlist /Users/ifeegoo/workspace/jenkins/${PROJECT_NAME}/${SCHEME_NAME_D}/ExportOptions.plist
以上针对多个 Scheme 的 iOS 工程编译导出 .ipa 的 Shell 命令比较粗糙,主要是让大家看这个过程,如果大家对于 Shell 命令和 Xcode 相关命令很熟的话,可以再优化下以上自动化编译过程,由于我自己时间有限,暂时未能做更深入研究,感兴趣的读者,可以继续深入。
问题汇总(其他问题可以在文中找出):
1.获取 /Users/Shared/Jenkins/Home/secrets/initialAdminPassword 提示:Permission denied。
解决方法:.pkg 形式安装的 Jenkins。这个是因为文件夹读写权限的问题,命令行输入
sudo chown -R $(whoami) /Users/Shared/Jenkins/Home/secrets。
2.编译出错:FATAL: String index out of range: 15。
FATAL: String index out of range: 15 java.lang.StringIndexOutOfBoundsException: String index out of range: 15 at java.lang.String.substring(String.java:1963) at com.sic.plugins.kpp.provider.KPPBaseProvisioningProfilesProvider.removeUUIDFromFileName(KPPBaseProvisioningProfilesProvider.java:171) at com.sic.plugins.kpp.model.KPPProvisioningProfile.getProvisioningProfileFilePath(KPPProvisioningProfile.java:76) at com.sic.plugins.kpp.KPPProvisioningProfilesBuildWrapper.copyProvisioningProfiles(KPPProvisioningProfilesBuildWrapper.java:157) at com.sic.plugins.kpp.KPPProvisioningProfilesBuildWrapper.setUp(KPPProvisioningProfilesBuildWrapper.java:99) at hudson.model.Build$BuildExecution.doRun(Build.java:157) at hudson.model.AbstractBuild$AbstractBuildExecution.run(AbstractBuild.java:490) at hudson.model.Run.execute(Run.java:1735) at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:43) at hudson.model.ResourceController.execute(ResourceController.java:97) at hudson.model.Executor.run(Executor.java:405)
这是因为你没有配置好 Provisioning Profile 文件。通过 Manage Jenkins -> Keychains and Provisioning Profiles Management -> Upload Keychain or Provisioning Profile File 上传 Provisioning Profile 文件,然后去项目中配置选择 Provisioning Profile 文件。
3.如果在使用 Jenkins 过程中出现加载空白、加载很缓慢、加载失败。
通过 Homebrew 命令安装的 Jenkins,可以通过 brew services restart jenkins-lts 命令来重启 Jenkins。或者可以通过载入:http://localhost:8080/restart 这个地址来进行重启。
4.用到 Xcode 命令,提示:xcode-select: error: tool ‘xcodebuild’ requires Xcode, but active developer directory。
可以通过 sudo xcode-select -s “/Applications/Xcode.app/Contents/Developer” 命令来解决。
5.Jenkins 更新或者插件跟新的时候,提示:This Jenkins instance appears to be offline。
这种问题一般是由于更新的服务器的网络连接存在一定的问题。可以通过修改 hudson.model.UpdateCenter.xml 文件中的服务器地址,然后重启 Jenkins 来解决问题。
macOS 上通过 .pkg 包安装的 Jenkins,hudson.model.UpdateCenter.xml 文件地址:
/Users/Shared/Jenkins/Home/hudson.model.UpdateCenter.xml
macOS 上通过 Homebrew 安装的 Jenkins,hudson.model.UpdateCenter.xml 文件地址:
/Users/{username}/.jenkins/hudson.model.UpdateCenter.xml
打开这个 .xml 文件:
<?xml version='1.1' encoding='UTF-8'?> <sites> <site> <id>default</id> <url>https://updates.jenkins.io/update-center.json</url> </site> </sites>
将第 5 行里面的地址修改成:
// 将 https 修改成 http http://updates.jenkins.io/update-center.json // 如果你在中国,可以修改成 https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/current/update-center.json
6.安装插件总是失败。
除了尝试更新地址之外,可以去官方下载对应的 .hpi 文件,手动安装。
7.通过 Homebrew 安装的 Jenkins 的相关命令。
一般安装的是 Jenkins LTS 版本,相关命令如下:
// 启动 Jenkins 服务 brew services start jenkins-lts // 重启 Jenkins 服务 brew services restart jenkins-lts // 停止 Jenkins 服务 brew services stop jenkins-lts
8.Jenkins 访问 Provisioning Profiles 文件,提示:java.nio.file.AccessDeniedException: /Users/ifeegoo/Library/MobileDevice。
这个一般是通过 .pkg 包安装的 Jenkins 才会出现这种情况,主要原因就是这种情况下的 Jenkins 是一个特殊的 Shared 用户,从安装目录:/Users/Shared/Jenkins 就可以看出,从 Jenkins 用户访问当前用户系统出现的问题,我曾经尝试过解决这个问题,但是没有解决掉,如果发现有这种情况解决不掉,建议卸载 .pkg 包安装的 Jenkins,然后通过 Homebrew 命令来安装 Jenkins。
FATAL: Failed to copy /Users/Shared/Jenkins/Home/kpp_upload/iOS_Distribution_Ad_Hoc_20180802.mobileprovision to /Users/ifeegoo/Library/MobileDevice/Provisioning Profiles/6bc16df2-1086-4671-9125-a97542632ccc.mobileprovision java.nio.file.AccessDeniedException: /Users/ifeegoo/Library/MobileDevice at sun.nio.fs.UnixException.translateToIOException(UnixException.java:84) at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102) at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107) at sun.nio.fs.UnixFileSystemProvider.checkAccess(UnixFileSystemProvider.java:308) at java.nio.file.Files.createDirectories(Files.java:746) at hudson.FilePath.mkdirs(FilePath.java:3098) at hudson.FilePath.write(FilePath.java:2002) at hudson.FilePath.copyTo(FilePath.java:2122) Caused: java.io.IOException: Failed to copy /Users/Shared/Jenkins/Home/kpp_upload/iOS_Distribution_Ad_Hoc_20180802.mobileprovision to /Users/ifeegoo/Library/MobileDevice/Provisioning Profiles/6bc16df2-1086-4671-9125-a97542632ccc.mobileprovision at hudson.FilePath.copyTo(FilePath.java:2126) at com.sic.plugins.kpp.KPPProvisioningProfilesBuildWrapper.copyProvisioningProfiles(KPPProvisioningProfilesBuildWrapper.java:161) at com.sic.plugins.kpp.KPPProvisioningProfilesBuildWrapper.setUp(KPPProvisioningProfilesBuildWrapper.java:99) at hudson.model.Build$BuildExecution.doRun(Build.java:157) at hudson.model.AbstractBuild$AbstractBuildExecution.run(AbstractBuild.java:504) at hudson.model.Run.execute(Run.java:1798) at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:43) at hudson.model.ResourceController.execute(ResourceController.java:97) at hudson.model.Executor.run(Executor.java:429) Finished: FAILURE
9.导出 .ipa 包过程中报错:FATAL: The path to store mobile provisioning profile files on the master is not configured。
这个是由于在进行远程 Git 仓库 master 分支 Build 的时候,没有配置 Provisioning Profiles 文件路径导致的。去到 Keychains and Provisioning Profiles 插件管理页面,将 Provisioning Profiles Directory Path 配置为:/Users/{username}/Library/MobileDevice/Provisioning Profiles 即可。
FATAL: The path to store mobile provisioning profile files on the master is not configured. Go the plugin main configuration page and give the path. java.io.IOException: The path to store mobile provisioning profile files on the master is not configured. Go the plugin main configuration page and give the path. at com.sic.plugins.kpp.KPPProvisioningProfilesBuildWrapper.copyProvisioningProfiles(KPPProvisioningProfilesBuildWrapper.java:142) at com.sic.plugins.kpp.KPPProvisioningProfilesBuildWrapper.setUp(KPPProvisioningProfilesBuildWrapper.java:99) at hudson.model.Build$BuildExecution.doRun(Build.java:157) at hudson.model.AbstractBuild$AbstractBuildExecution.run(AbstractBuild.java:504) at hudson.model.Run.execute(Run.java:1798) at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:43) at hudson.model.ResourceController.execute(ResourceController.java:97) at hudson.model.Executor.run(Executor.java:429) Finished: FAILURE
10.导出 .ipa 包过程中报错:Error Domain=NSCocoaErrorDomain Code=3840 “No value.”。
这种情况,一般是由于 ExportOptions.plist 文件中的 compileBitcode 设置成了 true 导致的。
+ xcodebuild -exportArchive -archivePath /Users/ifeegoo/Desktop/JenkinsTest.xcarchive -exportPath /Users/ifeegoo/Desktop/JenkinsTest.ipa -exportOptionsPlist /Users/ifeegoo/workspace/jenkins/JenkinsTest/ExportOptions.plist 2018-08-02 22:08:01.091 xcodebuild[3119:139257] [MT] IDEDistribution: -[IDEDistributionLogging _createLoggingBundleAtPath:]: Created bundle at path '/var/folders/vl/nvypcrfj25n20fhnmtl0t5p40000gn/T/JenkinsTest_2018-08-02_22-08-01.090.xcdistributionlogs'. 2018-08-02 22:08:01.460 xcodebuild[3119:139257] [MT] IDEDistribution: Step failed: <IDEDistributionPackagingStep: 0x7fed271c5c70>: Error Domain=NSCocoaErrorDomain Code=3840 "No value." UserInfo={NSDebugDescription=No value., NSFilePath=/var/folders/vl/nvypcrfj25n20fhnmtl0t5p40000gn/T/ipatool-json-filepath-bPPmdW} error: exportArchive: The data couldn’t be read because it isn’t in the correct format. Error Domain=NSCocoaErrorDomain Code=3840 "No value." UserInfo={NSDebugDescription=No value., NSFilePath=/var/folders/vl/nvypcrfj25n20fhnmtl0t5p40000gn/T/ipatool-json-filepath-bPPmdW} ** EXPORT FAILED ** Build step 'Execute shell' marked build as failure Finished: FAILURE
我们将 ExportOptions.plist 文件中的 compileBitcode 修改成 false 即可。
<?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/> **** **** </dict> </plist>
环境:
macOS: Mojave version 10.14
Jenkins: 2.138.3
辅助环境:
Git + GitLab
其实 Android 由于使用 Gradle 进行项目构建,并且可以在 build.gradle 文件中配置相关编译参数。因此采用 Jenkins 来进行 Android 项目的自动化编译,相对 iOS 来说,更加简单。
先配置下本地 JDK 的目录,如果没有安装 JDK,先去下载安装 JDK。可以通过以下命令找到 macOS 上的 JDK 安装目录:
ifeegoo:~ ifeegoo$ /usr/libexec/java_home -V Matching Java Virtual Machines (1): 1.8.0_181, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home
我们本地的 JDK 的路径为:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home 。
去到 Jenkins -> Manage Jenkins -> Global Tool Configuration -> JDK:
名称可以随意一点,填写好路径,保存即可。
再配置下本地 Android SDK 目录(Android SDK 的 安装建议通过安装 Android Studio 来安装)。在 Jenkins -> Manage Jenkins -> Configure System -> Global properties -> Environment variables:Name 填写 ANDROID_HOME,Value 填写 /Users/ifeegoo/Library/Android/sdk,保存即可。
然后配置下 Jenkins 的 Gradle 环境,我建议直接使用 Jenkins 的 Gradle 环境,使用本地的 Gradle 或者 Gradle Wrapper,感觉不是很好。
Jenkins -> Manage Jenkins -> Global Tool Configuration,根据你不同项目采用的不同版本的 Gradle,追加相对应的 Gradle 版本。备注:如果 Gradle 版本不对应,很有可能出现编译出错的问题。
理解:
1.配置 JDK 环境:给基于 JVM 的语言,例如 Android 开发的 Java/Kotlin 的编译环境。
2.配置 Android SDK 环境:针对 Android API 的编译以及 NDK(C/C++) 的处理。
3.配置 Gradle 环境:Gradle 是用来构建 Android 应用的构建工具。
针对单个项目,多渠道的 Android 工程,项目中的 build.gradle 文件如下:
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 27 useLibrary 'org.apache.http.legacy' defaultConfig { applicationId "com.chipsguide.app.ble.voicecontroller" minSdkVersion 18 targetSdkVersion 27 versionCode 109 versionName "1.09" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" multiDexEnabled true ndk { abiFilters 'armeabi-v7a' } } signingConfigs { release { storeFile file("/Users/ifeegoo/workspace/android/keystore/chipsguide.keystore") storePassword "*********" keyAlias "**********" keyPassword "*********" } } sourceSets { main { jniLibs.srcDirs = ['libs'] jniLibs.srcDirs = ['src/main/libs'] jni.srcDirs = ['src/main/jni', 'src/main/jni/'] } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } repositories { flatDir { dirs 'libs' } } lintOptions { checkReleaseBuilds false abortOnError false } flavorDimensions "market" productFlavors { chipsguide { dimension "market" } google_play { dimension "market" } huawei { dimension "market" } xiaomi { dimension "market" } oppo { dimension "market" } vivo { dimension "market" } samsung { dimension "market" } tencent { dimension "market" } baidu { dimension "market" } qihoo360 { dimension "market" } } productFlavors { chipsguide { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "chipsguide",UPDATE_TYPE_VALUE:"self"] } google_play { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "google_play",UPDATE_TYPE_VALUE:"none"] } huawei { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "huawei",UPDATE_TYPE_VALUE:"self"] } xiaomi { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi",UPDATE_TYPE_VALUE:"self"] } oppo { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "oppo",UPDATE_TYPE_VALUE:"self"] } vivo { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "vivo",UPDATE_TYPE_VALUE:"self"] } samsung { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "samsung",UPDATE_TYPE_VALUE:"self"] } tencent { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "tencent",UPDATE_TYPE_VALUE:"self"] } baidu { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu",UPDATE_TYPE_VALUE:"baidu"] } qihoo360 { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "qihoo360",UPDATE_TYPE_VALUE:"360"] } } android.applicationVariants.all { variant -> variant.outputs.all { def time = new Date().format("yyyyMMdd", TimeZone.getTimeZone("UTC")) variant.getPackageApplication().outputDirectory = new File("/Users/ifeegoo/workspace/jenkins/android/ta/${defaultConfig.versionName}") outputFileName = "android-Ta-v${defaultConfig.versionName}-${time}-${variant.productFlavors[0].name}.apk" } } sourceSets { main { jniLibs.srcDirs "libs" } } externalNativeBuild { ndkBuild { path "src/main/jni/Android.mk" } } dexOptions { javaMaxHeapSize "4g" } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:recyclerview-v7:27.1.1' debugImplementation 'me.ele:uetool:1.0.15' releaseImplementation 'me.ele:uetool-no-op:1.0.15' implementation 'com.zhy:okhttputils:2.6.2' implementation 'com.orhanobut:logger:2.2.0' implementation 'pub.devrel:easypermissions:1.2.0' implementation 'com.squareup.okhttp3:okhttp:3.3.1' implementation 'com.squareup.okhttp3:logging-interceptor:3.3.1' implementation 'com.android.support:design:27.1.1' implementation 'com.squareup.retrofit2:retrofit:2.1.0' implementation 'com.squareup.retrofit2:converter-gson:2.3.0' implementation 'com.github.bumptech.glide:glide:3.7.0' implementation 'jp.wasabeef:glide-transformations:2.0.1' implementation 'io.reactivex:rxandroid:1.2.1' implementation 'com.squareup.retrofit2:adapter-rxjava:2.3.0' implementation 'org.greenrobot:eventbus:3.0.0' implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.40' implementation 'com.android.support:multidex:1.0.3' implementation project(':FmxosPlatform_v1.1.9') implementation project(':lxrecorderlibrary') implementation project(':bluetoothlibrary') qihoo360Implementation(name: 'update360Library_20171221', ext: 'aar') baiduImplementation(name: 'updateBaiduLibrary_20171221', ext: 'aar') implementation files('libs/umeng-analytics-7.5.0.jar') implementation files('libs/umeng-common-1.5.0.jar') implementation files('libs/umeng-debug-1.0.0.jar') implementation 'com.orhanobut:hawk:2.0.1' implementation 'cn.bingoogolapple:bga-progressbar:1.0.1@aar' }
以上比较重要的配置有多渠道配置、证书配置、输出 .apk 包配置。
多渠道配置:友盟统计标识区分、更新机制标识、引入不同更新库。
productFlavors { chipsguide { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "chipsguide",UPDATE_TYPE_VALUE:"self"] } google_play { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "google_play",UPDATE_TYPE_VALUE:"none"] } huawei { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "huawei",UPDATE_TYPE_VALUE:"self"] } xiaomi { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi",UPDATE_TYPE_VALUE:"self"] } oppo { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "oppo",UPDATE_TYPE_VALUE:"self"] } vivo { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "vivo",UPDATE_TYPE_VALUE:"self"] } samsung { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "samsung",UPDATE_TYPE_VALUE:"self"] } tencent { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "tencent",UPDATE_TYPE_VALUE:"self"] } baidu { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu",UPDATE_TYPE_VALUE:"baidu"] } qihoo360 { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "qihoo360",UPDATE_TYPE_VALUE:"360"] } } qihoo360Implementation(name: 'update360Library_20171221', ext: 'aar') baiduImplementation(name: 'updateBaiduLibrary_20171221', ext: 'aar')
证书配置:当前项目所用证书文件和证书信息。注意:这个地方我是引用我本地的证书文件。
signingConfigs { release { storeFile file("/Users/ifeegoo/workspace/android/keystore/chipsguide.keystore") storePassword "*********" keyAlias "**********" keyPassword "*********" } }
输出 .apk 配置:针对输出 .apk 包的命名和位置配置。
android.applicationVariants.all { variant -> variant.outputs.all { def time = new Date().format("yyyyMMdd", TimeZone.getTimeZone("UTC")) variant.getPackageApplication().outputDirectory = new File("/Users/ifeegoo/workspace/jenkins/android/ta/${defaultConfig.versionName}") outputFileName = "android-Ta-v${defaultConfig.versionName}-${time}-${variant.productFlavors[0].name}.apk" }
对比 Android 的 Jenkins 过程,我们所有的 Gradle 编译配置信息都在这个文件里面了,我们只需要关联远程 Git 仓库 clone 下代码,然后用对应的 Jenkins 的 Gradle 来编译就可以了,就可以了!
同 iOS 项目一样,关联好远程 Git 仓库之后,在 Build -> Invoke Gradle script -> Invoke Gradle -> Gradle Version,选择与当前项目对应的 Gradle 版本,然后 Tasks 这个地方是重点!针对 Gradle/Gradle Wrapper 有以下格式的命令:
*clean project ./gradlew clean *build project ./gradlew build *build for debug package ./gradlew assembleDebug or ./gradlew aD *build for release package ./gradlew assembleRelease or ./gradlew aR *build for release package and install ./gradlew installRelease or ./gradlew iR Release *build for debug package and install ./gradlew installDebug or ./gradlew iD Debug *uninstall release package ./gradlew uninstallRelease or ./gradlew uR *uninstall debug package ./gradlew uninstallDebug or ./gradlew uD *all the above command + "--info" or "--debug" or "--scan" or "--stacktrace" can get more detail info.
我们使用 assembleRelease,这个命令默认的针对单个项目多渠道导出发布包。
我们来看看单个工程多项目、多渠道的 build.gradle 文件:
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'org.greenrobot.greendao' android { compileSdkVersion 27 defaultConfig { minSdkVersion 21 targetSdkVersion 26 versionCode 117 versionName "1.17" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" multiDexEnabled true manifestPlaceholders = [APP_NAME: 'iLight Lite'] } lintOptions { checkReleaseBuilds false // Or, if you prefer, you can continue to check for errors in release builds, // but continue the build even when errors are found: abortOnError false } sourceSets { main { jniLibs.srcDirs = ['libs'] } } buildTypes { release { aaptOptions.cruncherEnabled = false aaptOptions.useNewCruncher = false minifyEnabled false multiDexEnabled true debuggable false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } ilight.initWith(buildTypes.debug) ilight { applicationIdSuffix ".bluetooth.ilight24g.lite.gp" manifestPlaceholders = [APP_NAME: 'iLight Lite', UMENG_APPKEY_VALUE: '5ac2f35ef29d984fc900006c', UPDATE_FLAG: "com.chipsguide.app.bluetooth.ilight24g.lite"] } econ.initWith(buildTypes.debug) econ { applicationIdSuffix ".ilightlite.econ" manifestPlaceholders = [APP_NAME: '娱人制造', UMENG_APPKEY_VALUE: '5ac2f35ef29d984fc900006c', UPDATE_FLAG: "com.chipsguide.app.ilightlite.econ"] manifestPlaceholders = [APP_NAME: 'Econ Light', UMENG_APPKEY_VALUE: '5ac2f35ef29d984fc900006c', UPDATE_FLAG: "com.chipsguide.app.ilightlite.econ"] } maoqiu.initWith(buildTypes.debug) maoqiu { applicationIdSuffix ".ilightlite.maoqiu" manifestPlaceholders = [APP_NAME: '毛球灯控', UMENG_APPKEY_VALUE: '5b07afc6f43e481f9f000043', UPDATE_FLAG: "com.chipsguide.app.ilightlite.maoqiu"] } Lumi.initWith(buildTypes.debug) Lumi{ applicationIdSuffix ".ilightlite.lumi" manifestPlaceholders = [APP_NAME: 'Lumi Light', UMENG_APPKEY_VALUE: '5bc55ac4b465f5794a000340', UPDATE_FLAG: "com.chipsguide.app.ilightlite.lumi"] } } signingConfigs { debug { storeFile file("/Users/ifeegoo/workspace/android/keystore/chipsguide.keystore") storePassword "*********" keyAlias "*********" keyPassword "*********" } } flavorDimensions "market" productFlavors { chipsguide { dimension "market" } google_play { dimension "market" } huawei { dimension "market" } xiaomi { dimension "market" } oppo { dimension "market" } vivo { dimension "market" } samsung { dimension "market" } tencent { dimension "market" } baidu { dimension "market" } qihoo360 { dimension "market" } } productFlavors { chipsguide { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "chipsguide"] } google_play { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "google_play"] } huawei { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "huawei"] } xiaomi { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"] } oppo { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "oppo"] } vivo { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "vivo"] } samsung { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "samsung"] } tencent { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "tencent"] } baidu { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"] } qihoo360 { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "qihoo360"] } } android.applicationVariants.all { variant -> variant.outputs.all { def time = new Date().format("yyyyMMdd", TimeZone.getTimeZone("UTC")) def name if (buildType.name == "ilight") { name = "ilight-lite" } else { name = buildType.name } outputFileName = "android-${name}-v${defaultConfig.versionName}-${time}-${variant.productFlavors[0].name}.apk" variant.getPackageApplication().outputDirectory = new File("/Users/ifeegoo/workspace/jenkins/android/24g/${name}/${defaultConfig.versionName}") } } buildTypes { release { // signingConfig signingConfigs.config } } } greendao { schemaVersion 9 daoPackage 'com.chipsguide.app.bluetooth.ilight24g.lite.greendao.entitys' targetGenDir 'src/main/java' } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support:design:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation "com.google.code.gson:gson:$gson_version" implementation "com.github.bumptech.glide:glide:$glide_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) implementation 'org.greenrobot:greendao:3.0.1' implementation 'org.greenrobot:greendao-generator:3.0.0' implementation 'com.yanzhenjie:permission:2.0.0-rc4' implementation 'com.zhy:okhttputils:2.6.2' implementation 'com.google.code.gson:gson:2.8.5' testImplementation 'junit:junit:4.12' implementation project(':hsblue24library') implementation project(':zxinglibrary') implementation files('libs/SecurityEnvSDK-release-1.1.0.jar') implementation files('libs/umeng-analytics-7.5.0.jar') implementation files('libs/umeng-common-1.5.0.jar') implementation files('libs/umeng-debug-1.0.0.jar') implementation files('libs/utdid4all-1.1.5.3_proguard.jar') chipsguideImplementation(name: 'updateSelfLibrary_20171221', ext: 'aar') huaweiImplementation(name: 'updateSelfLibrary_20171221', ext: 'aar') xiaomiImplementation(name: 'updateSelfLibrary_20171221', ext: 'aar') oppoImplementation(name: 'updateSelfLibrary_20171221', ext: 'aar') vivoImplementation(name: 'updateSelfLibrary_20171221', ext: 'aar') samsungImplementation(name: 'updateSelfLibrary_20171221', ext: 'aar') tencentImplementation(name: 'updateSelfLibrary_20171221', ext: 'aar') qihoo360Implementation(name: 'updateSelfLibrary_20171221', ext: 'aar') qihoo360Implementation(name: 'update360Library_20171221', ext: 'aar') baiduImplementation(name: 'updateSelfLibrary_20171221', ext: 'aar') baiduImplementation(name: 'updateBaiduLibrary_20171221', ext: 'aar') implementation('com.github.hotchemi:permissionsdispatcher:3.1.0') { // if you don't use android.app.Fragment you can exclude support for them exclude module: "support-v13" } implementation files('libs/pinyin4j-2.5.0.jar') implementation files('libs/AIUI.jar') implementation files('libs/Msc.jar') implementation files('libs/Sunflower.jar') implementation 'com.github.zcweng:switch-button:0.0.3@aar' compile 'org.greenrobot:eventbus:3.0.0' implementation 'pub.devrel:easypermissions:1.2.0' } repositories { flatDir { dirs 'libs' } mavenCentral() } apply plugin: 'kotlin-android-extensions'
我们要注意以上包含了多个应用,多个渠道的配置信息,尤其是要注意的是证书相关的配置,Lumi.initWith(buildTypes.debug) 的这种写法,标识采用 debug 的相关配置信息。有时候可能会出现你配置的证书有问题,导致编译出来的 .apk 包不带有证书信息,无法安装!
针对项目里面编译多个应用多渠道,只需要用以下格式命令即可:
assembleLumi --info assembleecon --info assemblemaoqiu --info assembleilight --info
问题汇总(其他问题可以在文中找出):
1.以上编译过程,可能会出现 build.gradle 找不到。
解决方法:确认当前工程项目的目录,一定要处于文件夹的根目录下。
以上针对 Android/iOS 的 macOS 系统下 Jenkins 自动化编译的流程,还是相对粗糙,其实还有很多诸如:局域网多用户访问、多分支、自动化发布、生成下载二维码等,可以在实际的实践中,根据部门的投入的人力和时间,逐步的完善起来!只有真正的实践和不断的完善,我们才能体会到这个过程中 Jenkins 使用细节、设计的精妙和强大,也能深刻体会 Jenkins 为我们带来的自动化编译的好处!与各位移动开发的兄弟姐妹们共勉!