我司今年开始尝试一些代码质量相关建设,比如组织 codereview、修复代码扫描漏洞.这是一个很好的现象,当我们为了快速迭代,往往为了让需求上线,导致代码并使很规范,时间长了就留下了一堆技术债.
前日的一天,iOS 老哥找我说让看看能不能弄弄 iOS 代码扫描,扫描出一些代码漏洞,尝试去修复漏洞和 bug.
于是乎,下面就是我记录一下折腾了几天完成的 iOS 代码扫描初探的过程.
从去年开始,就一直研究 iOS 代码扫描这款.无奈乎,iOS 在代码扫描这个领域能选的工具其实不算太多.
这次主要介绍如下几个工具:
下面是在 mac 机器上安装的工具
OCLint 是基于 Clang Tooling 开发的静态分析工具,主要用来发现编译器检查不到的那些潜在的关键技术问题.
命令安装
brew tap oclint/formulae
brew install oclint
下载安装包安装
https://github.com/oclint/oclint/releases
配置环境变量
OCLint_PATH=/Users/xinxi/Documents/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH
source .bash_profile
验证是否安装成功。在终端输入 oclint --version
用于对 xcodebuild 的输出进行格式化
gem install xcpretty
用法
紧跟在 xcodebuild 相关语句后面,比如:
xcodebuild [flags] | xcpretty
可以结合 tee 进行日志收集
xcodebuild [flags] | tee xcodebuild.log | xcpretty
官网地址,sonarqube 分社区版本和商业化版本,能扫描多种语言并且开源
https://www.sonarqube.org/downloads/
docker 安装
docker pull sonarqube:8.6-community
二进制文件安装
在 bin/macosx-universal-64 目录下的输入:
sh sonar.sh start
控制台输出"Started SonarQube"说明启动成功.
在浏览器访问,能打开页面说明启动成功.
http://127.0.0.1:9000/
需要说明的是 SonarQube 如果想持久化保存数据,是需要依赖 mysql 数据库的.
SonarQube 默认提供 H2 存储,只能暂时存储一些小项目结果,仅为了演示使用.
在 conf/sonar.properties 下配置数据库地址即可.
可选 MySQL、Oracle、PostgreSQL
sonarqube 默认没有扫描 oc 的检查,sonarqube 官方的 sonar-objective-c 插件是收费的.
需要在找一个免费的插件,在 github 找到两个项目
插件一
https://github.com/Backelite/sonar-objective-c
这个插件在三年前没有修改了,在使用中发现有些扫描规则并没有.
插件二
这个项目稍微更新的时间短一些,有些规则适当的更新了
https://github.com/raatiniemi/sonar-objective-c
下载插件放到/extensions/plugins 目录下
sonar-scanner 用来扫描本地代码,并且上传到 SonarQube 平台中.
下载地址: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/
按照不同的操作系统选择不同安装包即可.
配置环境变量:
vim /etc/profile
export SONAR_HOME=/usr/local/sonarqube-6.7.5
export SONAR_SCANNER_HOME=/usr/local/sonar-scanner
PATH=$PATH:$SONAR_HOME/bin:$SONAR_RUNNER_HOME/bin
source /etc/profile
sonar-scanner 分为两种使用方式:
配置文件方式:
在项目根目录下新建 sonar-project.properties 文件,内容如下:
//项目的key
sonar.projectKey=projectKey
//项目的名字
sonar.projectName=projectName
//项目的版本
sonar.projectVersion=1.0.0
//需要分析的源码的目录,多个目录用英文逗号隔开
sonar.sources=D:/workspace/Demo/src
进入项目根目录下,然后输入 “sonar-scanner” 命令,执行代码分析
命令行方式:
在命令中设置了参数
sonar-scanner -Dsonar.host.url=http://sonarqube -Dsonar.projectKey=app -Dsonar.sources=.
上面的软件安装完成后,基本上具备的代码扫描的条件.找一个开源项目实验下
使用网络库 AFNetworking 项目:https://github.com/AFNetworking/AFNetworking
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.xml
oclint-json-compilation-database -- -report-type pmd -o oclint.xml -max-priority-1 100000 -max-priority-2 100000 -max-priority-3 100000
处理 oclint.xml
oclint 生成的报告中如下形式的规则会导致 Objective-c 分析插件出错(ERROR: The rule 'OCLint:compiler warning' does not exist, 刚才上面提到的 sonar-objective-c 插件并没有处理 warning 这些规则. 所以需要通过脚本删除这个结果.
脚本
#!/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')
在和 oclint.xml 一个目录下,执行该脚本
将如下内容保存为 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
扫描结果图一:
扫描结果图二:
在使用 demo 中非常顺滑,没什么问题.但是接入了实际项目,出现了如下问题.
编译项目失败
解决方案:
命令行编译的问题,必须携带参数" COMPILER_INDEX_STORE_ENABLE=NO"
oclint: error: violations exceed threshold
解决方案:
maxPriority=15000
${oclint_in} ${oclint_ex} -- -o=$BUILD_WORK_DIR/oclint/lint.xml -report-type=pmd -stats -max-priority-1=$maxPriority -max-priority-2=$maxPriority -max-priority-3=$maxPriority -rc LONG_LINE=500 -rc LONG_VARIABLE_NAME=10
如果扫描的生成的 compile_commands.json 文件过大,oclint-json-compilation-database 会提示出错 “OSError: [Errno 7] Argument list too long”
这个问题在网上看了很多帖子都是如下解决方案,但是实际中使用根本没有解决问题.
https://github.com/oclint/oclint/issues/233有网友给出解决方案
https://github.com/wuwen1030/oclint_argument_list_too_long_solution/tree/master
解决方案:
oclint-json-compilation-database 可以过滤不想扫描的文件和需要扫描的文件夹
-e 忽略扫描和-i 是指定扫描路径
oclint-json-compilation-database -e pods -i build
虽然使用上面的命令扫描,不报错误,但是在平台中扫描的 bug 数是 0,这个问题目前一直未解决.
mysql 存储问题
基于上面失败方案一度想放弃,但是无意中在社区中,看到了好未来开源的 iOS 代码扫描的帖子"我们开源了一款 SonarQube iOS 代码扫描插件",https://testerhome.com/topics/26967, 又激起了我想重新尝试的勇气.
github 地址:
https://github.com/tal-tech/sonar-swift
简单看了一下需要工具,需要 infer、xcpretty、sonar、sonar-swift 插件.
扫描规则:
https://github.com/tal-tech/sonar-swift/blob/master/docs/rule.md
这次尝试并没有急于着手干,看到帖子下面有个微信群并加了群
询问了开发者一些细节,确认是可以扫描 oc 项目的.
其中和开发者聊到工具怎么选择,推荐我在极客时间学习https://time.geekbang.org/column/article/87477,当然这个是付费的专栏.
插件地址:
https://github.com/tal-tech/sonar-swift/releases
当时我下载的是 v1.0.2 版本,把插件放到/extensions/plugins 目录下,重启 sonar
官方提供的脚本
xcodebuild clean build -workspace app.xcworkspace -scheme scheme -destination 'generic/platform=iOS' COMPILER_INDEX_STORE_ENABLE=NO | tee xcodebuild.log > /dev/null
xcpretty -r json-compilation-database -o compile_commands.json < xcodebuild.log > /dev/null
# --skip-analysis-in-path 是忽略扫描目录
infer run --skip-analysis-in-path Pods --compilation-database compile_commands.json
# 可选,如果有 swift 语言使用
swiftlint lint > swiftlint.txt
lizard --xml > lizard-report.xml
sonar-scanner -Dsonar.host.url=http://sonarqube -Dsonar.projectKey=app -Dsonar.sources=. -Dsonar.swift.swiftlint.report=swiftlint.txt -Dsonar.swift.lizard.report=lizard-report.xml -Dsonar.swift.infer.report=infer-out/report.json
infer 是 facebook 开源的一款代码扫描软件,可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题
在 releases 页面中下载二进制文件
https://github.com/facebook/infer/releases
设置环境变量
tar xf infer-osx-vXX.tar.xz
# this assumes you use bash, adapt to your needs in case you use
# another shell
echo "export PATH=$PATH:`pwd`/infer-osx/infer/infer/bin" \
>> ~/.bashrc && source ~/.bashrc
通过 infer --version 查看 infer 版本信息,说明安装成功.
扫描 iOS 命令:
infer -- xcodebuild -workspace "test.xcworkspace" -scheme "scheme"
扫描出的结果会在工程目录下的 infer-out 文件中,其中具体的代码会以 csv,txt,json 的格式分别存在对应的文件中。可以供我们分析.
infer 扫描阶段
扫描的 bug 数量
扫描规则
结果上传成功
sonar 平台展示数据
扫描结果图三:
扫描结果图四:
从下载代码到上传扫描结果,大概 1 小时 30 分支,和项目规模成正比.
解决方案: lizard 这个报告不要了,暂时去掉
java 包中没有这个规则
解决方案:
1、用 -Dsonar.exclusions=文件路径这个排除
2、在 report.json 中删除这个规则
没有这个规则
解决方案:使用新版本的 jar 包
有个异常,去掉-Dsonar.swift.swiftlint.report=swiftlint.txt
因为每次扫描都是增量扫描,如果使用多个分支同一个项目扫描,结果会被覆盖,sonar 本身也不支持多个分支扫描.
每次扫描的时候想知道是扫描的哪个版本的数据,通过参数-Dsonar.projectVersion 参数可以上传版本号.
shell 中获取版本号
version_number=`sed -n '/MARKETING_VERSION/{s/MARKETING_VERSION = //;s/;//;s/^[[:space:]]*//;p;q;}' ./PROJECTNAME.xcodeproj/project.pbxproj`
shell 中获取版本号构建号
build_number=`sed -n '/CURRENT_PROJECT_VERSION/{s/CURRENT_PROJECT_VERSION = //;s/;//;s/^[[:space:]]*//;p;q;}' ./PROJECTNAME.xcodeproj/project.pbxproj`
在活动页面展示了版本号
经过折腾了几天,也算顺利的完成了基础环境搭建,能正常扫描出结果来了.
给我最大的启发是:
做事需要有专业的"社区",如果我没有去 testerhome 中有浏览的习惯,也很难找到不错的工具.
做事需要有专业的"圈子",专业的人做专业事,方可事半功倍.
最后放一张截图, 技术共享、开源万岁.
如何使用脚本读取 Xcode 11 中的当前应用程序版本
https://stackoom.com/question/3q09t/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC%E8%AF%BB%E5%8F%96Xcode-%E4%B8%AD%E7%9A%84%E5%BD%93%E5%89%8D%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E7%89%88%E6%9C%AC
iOS+Jenkins 持续构建 - 代码扫描
https://www.jianshu.com/p/c0d49bcefeb0
使用 Jenkins+OCLint+SonarCube 对 iOS 项目进行代码分析
https://juejin.cn/post/6844903575680729102
iOS 静态代码扫描平台 Sonarqube 实战 Objective-C、Swift
https://testerhome.com/topics/13158
ios fastline sonarqube
https://medium.com/@aamir.ali/sonarqube-integration-with-fastlane-in-ios-3cd33e5abdc8
oclint_argument_list_too_long_solution 解决方案
https://github.com/wuwen1030/oclint_argument_list_too_long_solution
OCLint 静态代码检测实践
https://juejin.cn/post/6844904017424809998
OCLint 基本使用
https://www.jianshu.com/p/b2513f16d246
iOS 静态代码扫描平台 Sonarqube 实战 Objective-C、Swift
https://testerhome.com/topics/13158
Sonarqube & ObjectiveC 环境搭建
https://www.jianshu.com/p/5a01e56176bf