我写编译脚本

需求:做持续集成,一个核心点为编译脚本的编写,本文根据项目实际经验做一些总结。

help的重要性

一个shell脚本,如何快速上手,help至关重要,例如linux下的shell命令,通过man或–help 通常可以快速了解这个指令的用法,写编译脚本也不例外,因为很有可能后期用这个脚本的人不是当初写脚本的人。一个好的help 应该告诉使用者 如何传参数,参数的解释以及一些基本用法等。

参数解析

编译脚本应该可以适应不同的任务,不建议一个脚本对应一个编译任务,例如如果你需要编译一个debug版本的apk或者一个release版本的apk,不好的实践是你写了一个build_debug.sh 又写了一个build_release.sh, 而建议的做法是抽象到一个shell中去,做封装和参数解析,这样便于维护和扩展。如你可以这样写build.sh -t debug/release等,通过-t参数指定编译任务。shell里面的对于参数解析可以温习getopts这个指令,请自行搜索不再赘述。

参数有效性判定

在安全领域有句话叫不要相信用户的输入,对于一个脚本来说,你同样需要对用户输入做有效性校验,例如你的这个脚本只接收这些参数,其他都忽略,那么脚本中就得做相应的判断

[ "$buildType" != "debug" -a "$buildType" != "release" ]  && echo "buildType needs to be debug or release" && usage

以上例子是对参数做判定,只能接受”debug”或”release”其他输入就会提示用户。
当然还有参数非空判断,见以下例子:

para_check()
{
    echo "parameter error, please check......"
    echo 
    exit 1
}


[ -z "$1" ] && para_check

日志的技巧

如果有报错,看日志是最有效的做法,shell脚本中日志也必不可少,比如关键函数的执行轨迹,异常之后的日志输出,执行时间的记录等。

build()
{
    [ $# != 2 ] && para_check
    echo "------build begin------"
    TIME_BEFORE_UNIT=`date +%s`
    rm -rf ./output
    env_opt "$1"
    compile "$2"
    pwd && ls -lhR ./output

    if [ $? -ne 0 ] ; then
        echo "------build error------";
        return -1;
    fi

    TIME_AFTER_UNIT=`date +%s`
    UNIT_TIME=$(($TIME_AFTER_UNIT-$TIME_BEFORE_UNIT))
    echo "------compile elapse time:" $UNIT_TIME "s"
    echo "------build end------"

    return 0;
}

这个例子记录了shell的执行开始时间,结束时间,执行路径等,可以方面后期debug和维护。

编译环境

这个是一个容易忽略的点,但是往往再问题定位时会给出一个思路,例如同样的代码用java1.6编译和1.7编译,编译错误很有可能是不一样的,同时一些语言自身也在不断发展,例如gradle迭代速度就非常块,经常会有稳定性,性能提升方面的改进,当你发现编译的主逻辑没有变化,但是不同的机器,编译出来的结果差异很大时,需要考虑编译环境的问题,我的习惯做法是在编译脚本开始打印出软件的版本,如java或gradle的version等。
另外一点:使你定义的环境变量生效,简单的做法先定义好相关的path 和环境变了到profie,然后source ~/.bash_profile使其生效。

一个完整的例子

#!/bin/bash
#/***************************************************************************
# * 
# * Copyright (c) 2016 xxx.com, Inc. All Rights Reserved
# * 
# **************************************************************************/

#/**
# * @file build_ci.sh
# * @author xxx@xx.com
# * @date 2016/11/28/
# **/

# 帮助
usage()
{
    echo "-------------------------------------------------------------------------------"
    echo "Usage: build_ci.sh [-r] [-t debug/release] [-f dev/prod] [-a ut/cc] ..."
    echo "-------------------------------------------------------------------------------"
    echo 
    echo "Options:"
    echo " -r Turns the remote cluster compile on, By default, build task is a local build"
    echo " -t Specifies options for build type : debug/release"
    echo " -f Specifies options for flavor : dev/prod "
    echo " -a Specifies options for build task : ut(unit test)/cc(static code check)"
    echo " -s Specifies a android device(get id by exec adb devices)"
    echo " -h Displays usage information for build_ci.sh"
    echo 
    echo "Examples:"
    echo "sh build_ci.sh -t debug -f dev"
    echo "sh build_ci.sh -t debug -f prod"
    echo "sh build_ci.sh -t release -f dev"
    echo "sh build_ci.sh -t release -f prod"
    echo "sh build_ci.sh -t debug -f dev -s B2T0216513013613 -a ut"
    echo "sh build_ci.sh -t debug -f dev -s B2T0216513013613 -a cc"
    echo 
    exit 1
}

para_check()
{
    echo "parameter error, please check......"
    echo 
    exit 1
}

checkVersion()
{
    java -version
    gradle -v
}

# 编译集群环境准备
env_opt()
{
    echo "set build env......"
    [ -z "$1" ] && para_check
    echo "is_remote_build:$1"
    if [ "$1" == true ]; then
        echo "set remote build env"
        export BUILD_KIT_PATH=/home/scmtools/buildkit
        echo "BUILD_KIT_PATH:"$BUILD_KIT_PATH
        export JAVA_HOME=$BUILD_KIT_PATH/jdk-1.8u92
        export GRADLE_HOME=$BUILD_KIT_PATH/gradle/gradle-3.2.1
        export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH

    fi
    echo "source ~/.bash_profile>/dev/null 2>&1"
    source ~/.bash_profile>/dev/null 2>&1
    checkVersion
}

# 编译
compile()
{
    [ -z "$1" ] && para_check
    echo $1
    echo "gradle compile begin"
    chmod 755 ./gradlew
    $($1)
    echo "gradle compile end"
}

build()
{
    [ $# != 2 ] && para_check
    echo "------build begin------"
    TIME_BEFORE_UNIT=`date +%s`
    rm -rf ./output
    env_opt "$1"
    compile "$2"
    pwd && ls -lhR ./output

    if [ $? -ne 0 ] ; then
        echo "------build error------";
        return -1;
    fi

    TIME_AFTER_UNIT=`date +%s`
    UNIT_TIME=$(($TIME_AFTER_UNIT-$TIME_BEFORE_UNIT))
    echo "------compile elapse time:" $UNIT_TIME "s"
    echo "------build end------"

    return 0;
}

# 变量初始化
workspace=$(pwd)
is_remote_build=false
buildType="debug"
flavor="prod"
buildAction=""
deviceId=""

[ $# -eq 0 ] && usage

# get options
while getopts "rt:f:a:s:" arg
do
    case $arg in
        r)
          is_remote_build=true
          ;;
        t)
          buildType=$OPTARG
          echo "buildType:$buildType"
          [ "$buildType" != "debug" -a "$buildType" != "release" ] \
          && echo "buildType needs to be debug or release" && usage
          ;;
        f)
          flavor=$OPTARG
          echo "flavor:$flavor"
          [ "$flavor" != "dev" -a "$flavor" != "prod" ] \
          && echo "flavor needs to be dev or prod" && usage
          ;;
        a)
          buildAction=$OPTARG
          echo "buildAction:$buildAction"
          [ "$buildAction" != "ut" -a "$buildAction" != "utup" -a "$buildAction" != "cc" ] \
          && echo "buildAction needs to be ut or cc or utup" && usage
          ;;
        s)
          deviceId=$OPTARG
          echo "deviceId:$deviceId"
          ;;
        ?)
          usage
          ;;
    esac
done

buildType="$(tr '[:lower:]' '[:upper:]' <<< ${buildType:0:1})${buildType:1}"
flavor="$(tr '[:lower:]' '[:upper:]' <<< ${flavor:0:1})${flavor:1}"

build_cmd_header="./gradlew clean "
build_cmd=$build_cmd_header"assemble"$flavor$buildType
device_model="";
device_version="";

if [ ! -z "${deviceId}" ] ; then
    echo "export ANDROID_SERIAL=$deviceId"
    export ANDROID_SERIAL=$deviceId

    echo "adb -s $deviceId shell getprop ro.product.model | xargs"
    echo "adb -s $deviceId shell getprop ro.build.version.release | xargs"

    device_model=$(adb -s $deviceId shell getprop ro.product.model | xargs)
    device_version=$(adb -s $deviceId shell getprop ro.build.version.release | xargs)
fi

if [ ! -z "${buildAction}" -a "${buildAction}" == "ut" ] ; then
    build_cmd=$build_cmd_header"create"$flavor$buildType"CoverageReport"
fi

if [ ! -z "${buildAction}" -a "${buildAction}" == "cc" ] ; then
    build_cmd=$build_cmd_header"assemble"$flavor$buildType" check"
fi

echo "build_cmd:$build_cmd"
build "$is_remote_build" "$build_cmd"