大致的流程是使用Jenkins來(lái)進(jìn)行持續(xù)構(gòu)建,執(zhí)行OCLint來(lái)進(jìn)行代碼分析,然后將OCLint生成的分析報(bào)告?zhèn)鹘oSonarCube來(lái)對(duì)項(xiàng)目代碼進(jìn)行持續(xù)的分析。對(duì)于項(xiàng)目構(gòu)建可以采取Git提交觸發(fā)或者是定時(shí)任務(wù)的方式。這里面的主要問(wèn)題是:
- 這是個(gè)分散的系統(tǒng),各個(gè)環(huán)節(jié)相互依賴(lài),每個(gè)環(huán)節(jié)都可能出錯(cuò);
- Jenkins的構(gòu)建環(huán)境比較特殊,與我們直接用命令行操作是有差異的;
- SonarCube支持Objective-c的插件是收費(fèi)的,開(kāi)源插件對(duì)最新OCLint的支持不太夠;
- 注意軟件版本、資料的時(shí)效性(Xcode版本需要特別注意);
在我們搭建的過(guò)程中遇到的問(wèn)題基本上屬于上面三個(gè)方面,如果遇到了本文中未出現(xiàn)的問(wèn)題,嘗試從上面幾個(gè)方面入手分析。為了更好的理解整個(gè)流程,最后會(huì)采用一個(gè)開(kāi)源項(xiàng)目進(jìn)行代碼分析。
閱讀說(shuō)明:本篇內(nèi)容步驟較為繁瑣,大概分為三部分內(nèi)容Jenkins安裝、SonarCube安裝、以及OCLint安裝。其中OCLint是核心部分,而這部分最為繁瑣,一定要注意路徑、環(huán)境變量等細(xì)節(jié)設(shè)置。
〇、基礎(chǔ)環(huán)境準(zhǔn)備
因?yàn)椴糠周浖侵苯咏鈮菏褂玫?,為了確保環(huán)境一致,我們現(xiàn)在用戶(hù)目錄下建立一個(gè)jenkins目錄,將所有解壓使用的軟件放在該目錄下。在終端命令行中使用如下命令建立目錄:
cd ~
mkdir jenkins
使用如下命令獲取jenkins目錄的完整路徑:
cd ~/jenkins
pwd
# 我本機(jī)的路徑是 /Users/drsun/jenkins
注意保存該路徑,接下來(lái)會(huì)頻繁使用該路徑。
一、Jenkins安裝
版本 :2.89.4
下載地址:https://jenkins.io/download/
推薦直接使用dmg安裝包,雙擊安裝即可,安裝后的一些調(diào)整(如http端口、卸載等)可以參考這里。
這個(gè)版本Jenkins的插件服務(wù)器啟用了https,安裝過(guò)程中會(huì)出現(xiàn)如下提示:
This Jenkins instance appears to be offline
解決的方式是將hudson.model.UpdateCenter.xml文件中的https改為http,在Mac系統(tǒng)下該文件位于/Users/Shared/Jenkins/Home/hudson.model.UpdateCenter.xml。(注意需要使用sudo權(quán)限修改)
修改完成后,執(zhí)行如下命令重啟Jenkins服務(wù):
sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist
sudo launchctl load /Library/LaunchDaemons/org.jenkins-ci.plist
重啟完成后,繼續(xù)完成Jenkins配置,插件安裝直接默認(rèn)安裝即可,注意一定要安裝Git插件,如下圖所示:

Jenkins服務(wù)默認(rèn)的監(jiān)聽(tīng)端口是8080,這個(gè)端口比較常用,可以通過(guò)如下命令來(lái)調(diào)整端口,避免與其它服務(wù)沖突。
sudo defaults write /Library/Preferences/org.jenkins-ci httpsPort 8100
這里我們將Jenkins的端口改為了8100,使用之前的命令重啟Jenkins服務(wù),接下來(lái)就可以使用http://localhost:8100 來(lái)訪(fǎng)問(wèn)Jenkins了。
二、SonarCube安裝
版本 :6.7.2
下載地址:https://www.sonarqube.org/downloads/
由于Objective-c分析插件不支持最新的7.0版本,所以只能使用6.7版本的SonarCube。SonarCube的安裝比較簡(jiǎn)單,直接解壓到之前的jekins目錄即可。使用如下命令啟動(dòng)SonarCube:
~/jenkins/sonarqube-6.7.2/bin/macosx-universal-64/sonar.sh
啟動(dòng)之后,可以通過(guò)http://localhost:9000/來(lái)訪(fǎng)問(wèn)SonarCube。SonarCube的配置不多,如果需要在生產(chǎn)環(huán)境中使用,需要再配置數(shù)據(jù)庫(kù)連接,這里我們使用內(nèi)置的數(shù)據(jù)庫(kù)來(lái)跑通流程。
從這里下載SonarCube的Objective-c插件(版本:0.6.2),將jar文件拷貝到~/jenkins/sonarqube-6.7.2/extensions/plugins目錄下,通過(guò)Administration->System->System Info->RestartServer重新啟動(dòng)SonarCube。之后可以通過(guò)Administration->System->Update Center->Installed查看安裝好的Objective-c插件,如下圖所示:

三、OCLint安裝
這一步中需要安裝三個(gè)軟件,其中xcpretty就將xcodebuild的輸出轉(zhuǎn)化為oclint所需要的json文件,然后使用oclint附帶的oclint-json-compilation-database命令對(duì)項(xiàng)目進(jìn)行分析生成分析報(bào)告,最后通過(guò)SonarScanner將分析報(bào)告發(fā)送至SonarCube完成一個(gè)完整的分析過(guò)程。
1. 安裝OCLint(版本:0.13.1)
從這里下載,將oclint-0.13.1-x86_64-darwin-17.4.0.tar.gz 解壓至~/jenkins目錄。
2. 安裝xcpretty(版本:0.28)
使用如下命令安裝xcpretty:
sudo gem install xcpretty
安裝完成后使用如下命令檢查版本:
xcpretty -v
3. 安裝SonarScanner(版本:3.0.3.778)
從這里下載安裝包,將安裝包解壓至~/jenkins目錄。
4. 設(shè)置PATH變量
因?yàn)槲覀冃枰獜慕K端直接執(zhí)行oclint等,所以需要將它們添加至PATH變量,如果你使用的是bash(一般默認(rèn)的都是bash),通過(guò)如下命令設(shè)置PATH,并使其生效(注意如下命令中的路徑,將drsun修改為你的用戶(hù)名,該路徑也就是我們?cè)诘讴柌綇?qiáng)調(diào)的路徑)
echo "export PATH=$PATH:/Users/drsun/jenkins/oclint-0.13.1/bin" >> ~/.bashrc
echo "export PATH=$PATH:/Users/drsun/jenkins/sonar-scanner-3.0.3.778-macosx/bin" >> ~/.bashrc
source ~/.bashrc
四、創(chuàng)建分析項(xiàng)目
1. 手動(dòng)執(zhí)行
在第三步中已經(jīng)安裝好了所需要的軟件,并且也設(shè)置好了PATH,此時(shí)可以通過(guò)終端來(lái)手動(dòng)生成一個(gè)項(xiàng)目分析報(bào)告,為接下來(lái)在Jenkins中進(jìn)行持續(xù)分析準(zhǔn)備。這里我們以AFNetworking的代碼為例來(lái)進(jìn)行分析,以下操作均在AFNetworking源代碼目錄下執(zhí)行。另外注意Xcode的版本,我用的版本是Version 9.2 (9C40b),如果你的版本不一致,請(qǐng)先閱讀完1.6)之后再來(lái)執(zhí)行各個(gè)步驟。
1.1)清理工程
xcodebuild -workspace AFNetworking.xcworkspace -scheme AFNetworking\ iOS -sdk iphonesimulator11.2 -configuration Debug clean
1.2)生成compile_commands.json
xcodebuild -workspace AFNetworking.xcworkspace -scheme AFNetworking\ iOS -sdk iphonesimulator11.2 -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json
1.3)生成oclint.xml
oclint-json-compilation-database -- -report-type pmd -o oclint.xml -max-priority-1 100000 -max-priority-2 100000 -max-priority-3 100000
1.4)處理oclint.xml
oclint生成的報(bào)告中如下形式的規(guī)則會(huì)導(dǎo)致Objective-c分析插件出錯(cuò)(ERROR: The rule 'OCLint:compiler warning' does not exist.),
<violation begincolumn="24" endcolumn="0" beginline="90" endline="0" priority="2" rule="compiler warning" ruleset="clang">
implicit conversion loses integer precision: 'NSInteger' (aka 'long') to 'int'
</violation>
這些規(guī)則一般是編譯警告,正常來(lái)說(shuō)在工程中是應(yīng)該消除編譯警告的,但是對(duì)于一些歷史項(xiàng)目來(lái)說(shuō)可能是不現(xiàn)實(shí)的。Objective-c分析插件沒(méi)有將這些編譯警告轉(zhuǎn)化為對(duì)應(yīng)的規(guī)則,可以使用如下python腳本將oclint.xml中的這些規(guī)則刪除掉,腳本的處理方式比較簡(jiǎn)單粗暴,直接將所有ruleset是clang的XML節(jié)點(diǎn)全部刪掉了。
#!/usr/bin/python
import xml.etree.ElementTree as ET
import os
os.system('mv oclint.xml oclint.xml.origin')
tree = ET.ElementTree(file='oclint.xml.origin')
root = tree.getroot()
del_items = []
for child in root:
for one in child:
if one.attrib['ruleset'] == 'clang':
print child.attrib['name']
del_items.append(child)
break
for del_item in del_items:
root.remove(del_item)
tree.write('oclint.xml')
將上述代碼保存到 ~/jenkins目錄下,命名為rm_clang.py,使用如下命令處理oclint.xml。
python ~/jenkins/rm_clang.py
注意:AFNetworking的代碼實(shí)際上不需要這樣處理!
1.5)生成Sonar報(bào)告
將如下內(nèi)容保存為sonar-project.properties文件,放到AFNetworking目錄下。
sonar.projectKey=AFNetworking
sonar.host.url=http://localhost:9000
sonar.login=admin
sonar.password=admin
sonar.language=objc
sonar.objectivec.workspace=AFNetworking.xcworkspace
sonar.objectivec.appScheme=AFNetworking iOS
sonar.sources=AFNetworking
sonar.objectivec.oclint.report=oclint.xml
上述文件中,第一部分是SonarCube相關(guān)的配置,主要是sonar.host、sonar.login、sonar.password這幾項(xiàng)需要根據(jù)自己的情況修改,另外不需要專(zhuān)門(mén)在SonarCube創(chuàng)建項(xiàng)目,如果項(xiàng)目不存在SonarScanner會(huì)自動(dòng)創(chuàng)建。
第二部分是Xcode工程相關(guān)的配置,根據(jù)項(xiàng)目實(shí)際情況填寫(xiě)即可。
第三部分是oclint生成的分析報(bào)告。
執(zhí)行如下命令即可在SonarCube中查看分析報(bào)告:
sonar-scanner
命令執(zhí)成功之后可以看到這樣的輸出
INFO: Analysis report generated in 268ms, dir size=390 KB
INFO: Analysis reports compressed in 105ms, zip size=106 KB
INFO: Analysis report uploaded in 199ms
INFO: ANALYSIS SUCCESSFUL, you can browse http://localhost:9000/dashboard/index/AFNetworking
INFO: Note that you will be able to access the updated dashboard once the server has processed the submitted analysis report
INFO: More about the report processing at http://localhost:9000/api/ce/task?id=AWIeTscSMwxcUMvSI7Pm
INFO: Task total time: 8.012 s
INFO: ------------------------------------------------------------------------
INFO: EXECUTION SUCCESS
INFO: ------------------------------------------------------------------------
INFO: Total time: 9.999s
INFO: Final Memory: 47M/335M
INFO: ------------------------------------------------------------------------
直接在瀏覽器中打開(kāi)上面的鏈接就可以看分析報(bào)告了。
1.6)命令說(shuō)明
之前的幾個(gè)步驟中使用的命令中有一堆參數(shù),在這里簡(jiǎn)單的說(shuō)明一下。xcodebuild命令中主要用了-workspace 、-scheme 、-configuration、-sdk ,這些參數(shù)怎么來(lái)的呢?其中-sdk 參數(shù)我們可以通過(guò)如下命令獲得:
xcodebuild -showsdks
#輸出如下所示
iOS SDKs:
iOS 11.2 -sdk iphoneos11.2
iOS Simulator SDKs:
Simulator - iOS 11.2 -sdk iphonesimulator11.2
macOS SDKs:
macOS 10.13 -sdk macosx10.13
tvOS SDKs:
tvOS 11.2 -sdk appletvos11.2
tvOS Simulator SDKs:
Simulator - tvOS 11.2 -sdk appletvsimulator11.2
watchOS SDKs:
watchOS 4.2 -sdk watchos4.2
watchOS Simulator SDKs:
Simulator - watchOS 4.2 -sdk watchsimulator4.2
所以,可以確定-sdk iphonesimulator11.2參數(shù)。
其它幾個(gè)參數(shù)可以通過(guò)如下命令獲得:
xcodebuild -list
#輸出如下所示
Information about project "AFNetworking":
Targets:
AFNetworking iOS
AFNetworking watchOS
AFNetworking OS X
AFNetworking tvOS
AFNetworking iOS Tests
AFNetworking Mac OS X Tests
AFNetworking tvOS Tests
Build Configurations:
Debug
Release
If no build configuration is specified and -scheme is not passed then "Release" is used.
Schemes:
AFNetworking iOS
AFNetworking OS X
AFNetworking tvOS
AFNetworking watchOS
根據(jù)我們需要分析的內(nèi)容選擇對(duì)應(yīng)的參數(shù)即可。
關(guān)于oclint-json-compilation-database命令需要特別說(shuō)明的是,該命令最終會(huì)調(diào)用oclint命令,所以在1.3)步驟中 --(雙橫線(xiàn))之后的參數(shù)實(shí)際上是傳遞給oclint的。而對(duì)于oclint-json-compilation-database命令需要特別注意的是-e參數(shù),該參數(shù)可以幫助我們排除不需要進(jìn)行分析的第三方代碼,比如使用了cocoapods之后,就需要使用如下命令來(lái)將Pods目錄排除。
oclint-json-compilation-database -e Pods -- -report-type pmd -o oclint.xml -max-priority-1 100000 -max-priority-2 100000 -max-priority-3 100000
這個(gè)參數(shù)不需要特別的區(qū)分目錄的路徑,只要目錄名即可。如果使用的pod庫(kù)較多,而使用oclint的時(shí)候沒(méi)有排除的話(huà)可能導(dǎo)致oclint要處理的文件太多,而最終生成的oclint.xml不完整。
另外還需要特別說(shuō)明的是,如果項(xiàng)目比較大oclint-json-compilation-database執(zhí)行的時(shí)間會(huì)比較長(zhǎng),十幾分鐘以上是正常的。
在oclint分析過(guò)程中可能會(huì)出現(xiàn)各種問(wèn)題,我遇到兩個(gè)比較棘手的問(wèn)題,一是"error: one compiler command contains multiple jobs”,另一個(gè)是“cannot open report file”。
關(guān)于第一個(gè),最終確認(rèn)是pod的問(wèn)題,需要在xcodebuild中加入COMPILER_INDEX_STORE_ENABLE=NO?;蛘咴诠こ讨信渲靡残?,但直接在命令行中加入比較方便。在上面的命令中我已經(jīng)加入了。
關(guān)于第二個(gè)問(wèn)題,真是百思不得其解,最終通過(guò)輸出重定向生成了report,打開(kāi)發(fā)現(xiàn)許多文件沒(méi)有分析,原因是cannot reading file,然后提示too many opened files。一開(kāi)始沒(méi)有意識(shí)到,后來(lái)突然想明白了,這是oclint處理的文件太多了,需要通過(guò)-e參數(shù)來(lái)排除一些不需要分析的庫(kù)文件來(lái)減輕oclint的負(fù)擔(dān)。
出現(xiàn)第二個(gè)問(wèn)題的時(shí)候,還有一個(gè)情況可能是需要清理一下整個(gè)項(xiàng)目的代碼,來(lái)重新執(zhí)行分析來(lái)解決。
2. 在Jenkins中執(zhí)行
其實(shí)上一步中的各種命令就是我們需要用Jenkins自動(dòng)執(zhí)行的,在手動(dòng)完成一次分析報(bào)告后,使用Jenkins自動(dòng)構(gòu)建就非常的簡(jiǎn)單了,出了問(wèn)題也很容易檢查。
因?yàn)樵谖恼麻_(kāi)頭,就已經(jīng)提過(guò)Jenkins的構(gòu)建環(huán)境跟手動(dòng)執(zhí)行的環(huán)境是不一樣的。因?yàn)镴enkins在Mac系統(tǒng)下創(chuàng)建了一個(gè)名為Jenkins的用戶(hù),所有構(gòu)建操作是以該用戶(hù)執(zhí)行的,因此環(huán)境上是有差異的。
2.1)配置環(huán)境變量
先通過(guò)系統(tǒng)管理->系統(tǒng)設(shè)置->全局屬性來(lái)配置環(huán)境變量,如下圖所示:

增加鍵LC_ALL值en_US.UTF-8,這個(gè)是為了解決xcpretty無(wú)法處理中文字符的問(wèn)題。
增加鍵Path值/Users/drusun/jenkins/oclint-0.13.1/bin:/Users/drusun/jenkins/sonar-scanner-3.0.3.778-macosx/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:,這一步等同于第三步中的設(shè)置PATH變量,同樣需要修改路徑為你機(jī)器上的路徑。
增加鍵PYTHONIOENCODING值utf-8,這個(gè)是為了解決rm_clang.py腳本無(wú)法處理包含中文字符的問(wèn)題。
2.2)創(chuàng)建項(xiàng)目

如圖所示,新建一個(gè)構(gòu)建一個(gè)自由風(fēng)格的軟件項(xiàng)目任務(wù)。然后,在源碼管理中選擇Git,如下圖所示,將AFNetworking的倉(cāng)庫(kù)地址https://github.com/AFNetworking/AFNetworking.git填入。

在構(gòu)建中選擇增加構(gòu)建步驟->Execute shell,如下所示:

內(nèi)容如下:
xcodebuild -workspace AFNetworking.xcworkspace -scheme AFNetworking\ iOS -sdk iphonesimulator11.2 -configuration Debug clean
xcodebuild -workspace AFNetworking.xcworkspace -scheme AFNetworking\ iOS -sdk iphonesimulator11.2 -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json
oclint-json-compilation-database -- -report-type pmd -o oclint.xml -max-priority-1 100000 -max-priority-2 100000 -max-priority-3 100000
python /Users/drsun/jenkins/rm_clang.py
cp /Users/drsun/jenkins/sonar-project.properties .
sonar-scanner
rm oclint.xml oclint.xml.origin sonar-project.properties compile_commands.json
上面的腳本內(nèi)容就是在上一步手動(dòng)執(zhí)行中輸入的命令,點(diǎn)擊保存后,再點(diǎn)擊左側(cè)的立即構(gòu)建進(jìn)行項(xiàng)目構(gòu)建。
在真實(shí)的項(xiàng)目中,我們可能需要每天半夜1點(diǎn)的時(shí)候進(jìn)行一次構(gòu)建,這時(shí)可以通過(guò)構(gòu)建觸發(fā)器->Build periodically來(lái)設(shè)置,如下所示:

五、總結(jié)
至此,我們已經(jīng)可以用Jenkins+OCLint+SonarCube來(lái)進(jìn)行iOS項(xiàng)目的代碼分析了。在這篇文章里Jenkins的作用好像小了一點(diǎn),但在具體的項(xiàng)目中可能還會(huì)使用Jenkins來(lái)自動(dòng)打包,做每日構(gòu)建等,這時(shí)候在增加一個(gè)構(gòu)建后操作,將本文中的代碼分析結(jié)合進(jìn)去。另外,OCLint還可以對(duì)一些規(guī)則進(jìn)行設(shè)置來(lái)滿(mǎn)足項(xiàng)目規(guī)范,具體可以參考官方文檔。
如果公司內(nèi)部需要接入多個(gè)項(xiàng)目的話(huà),每個(gè)項(xiàng)目輸入一堆命令就有點(diǎn)繁瑣了,而且每個(gè)項(xiàng)目的負(fù)責(zé)人也可能不同,所以為了減輕負(fù)擔(dān),并且能夠快速的將項(xiàng)目接入SonarCube,我用Vue做了一個(gè)簡(jiǎn)單的配置頁(yè)面,可以減輕一些工作量。關(guān)于這個(gè)配置頁(yè)面,我會(huì)再寫(xiě)一篇簡(jiǎn)單講解一下。