您的位置:首页 > 编程语言 > Python开发

用Python写自动化编译工具

2017-05-17 18:15 1046 查看
我上家公司的主管,用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]


相关工具

我们使用了几个工具:

PlistBuddy: 一款Apple发布的plist编辑文件

security: 一款解析provisioning profile的工具

PlistBuddy

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

获取值:/usr/libexec/PlistBuddy -c ‘Print [key]’ [plistFile]

设置值:/usr/libexec/PlistBuddy -c ‘Set :[key] [value]’ [plistFile]

添加值:/usr/libexec/PlistBuddy -c ‘Add :[key] [type] [value]’ [plistFile]

删除值: /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: WangLuofan

import 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");

14cb2
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/bash

showUsage()
{
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";
}
fi
fi


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

效果展示

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  python shell 自动化 xcode