Docker+Jenkins持续集成环境(5): android构建与apk发布
项目组除了常规的java项目,还有不少android项目,如何使用jenkins来实现自动构建呢?本文会介绍安卓项目通过jenkins构建的方法,并设计开发一个类似蒲公英的app托管平台。
android 构建
安装android sdk:
- 先下载sdk tools
- 然后使用sdkmanager安装:
./sdkmanager "platforms;android-21" "platforms;android-22" "platforms;android-23" "platforms;android-24" "platforms;android-25" "build-tools;27.0.3" "build-tools;27.0.2" "build-tools;27.0.1" "build-tools;27.0.0" "build-tools;26.0.3" "build-tools;26.0.2" "build-tools;26.0.1" "build-tools;25.0.3" "platforms;android-26"
然后把把sdk拷贝到volume所在的目录。
jenkins 配置
jenkins需要安装gradle插件,构建的时候选择gradle构建,选择对应的版本即可。
构建也比较简单,输入clean build即可。
android 签名
修改build文件
android {
signingConfigs {
release {
storeFile file("../keystore/keystore.jks")
keyAlias "xxx"
keyPassword "xxx"
storePassword "xxx"
}
}
buildTypes {
release {
debuggable true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
applicationVariants.all { variant ->
if (variant.buildType.name.equals('release')) {
variant.outputs.each {
output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
def fileName = "${defaultConfig.applicationId}_${defaultConfig.versionName}_${releaseTime()}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
}
}
}
lintOptions {
abortOnError false
}
}
def releaseTime() {
new Date().format("yyyyMMdd_HH_mm_ss", TimeZone.getTimeZone("Asia/Chongqing"))
}
构建时自动生成版本号
android的版本号分为version Nubmer和version Name,我们可以把版本定义为
versionMajor.versionMinor.versionBuildNumber,其中versionMajor和versionMinor自己定义,versionBuildNumber可以从环境变量获取。
ext.versionMajor = 1
ext.versionMinor = 0
android {
defaultConfig {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
applicationId "com.xxxx.xxxx"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName computeVersionName()
versionCode computeVersionCode()
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
// Will return "1.0.42"
def computeVersionName() {
// Basic <major>.<minor> version name
return String.format('%d.%d.%d', versionMajor, versionMinor,Integer.valueOf(System.env.BUILD_NUMBER ?: 0))
}
// Will return 100042 for Jenkins build #42
def computeVersionCode() {
// Major + minor + Jenkins build number (where available)
return (versionMajor * 100000)
+ (versionMinor * 10000)
+ Integer.valueOf(System.env.BUILD_NUMBER ?: 0)
}
apk发布
解决方案分析
jenkins构建的apk能自动发布吗?
国内已经有了fir.im,pgyer蒲公英等第三方的内测应用发布管理平台,对于小团队,注册使用即可。但是使用这类平台:
- 需要实名认证,非常麻烦
- 内部有些应用放上面不合适
如果只是简单的apk托管,功能并不复杂,无非是提供一个http接口提供上传,我们可以自己快速搭建一个,称之为apphosting。
大体的流程应该是这样的:
- 开发人员commit代码到SVN
- jenkins 从svn polling,如果有更新,jenkins启动自动构建
- jenkins先gradle build,然后apk签名
- jenkins将apk上传到apphosting
- jenkins发送成功邮件,通知开发人员
- 开发人员从apphosting获取最新的apk
apphosting 服务设计
首先,分析领域模型,两个核心对象,APP和app版本,其中app存储appid、appKey用来唯一标识一个app,app版本存储该app的每次build的结果。
再来分析下,apphosting系统的上下文
然后apphosting简单划分下模块:
我们需要开发一个apphosting,包含web和api,数据库采用mongdb,文件存储采用mongdb的grid fs。除此外,需要开发一个jenkins插件,上传apk到apphosting。
文件存储
文件可以存储到mongodb或者分布式文件系统里,这里内部测试使用mongdb gridfs即可,在spring boot里,可以使用GridFsTemplate来存储文件:
/**
* 存储文件到GridFs
* @param fileName
* @param mediaContent
* @return fileid 文件id
*/
public String saveFile(String fileName,byte[] mediaContent){
DBObject metaData = new BasicDBObject();
metaData.put("fileName", fileName);
InputStream inputStream = new ByteArrayInputStream(mediaContent);
GridFSFile file = gridFsTemplate.store(inputStream, metaData);
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return file.getId().toString();
}
存储文件成功的话会发挥一个fileid,通过这个id可以从gridfs获取文件。
/**
* 读取文件
* @param fileid
* @return
*/
public FileInfo getFile(String fileid){
GridFSDBFile file = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(fileid)));
if(file==null){
return null;
}
FileInfo info = new FileInfo();
info.setFileName(file.getMetaData().get("fileName").toString());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
file.writeTo(bos);
info.setContent(bos.toByteArray());
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
return info;
}
APK上传接口
处理上传使用MultipartFile,双穿接口需要检验下appid和appKey,上传成功会直接返回AppItem apk版本信息。
@RequestMapping(value = {"/api/app/upload/{appId}"},
produces = MediaType.APPLICATION_JSON_UTF8_VALUE,
method = {RequestMethod.POST})
@ResponseBody
public String upload(@PathVariable("appId") String appId, String appKey, AppItem appItem, @RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return error("文件为空");
}
appItem.setAppId(appId);
AppInfo appinfo = appRepository.findByAppId(appItem.getAppId());
if (appinfo == null) {
return error("无效appid");
}
if (!appinfo.getAppKey().equals(appKey)) {
return error("appKey检验失败!");
}
if (saveUploadFile(file, appItem)) {
appItem.setCreated(System.currentTimeMillis());
appItemRepository.save(appItem);
appinfo.setAppIcon(appItem.getIcon());
appinfo.setAppUpdated(System.currentTimeMillis());
appinfo.setAppDevVersion(appItem.getVesion());
appRepository.save(appinfo);
return successData(appItem);
}
return error("上传失败");
}
/**
* 存储文件
*
* @param file 文件对象
* @param appItem appitem对象
* @return 上传成功与否
*/
private boolean saveUploadFile(@RequestParam("file") MultipartFile file, AppItem appItem) {
String fileName = file.getOriginalFilename();
logger.info("上传的文件名为:" + fileName);
String fileId = null;
try {
fileId = gridFSService.saveFile(fileName, file.getBytes());
appItem.setFileId(fileId);
appItem.setUrl("/api/app/download/" + fileId);
appItem.setFileSize((int) file.getSize());
appItem.setCreated(System.currentTimeMillis());
appItem.setDownloadCount(0);
if (fileName.endsWith(".apk")) {
readVersionFromApk(file, appItem);
}
return true;
} catch (IOException e) {
logger.error(e.getMessage(),e);
}
return false;
}
因为我们是apk,apphosting需要知道apk的版本、图标等数据,这里可以借助apk.parser库。先把文件保存到临时目录,然后使用apkFile类解析。注意这里把icon读取出来后,直接转换为base64的图片。
/**
* 读取APK版本号、icon等数据
*
* @param file
* @param appItem
* @throws IOException
*/
private void readVersionFromApk(@RequestParam("file") MultipartFile file, AppItem appItem) throws IOException {
// apk 读取
String tempFile = System.getProperty("java.io.tmpdir") +File.separator + System.currentTimeMillis() + ".apk";
file.transferTo(new File(tempFile));
ApkFile apkFile = new ApkFile(tempFile);
ApkMeta apkMeta = apkFile.getApkMeta();
appItem.setVesion(apkMeta.getVersionName());
// 读取icon
byte[] iconData = apkFile.getFileData(apkMeta.getIcon());
BASE64Encoder encoder = new BASE64Encoder();
String icon = "data:image/png;base64,"+encoder.encode(iconData);
appItem.setIcon(icon);
apkFile.close();
new File(tempFile).delete();
}
jenkins 上传插件
jenkins插件开发又是另外一个话题,这里不赘述,大概讲下:
- 继承Recorder并实现SimpleBuildStep,实现发布插件
- 定义jelly模板,让用户输入appid和appkey等参数
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="appid" field="appid">
<f:textbox />
</f:entry>
<f:entry title="appKey" field="appKey">
<f:password />
</f:entry>
<f:entry title="扫描目录" field="scanDir">
<f:textbox default="$${WORKSPACE}"/>
</f:entry>
<f:entry title="文件通配符" field="wildcard">
<f:textbox />
</f:entry>
<f:advanced>
<f:entry title="updateDescription(optional)" field="updateDescription">
<f:textarea default="自动构建 "/>
</f:entry>
</f:advanced>
</j:jelly>
- 在UploadPublisher定义jelly里定义的参数,实现绑定
private String appid;
private String appKey;
private String scanDir;
private String wildcard;
private String updateDescription;
private String envVarsPath;
Build build;
@DataBoundConstructor
public UploadPublisher(String appid, String appKey, String scanDir, String wildcard, String updateDescription, String envVarsPath) {
this.appid = appid;
this.appKey = appKey;
this.scanDir = scanDir;
this.wildcard = wildcard;
this.updateDescription = updateDescription;
this.envVarsPath = envVarsPath;
}
- 然后在perfom里执行上传,先扫描到apk,再上传
Document document = Jsoup.connect(UPLOAD_URL +"/" + uploadBean.getAppId())
.ignoreContentType(true)
.data("appId", uploadBean.getAppId())
.data("appKey", uploadBean.getAppKey())
.data("env", uploadBean.getEnv())
.data("buildDescription", uploadBean.getUpdateDescription())
.data("buildNo","build #"+ uploadBean.getBuildNumber())
.data("file", uploadFile.getName(), fis)
.post();
插件开发好后,编译打包,然后上传到jenkins,最后在jenkins项目里构建后操作里,选择我们开发好的插件:
apphosting web
仿造蒲公英,编写一个app展示页面即可,参见下图:
还可以将历史版本返回,可以看到我们的版本号每次构建会自动变化:
@GetMapping("/app/{appId}")
public String appInfo(@PathVariable("appId") String appId, Map<String, Object> model) {
model.put("app", appRepository.findByAppId(appId));
Page<AppItem> appItems = appItemRepository.findByAppIdOrderByCreatedDesc(appId,new PageableQueryArgs());
AppItem current = appItems.getContent().get(0);
model.put("items",appItems.getContent());
model.put("currentItem",current);
return "app";
}
延伸阅读
Jenkins+Docker 搭建持续集成环境:
- Docker+Jenkins持续集成环境(1)使用Docker搭建Jenkins+Docker持续集成环境
- Docker+Jenkins持续集成环境(2)使用docker+jenkins构建nodejs前端项目
- Docker+Jenkins持续集成环境(3)集成PMD、FindBugs、Checkstyle静态代码检查工具并邮件发送检查结果
- Docker+Jenkins持续集成环境(4):使用etcd+confd实现容器服务注册与发现
作者:Jadepeng
出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
Docker+Jenkins持续集成环境(5): android构建与apk发布的更多相关文章
- Docker+Jenkins持续集成环境(4):使用etcd+confd实现容器服务注册与发现
前面我们已经通过jenkins+docker搭建了基本的持续集成环境,实现了服务的自动构建和部署,但是,我们遇到一个问题,jenkins构建出来的镜像部署后,需要通过ip:port去访问,有什么更好的 ...
- Docker+Jenkins持续集成环境(2)使用docker+jenkins构建nodejs前端项目
前文使用Docker搭建Jenkins+Docker持续集成环境我们已经搭建了基于docker+jenkins的持续集成环境,并构建了基于maven的项目.这一节,我们继续扩展功能,增加对Nodejs ...
- Docker+Jenkins持续集成环境(1)使用Docker搭建Jenkins+Docker持续集成环境
本文介绍如何通过Jenkins的docker镜像从零开始构建一个基于docker镜像的持续集成环境,包含自动化构建.发布到仓库\并部署上线. 0. 前置条件 服务器安装docker,并启动docker ...
- Docker+Jenkins持续集成环境(3)集成PMD、FindBugs、Checkstyle静态代码检查工具并邮件发送检查结果
为了规范代码,我们一般会集成静态代码检测工具,比如PMD.FindBugs.Checkstyle,那么Jenkins如何集成这些检查工具,并把检查结果放到构建邮件里呢? 今天做了调研和实现,过程如下 ...
- Docker+Jenkins持续集成
Docker+Jenkins持续集成 使用etcd+confd实现容器服务注册与发现 前面我们已经通过jenkins+docker搭建了基本的持续集成环境,实现了服务的自动构建和部署,但是,我们遇 ...
- [Jenkins]持续集成环境下fingbug插件的安装使用与配置
参考:https://wiki.jenkins.io/display/JENKINS/FindBugs+Plugin 突然,天降杂事.我是想安安静静的做个美丽的测试...但是事与愿违,项目经理叫我帮忙 ...
- Windows系统的Jenkins持续集成环境
Windows系统的Jenkins持续集成环境 如题:本文将介绍如何在Windows环境下运用Jenkins部署持续集成环境.之所以写本文,是因为在最近工作当中,学习使用Jenkins时,确实遇到了一 ...
- Jenkins持续集成环境, 如何自定义 maven repositories
假设自定义的仓库路径为“/opt/repository”,那么在“系统管理-系统设置”中,修改“全局MAVEN_OPTS”的值为如下的内容: -Dmaven.repo.local=/opt/repos ...
- Jekens 配置多项目SCM GitLab+Jenkins持续集成环境
参考: 搭建GitLab+Jenkins持续集成环境图文教程 https://blog.csdn.net/ruangong1203/article/details/73065410 Jenkins中配 ...
随机推荐
- 【个人笔记】《知了堂》ajax的get及post请求
ajax 执行步骤 // 步骤 设置事件 调用函数 创建一个XHR对象 打开ajax通道,链接服务器,配置请求信息和参数 发送数据 设置回调函数 服务器接受请求,处理请求,查询数据库,响应 及 返回数 ...
- bzoj 3295: [Cqoi2011]动态逆序对(树套树 or CDQ分治)
Description 对于序列A,它的逆序对数定义为满足i<j,且Ai>Aj的数对(i,j)的个数.给1到n的一个排列,按照某种顺序依次删除m个元素,你的任务是在每次删除一个元素之前统计 ...
- [bzoj3955] [WF2013]Surely You Congest
首先最短路长度不同的人肯定不会冲突. 对于最短路长度相同的人,跑个最大流就行了..当然只有一个人就不用跑了 看起来会T得很惨..但dinic在单位网络里是O(m*n^0.5)的... #include ...
- HDU 1232 并查集
畅通工程 Time ...
- 构建LVS-DR+Keepalive高可用集群
------client----------主LVS----------从LVS------------WEB1-------------WEB2--------- 2.2.2.250 2.2.2. ...
- GitHub上传文件不能超过100M的解决办法
http://blog.csdn.net/u010545480/article/details/52995794 上传项目到GitHub上,当某个文件大小超过100M时,就会上传失败,因为默认 ...
- VisualSVN Server的配置和使用方法
VisualSVN Server是免费的,而VisualSVN是收费的.VisualSVN是SVN的客户端,和Visual Studio集成在一起, VisualSvn Server是SVN的服务器端 ...
- php对数组进行分页
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 ...
- 《并行程序设计导论》——Pthreads
这部分不需要看了. 因为C++11和BOOST比这个Pthreads要好一点. 如果不考虑移植性,在Windows平台上用核心编程的东西比C++11和BOOST更好控制.
- 利用 HTML5 WebGL 构建的 3D 拓扑图
现在,3D 模型已经用于各种不同的领域.在医疗行业使用它们制作器官的精确模型:电影行业将它们用于活动的人物.物体以及现实电影:视频游戏产业将它们作为计算机与视频游戏中的资源:在科学领域将它们作为化合物 ...