用Python写自动化编译工具

来源:互联网 发布:お知らせいたします 编辑:程序博客网 时间:2024/05/21 14:44

我上家公司的主管,用Python写了一个自动化编译工具,用于一条命令编译出ipa,然后把ipa上传到公司的服务器,生成一个链接,可以直接下载,不明觉厉,所以我决定自己尝试写一个。有些事真是,你原本会以为很难,但当你下定决心去做的时候,其实就很简单了。

  • 说明
  • 相关工具
    • PlistBuddy
    • security
  • 代码示例
  • 代码说明
  • 配置文件
  • Shell版本非完整
  • 效果展示

说明

其实自动化编译就是利用Xcode提供的命令行编译工具xcodebuild,可以查看xcodebuild的使用方法,如下所示:

我们使用xcodebuid archive和xcodebuild -exportArchive两个命令来archive和export文件,最终生成ipa。使用的命令如下所示:

xcodebuild archive [-workspace|-project] [-scheme] [-configuration] [-archivePath] [CODE_SIGN_IDENTITY] [PROVISIONING_PROFILE]xcodebuild [-exportArchive] [-archivePath] [-exportPath] [-exportOptionsPlist]

相关工具

我们使用了几个工具:

  1. PlistBuddy: 一款Apple发布的plist编辑文件
  2. security: 一款解析provisioning profile的工具

PlistBuddy

PlistBuddy位置目录:/usr/libexec,该工具用于编辑plist文件。

  1. 获取值:/usr/libexec/PlistBuddy -c ‘Print [key]’ [plistFile]
  2. 设置值:/usr/libexec/PlistBuddy -c ‘Set :[key] [value]’ [plistFile]
  3. 添加值:/usr/libexec/PlistBuddy -c ‘Add :[key] [type] [value]’ [plistFile]
  4. 删除值: /usr/libexec/PlistBuddy -c ‘Delete : [key]’ [plistFile]

security

security是用于解析.mobileprovision文件的工具,其实这个工具我不知道怎么用,我只知道这一个用法,.mobileprovision文件位于”~/Library/MobileDevice/Provisioning Profiles”目录下,命令如下:

security cms -D -i [FilePath]

代码示例

整段代码如下所示:

#!/usr/bin/python # -*- coding:utf-8 -*-# Filename: compile.py# Author: WangLuofanimport os;import sys;import json;import re;import stat;import subprocess;class PListOperation():    def __init__(self, path):        self.path = path;    def getValueForKey(self, key):        pipe = subprocess.Popen(["/usr/libexec/PlistBuddy", "-c", "Print " + key, self.path], stdout=subprocess.PIPE);        result, _ = pipe.communicate();        return result;    def setValueForKey(self, key, value):        subprocess.call(["/usr/libexec/PlistBuddy", "-c", "Set :" + key + " " + value, self.path]);    def addValueForKey(self, key, type, value):        subprocess.call(["/usr/libexec/PlistBuddy", "-c", "Add :" + key +" " + type + " " + value, self.path]);    def delValueForKey(self, key):        subprocess.call(["/usr/libexec/PlistBuddy", "-c", "Delete :" + key, self.path]);def checkXcode():    XcodePath = "/Applications/Xcode.app";    if(os.path.exists(XcodePath)):        getXcodeInfo();    else:        print "请确认本机已经正确安装Xcode";        exit();    return ;def getXcodeInfo():    plist = PListOperation("/Applications/Xcode.app/Contents/version.plist");    version = plist.getValueForKey("CFBundleShortVersionString");    if(version == None):        print "无法获取本机Xcode的版本信息";    else:        print "本机当前安装的Xcode版本: " + version;    return ;def generateOptionPlist(configs):    content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + os.linesep;    content += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" + os.linesep;    content += "<plist version=\"1.0\">" + os.linesep;    content += "<dict>" + os.linesep;    content += "</dict>" + os.linesep;    content += "</plist>" + os.linesep;    with open("option.plist", "w") as plistFile:        plistFile.writelines(content);    plistOper = PListOperation(os.path.join(os.path.abspath(os.curdir), "option.plist"));    if dict.has_key(configs, "useBitcode"):        value = configs["useBitcode"];        if(value == "yes" or value == "true"):            plistOper.addValueForKey("compileBitcode", "bool", "true")        else:            plistOper.addValueForKey("compileBitcode", "bool", "false");    else:        plistOper.addValueForKey("compileBitcode", "bool", "false");    if dict.has_key(configs, "exportMethod"):        plistOper.addValueForKey("method", "string", configs["exportMethod"]);    else:        plistOper.addValueForKey("method", "string", "development");    return ;def setting_before_archive(configs):    ProjName = configs["ProjectName"];    pbxPath = ProjName + ".xcodeproj/project.pbxproj";    if(os.path.exists(pbxPath) == False):        print "工程配置不正确,请自行验证.";        return False;    infoPlist = "";    if(os.path.exists("info.plist")):        infoPlist = "info.plist";    elif(os.path.exists(ProjName + "-info.plist")):        infoPlist = ProjName + "-info.plist";    elif(os.path.exists(os.path.join(ProjName, "info.plist"))):        infoPlist = os.path.join(ProjName, "info.plist");    elif(os.path.exists(os.path.join(ProjName, ProjName + "-info.plist"))):        infoPlist = os.path.join(ProjName, ProjName + "-info.plist");    else:        print "无法获取到info.plist的正确路径";        return False;    infoPlist = os.path.join(os.path.abspath(os.curdir), infoPlist);    op = PListOperation(infoPlist);    op.setValueForKey("CFBundleIdentifier", configs["BundleID"]);    op.setValueForKey("CFBundleShortVersionString", configs["Version"]);    op.setValueForKey("CFBundleVersion", configs["BuildVersion"]);    uuid = getProvisioningProfileUUID(configs["ProvisioningProfile"]);    configs["UUID"] = uuid;    pbxContent = "";    with open(pbxPath, "r") as pbxFile:        changed = False; sectionStart = False;        for line in pbxFile:            if(line.find("CODE_SIGN_IDENTITY[sdk=iphoneos*]") != -1):                index = line.find("CODE_SIGN_IDENTITY[sdk=iphoneos*]");                content = line[0:index] + "CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"" + configs["CodeSignIdentity"] + "\";" + os.linesep;                if(content != line):                    pbxContent += content;                    changed = True;                else:                    pbxContent += line;            elif(line.find("CODE_SIGN_IDENTITY") != -1):                sectionStart = True;                index = line.find("CODE_SIGN_IDENTITY");                content = line[0:index] + "CODE_SIGN_IDENTITY = \"" + configs["CodeSignIdentity"] + "\";" + os.linesep;                if(content != line):                    pbxContent += content;                    changed = True;                else:                    pbxContent += line;            elif(line.find("PROVISIONING_PROFILE_SPECIFIER") != -1):                index = line.find("PROVISIONING_PROFILE_SPECIFIER");                content = line[0:index] + "PROVISIONING_PROFILE_SPECIFIER = " + configs["ProvisioningProfile"] + ";" + os.linesep;                if(content == line or sectionStart == False):                    pbxContent += line;                else:                    pbxContent += content;                    changed = True;            elif(line.find("PROVISIONING_PROFILE") != -1):                index = line.find("PROVISIONING_PROFILE");                content = line[0:index] + "PROVISIONING_PROFILE = \"" + uuid + "\";" + os.linesep;                if(content == line or sectionStart == False):                    pbxContent += line;                else:                    pbxContent += content;                    changed = True;            elif line.find("PRODUCT_BUNDLE_IDENTIFIER") != -1:                index = line.find("PRODUCT_BUNDLE_IDENTIFIER");                content = line[0:index] + "PRODUCT_BUNDLE_IDENTIFIER = \"" + configs["BundleID"] + "\";" + os.linesep;                if(content == line or sectionStart == False):                    pbxContent += line;                else:                    pbxContent += content;                    changed = True;            elif line.find("name = Debug;") != -1 or line.find("name = Release;") != -1 :                sectionStart = False;                pbxContent += line;            else:                pbxContent += line;        if changed :            with open(pbxPath, "w") as pbxFile:                pbxFile.writelines(pbxContent);    return True;def export(configs):    buildTool = "/usr/bin/xcodebuild";    if(os.path.exists(buildTool) == False):        print "xcodebuild工具不存在,请确认您的Xcode安装是否正确";        return -1;    generateOptionPlist(configs);    targetPath = os.path.abspath(os.curdir);    if sys.argv[1] == "-exportOnly":        if len(sys.argv) == 4:            targetPath = os.path.expanduser(sys.argv[3]);    else:        if len(sys.argv) == 3:            targetPath = os.path.expanduser(sys.argv[2]);    argList = [buildTool, "-exportArchive", "-archivePath", os.path.join(os.path.abspath(os.path.curdir), configs["ProjectName"] + ".xcarchive"),         "-exportPath", targetPath, "-exportOptionsPlist",         os.path.join(os.path.abspath(os.curdir), "option.plist")];    return subprocess.call(argList);def clean(ProjName):    archivePath = ProjName + ".xcarchive";    optionPlist = "option.plist";    print "准备清理数据...";    if(os.path.exists(archivePath)):        print "正在清理archive...";        subprocess.call(["sudo", "rm", "-rf", os.path.abspath(archivePath)]);    if(os.path.exists(optionPlist)):        print "正在清理option...";        os.remove(optionPlist);    print "清理完毕...";    return ;def buildClean():    buildTool = "/usr/bin/xcodebuild";    return subprocess.call([buildTool, "clean"]);def archive(configs):    buildTool = "/usr/bin/xcodebuild";    if(os.path.exists(buildTool) == False):        print "xcodebuild工具不存在,请确认您的Xcode安装是否正确";        return -1;    buildClean();    ProjName = configs["ProjectName"];    ProjType = configs["ProjectType"];    BuildConfig = configs["BuildConfiguration"];    CodeSign = configs["CodeSignIdentity"];    ProvFile = configs["ProvisioningProfile"];    UUID = configs["UUID"];    argList = [buildTool, "archive"];    if(ProjType == "workspace"):        argList.append("-workspace");        argList.append(ProjName + ".xcworkspace");        argList.append("-scheme");        argList.append(ProjName);    else:        argList.append("-project");        argList.append(ProjName + ".xcodeproj");    argList.append("-configuration");    argList.append(BuildConfig);    argList.append("-archivePath");    argList.append(ProjName + ".xcarchive");    if dict.has_key(configs, "CodeSignIdentity"):        argList.append("CODE_SIGN_IDENTITY=" + configs["CodeSignIdentity"]);    if dict.has_key(configs, "UUID"):        argList.append("PROVISIONING_PROFILE=" + configs["UUID"]);    return subprocess.call(argList);def getProvisioningProfileUUID(ProvisioningProfile):    uuid_pattern = re.compile("<string>([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})</string>");    if(re.match(uuid_pattern, ProvisioningProfile)):        return ProvisioningProfile;    provisioningDir = os.path.expanduser(r"~/Library/MobileDevice/Provisioning Profiles");    for item in os.listdir(provisioningDir):        if str.endswith(item, ".mobileprovision"):            pipe = subprocess.Popen(["security", "cms", "-D", "-i", os.path.join(provisioningDir, item)], stdout=subprocess.PIPE);            result, _ = pipe.communicate();            pattern = re.compile(ProvisioningProfile);            if(re.findall(pattern, result) != None):                uuid = re.findall(uuid_pattern, result);                return uuid[0];    return None;def parseBuildConfiguration(path):    with open(path, "r") as json_file:        return json.load(json_file);    return None;def run():    configs = parseBuildConfiguration(sys.argv[1]);    ProjectDir = os.path.expanduser(configs["ProjectDir"]);    currentPath = os.path.abspath(os.curdir);    os.chdir(ProjectDir);    if(setting_before_archive(configs) == True):        if(archive(configs) == 0):            if(export(configs) == 0):                if(len(sys.argv) >= 4):                    os.system("open " + sys.argv[3]);                else:                    os.system("open " + os.path.abspath(os.curdir));    clean(configs["ProjectName"]);    os.chdir(currentPath);    return ;def performArchiveOnly():    checkXcode();    configs = parseBuildConfiguration(sys.argv[2]);    ProjectDir = os.path.expanduser(configs["ProjectDir"]);    currentPath = os.path.abspath(os.curdir);    os.chdir(ProjectDir);    if(setting_before_archive(configs) == True):        if(archive(configs) == 0):            os.system("open " + os.path.abspath(os.curdir));    os.chdir(currentPath);    return ;def performBuildCleanOnly():    checkXcode();    configs = parseBuildConfiguration(sys.argv[2]);    ProjectDir = os.path.expanduser(configs["ProjectDir"]);    currentPath = os.path.abspath(os.curdir);    os.chdir(ProjectDir);    buildClean();    os.chdir(currentPath);    return ;def performExportOnly():    checkXcode();    configs = parseBuildConfiguration(sys.argv[2]);    ProjectDir = os.path.expanduser(configs["ProjectDir"]);    currentPath = os.path.abspath(os.curdir);    os.chdir(ProjectDir);    if(export(configs) == 0):        if(len(sys.argv) >= 4):            os.system("open " + sys.argv[3]);        else:            os.system("open " + os.path.abspath(os.curdir));    os.chdir(currentPath);    return ;def performCleanOnly():    configs = parseBuildConfiguration(sys.argv[2]);    ProjectDir = os.path.expanduser(configs["ProjectDir"]);    currentPath = os.path.abspath(os.curdir);    os.chdir(ProjectDir);    clean(configs["ProjectName"]);    os.chdir(currentPath);    return ;def showStandardConfig():    if os.path.exists("standard_config.json"):        os.remove("standard_config.json");    configDict = {        "ProjectName" : "项目名称",        "ProjectDir" : "项目根目录(xcworkspace或xcodeproj文件所在目录)",        "ProjectType" : "项目类型(使用Pods或xcworkspace则为workspace,使用xcodeproj则为project)",        "BuildConfiguration" : "编译类型(Debug|Release)",        "BundleID" : "BundleID",        "Version" : "1.0.0",        "BuildVersion" : "100",        "CodeSignIdentity" : "使用证书名,请打开钥匙串查看名称",        "ProvisioningProfile" : "描述文件名称,请勿填入UUID",        "useBitcode" : "是否使用Bitcode(true|false)",        "exportMethod" : "development|ad-hoc|appstore|enterprise"    };    with open("standard_config.json", "w") as jsonFile:        json.dump(configDict, jsonFile, ensure_ascii=False, indent=4, sort_keys=True);    return ;def showUsage():    print ;    print "python " + sys.argv[0];    print "    -help: Show This Help Menu.";    print "    -showConfig: Show StandardConfig at the Current Directory.";    print "    -buildClean [ConfigFilePath]: Clean the Workspace Or Project Before Archive.";    print "    -archiveOnly [ConfigFilePath]: Only Archive and Generate .xcarchive According to [ConfigFilePath], Do NOT Export ipa.";    print "    -exportOnly [ConfigFilePath] ([TargetPath]): Only Export ipa From .xcarchive at [TargetPath], Do NOT Archive.";    print "    -clean [ConfigFilePath]: Clean the Temporary File According to [ConfigFilePath].";    print "    [ConfigFilePath] ([TargetPath]): Run All the Steps.";    print ;    return ;def parseArgs():    argc = len(sys.argv);    if argc <= 1:        showUsage();    else:        if sys.argv[1] == "-help":            showUsage();        elif sys.argv[1] == "-showConfig":            showStandardConfig();        else:            if argc < 2:                showUsage();            elif sys.argv[1] == "-archiveOnly":                if argc < 3:                    showUsage();                else:                    performArchiveOnly();            elif sys.argv[1] == "-exportOnly":                if argc < 3:                    showUsage();                else:                    performExportOnly();            elif sys.argv[1] == "-clean":                if argc < 3:                    showUsage();                else:                    performCleanOnly();            elif sys.argv[1] == "-buildClean":                if argc < 3:                    showUsage();                else:                    performBuildCleanOnly();            else:                run();    return ;if __name__ == "__main__":    reload(sys);    sys.setdefaultencoding('utf8');    parseArgs();

代码说明

代码很简单,整个脚本就是在构建xcodebuild所需要的参数罢了,修改参数之后,还需要修改项目目录下的.xcodeproj的project.pbxproj文件中的内容。我们需要修改其中相应的字段才可以。

配置文件

使用-showConfig选项会在脚本所在目录下生成一个标准的配置文件,生成的文件如下,其中对每个字段都有说明:

Shell版本(非完整)

其实我最初是用Shell脚本写的,也想趁这个机会学学Shell脚本,但是写到最后,用awk修改字段的时候,就是不知道用Shell怎么把awk修改之后的内容输出到原文件,于是就放弃了,水平还是不够啊,不过还是放上来装装逼:

#!/bin/bashshowUsage(){    echo $0" [ProjectDir]";    return ;}expandUser(){    prefix=${1:0:1};    if [ $prefix == "~" ]    then        echo "/User/`whoami`${1:1}";    fi    return;}check() {    XcodePath="/Applications/Xcode.app";    if [ -d $XcodePath ]    then        return 1;    else        return 0;    fi    return false;}getValueForKeyAtFile(){    value=`/usr/libexec/PlistBuddy -c "Print $1" $2`;    echo $value;    return ;}addValueForKeyAtFile(){    cmd="/usr/libexec/PlistBuddy -c 'Add :$1 string $2' $3";    eval $cmd;    return ;}setValueForKeyAtFile(){    cmd="/usr/libexec/PlistBuddy -c 'Set :$1 $2' $3";    eval $cmd;    return ;}deleteValueForKeyAtFile(){    cmd="/usr/libexec/PlistBuddy -c 'Delete :$1' $2";    eval $cmd;    return ;}getXcodeInfo() {    infoPath="/Applications/Xcode.app/Contents/version.plist";    if [ -e $infoPath ]    then    {        version=$(getValueForKeyAtFile "CFBundleShortVersionString" $infoPath);        echo "当前Xcode版本: "$version;    }    else        echo "无法获取Xcode的相关信息";    fi    return ;}setting_before_archive(){    ProjName=$(getValueForKeyAtFile "ProjectName" "build.plist");    pbxPath=$ProjName".xcodeproj/project.pbxproj";    infoPlist="";    if [ -e "info.plist" ]    then        infoPlist="info.plist";    elif [ -e $ProjName"-info.plist" ]    then        infoPlist=$ProjName"-info.plist";    elif [ -e $ProjName"/info.plist" ]    then        infoPlist=$ProjName"/info.plist";    elif [ -e $ProjName"/"$ProjName"-info.plist" ]    then        infoPlist=$ProjName"/"$ProjName"-info.plist";    else    {        echo "无法获取info.plist的路径";        return ;    }    fi    BundleID=$(getValueForKeyAtFile "BundleID" "build.plist");    Version=$(getValueForKeyAtFile "Version" "build.plist");    BuildVersion=$(getValueForKeyAtFile "BuildVersion" "build.plist");    setValueForKeyAtFile "CFBundleIdentifier" $BundleID $infoPlist;    setValueForKeyAtFile "CFBundleShortVersionString" $Version $infoPlist;    setValueForKeyAtFile "CFBundleVersion" $BuildVersion $infoPlist;    CodeSign=$(getValueForKeyAtFile "CodeSign" "build.plist");    ProvisioningProfile=$(getValueForKeyAtFile "ProvisioningProfile" "build.plist");    ProvisioningProfile=$(getUUIDByName $ProvisioningProfile);    CodeSign=$CodeSign";\"";    cat $pbxPath | while read line    do#content=echo $line | `awk -v sign=$CodeSign 'BEGIN{FS=" = \""; OFS=" =\"";} /CODE_SIGN_IDENTITY/ {$2=sign}1'`;#result=$(echo $line | grep "CODE_SIGN_IDENTITY");        echo -e $line"\n" >> "/Users/wangluofan/Desktop/test.txt";    done    return ;}getUUIDByName(){    SAVEIFS=$IFS;    IFS=$(echo -en "\n\b");    old_dir=`pwd`;    cd "/Users/`whoami`/Library/MobileDevice/Provisioning Profiles";    finded=0;    ls | while read mobileprovision     do        content=`/usr/bin/security cms -D -i $mobileprovision 2>/dev/null`        mobileprovision_name=`/usr/libexec/PlistBuddy -c "Print Name" /dev/stdin <<< $content`;        mobileprovision_uuid=`/usr/libexec/PlistBuddy -c "Print UUID" /dev/stdin <<< $content`;        if [ $mobileprovision_name == $1 ]        then            finded=1;            echo $mobileprovision_uuid;            break;        fi    done    if [ $finded -eq 0 ]    then        echo $1;    fi    cd $old_dir;    IFS=$SAVEIFS;    return ;}archive(){    SAVEIFS=$IFS;    IFS=$(echo -en "\n\b");    if [ -e "/usr/bin/xcodebuild" ]    then    {        if [ -e $1 ]        then        {            ProjName=$(getValueForKeyAtFile "ProjectName" $1);            ProjType=$(getValueForKeyAtFile "ProjectType" $1);            BuildConfig=$(getValueForKeyAtFile "BuildConfiguration" $1);            CodeSign=$(getValueForKeyAtFile "CodeSignIdentity" $1);            ProvFile=$(getValueForKeyAtFile "ProvisioningProfile" $1);            mobileprovision_uuid=$(getUUIDByName $ProvFile);            cmd="xcodebuild archive";            if [ $ProjType == "workspace" ]            then                cmd=$cmd" -workspace "$ProjName".xcworkspace -scheme "$ProjName;            else                cmd=$cmd" -project "$ProjName".xcodeproj";            fi            cmd=$cmd" -configuration "$BuildConfig;            cmd=$cmd" -archivePath "$ProjName".archive";            if [ ${#CodeSign} -ne 0 ]            then                cmd=$cmd' CODE_SIGN_IDENTITY="'$CodeSign'"';            fi            if [ ${#mobileprovision_uuid} -ne 0 ]            then                cmd=$cmd' PROVISIONING_PROFILE="'$mobileprovision_uuid'"';            fi            echo $cmd;            eval $cmd;        }        else            echo "No Such File Or Directory";        fi    }    else        echo "无法完成编译,请确定您已正确安装Xcode";    fi    IFS=$SAVEIFS;    return ;}if [ $# -ne 1 ]then    showUsage;else    check;    if [ $? -eq 0 ]    then        echo "请确认Xcode已正确安装";    else {        getXcodeInfo;        cd $1;        setting_before_archive;        archive "build.plist";    }    fifi

这里只写到了archive过程,因为水平问题,所以到此就暂停了。没什么好写的了,就此结束吧。对了,来个实际公司的项目效果展示吧。

效果展示