用Python写自动化编译工具
2017-05-17 18:15
1046 查看
我上家公司的主管,用Python写了一个自动化编译工具,用于一条命令编译出ipa,然后把ipa上传到公司的服务器,生成一个链接,可以直接下载,不明觉厉,所以我决定自己尝试写一个。有些事真是,你原本会以为很难,但当你下定决心去做的时候,其实就很简单了。
说明
相关工具
PlistBuddy
security
代码示例
代码说明
配置文件
Shell版本非完整
效果展示
我们使用xcodebuid archive和xcodebuild -exportArchive两个命令来archive和export文件,最终生成ipa。使用的命令如下所示:
PlistBuddy: 一款Apple发布的plist编辑文件
security: 一款解析provisioning profile的工具
获取值:/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]
这里只写到了archive过程,因为水平问题,所以到此就暂停了。没什么好写的了,就此结束吧。对了,来个实际公司的项目效果展示吧。
说明
相关工具
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过程,因为水平问题,所以到此就暂停了。没什么好写的了,就此结束吧。对了,来个实际公司的项目效果展示吧。
效果展示
相关文章推荐
- scons —— Python自动化编译构建工具
- Buildroot自动化交叉编译工具
- Python游戏服务器开发日记(四)scons编译工具、C和C++混合使用
- Python脚本自动化编译RPM包
- Python自动化构建工具scons使用入门笔记
- 自动化编译工具(autotools系列工具)使用实例
- python自动化工具之pywinauto(四)——批量转换exe视频
- python 自动化部署工具Fabric简介
- Python自动化构建工具scons使用入门笔记
- 初识TPOT:一个基于Python的自动化机器学习开发工具
- Python自动化测试工具Splinter简介和使用实例
- scons是一个Python写的自动化构建工具,和GNU make相比优点明显
- Android自动化工具Monkeyrunner使用(六) —— python 里的import
- Python自动化测试工具Splinter简介和使用实例
- python文件编译成so介绍 - 2.使用makefile将py文件编译成so文件并制作成rpm包,实现自动化
- Python-Selenium2做Web自动化测试(2)-自动化测试常用工具
- Python自动化构建工具scons使用入门笔记
- 简单的抓取淘宝关键字信息、图片的Python爬虫|Python3中级玩家:淘宝天猫商品搜索爬虫自动化工具(第二篇)
- python自动化工具之pywinauto(零)