项目组除了常规的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 = 1ext.versionMinor = 0android {    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 . version name    return String.format('%d.%d.%d', versionMajor, versionMinor,Integer.valueOf(System.env.BUILD_NUMBER ?: 0))}// Will return 100042 for Jenkins build #42def 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'?>                                                  
  • 在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 model) {        model.put("app", appRepository.findByAppId(appId));        Page 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
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

更多相关文章

  1. 添加白名单功能
  2. android values-v11和values-v14文件夹是干啥的?
  3. android屏幕适配布局和字体
  4. Android实现的ListView分组布局改进示例
  5. Android(安卓)adb的权限问题(可以不通过adb,而直接访问.db文件)
  6. Ubuntu Linux14 64位下在Android(安卓)studio下用gradle编译Andr
  7. Android(安卓)通过API获取数据库中的图片文件方式
  8. Android(安卓)开发常见问题
  9. Android(安卓)Studio快捷键(自用)

随机推荐

  1. Android请求服务器的两种方式--post, get
  2. Android布局中ScrollView与ListView的冲
  3. Android(安卓)ListView 滑动背景为黑色的
  4. 查看Android设备Mem命令
  5. Android(安卓)UI 常用控件讲解
  6. 当android里一堆button,用数组来循环建立
  7. 关于Android(安卓)studio打包发布,以及And
  8. 服务--Service
  9. ColorMatrixColorFilter颜色过滤(离线用户
  10. android studio gradle 插件无法下载,grad