Unity 构建IOS和ANDROID工程 (二)
之前记录了下Unity构建ios和android框架设计方面的方案
上文地址:
http://blog.csdn.net/jbl20078/article/details/77715570
这次记录下Unity打包IOS相关脚本的整理和库工程的依赖关系(下篇继续介绍android)
实现目标:
1、持续构建:Unity+Jenkins+Xcode一键打包并做到持续化构建,Jenkins方面本文不讨论,就是个构建服务器
2、多渠道多平台构建:做到一键多渠道多平台构建,其实就是使劲折腾构建脚本
3、灵活的构建框架:在支持一键打包的同时也要方便本地的调试,这个我认为也是蛮重要的,希望构建结构方便程序员切入和修改
第一点:网上资源很多,尤其是momo的文章早在n年前就总结了,Unity平台打包无非是两步走:1、导出xcode/android工程 2、工程分别打包签名 3、放入jenkins构建(如果有用Jenkins的话),我这边再引用下上次文章的框架结构图
我希望xcode工程被导出来后是完全独立的,要做一个库工程给它依赖,这样做是为了保证公共sdk方法最大程度的复用(库工程是唯一的),同时Unity导出的Xcode工程我们也可以单独拿出来扩展功能,这也是第三点的要求,后面脚本的每一步设计都基于这个考虑来的(没做过大项目群维护工作的同学可能理解不了这样做的好处)。
做法步骤:写一个python脚本(shell/ruby都可以,感觉python更简洁),脚本就两个功能:
以下做法是以5.x介绍,4.x差别比较大,请忽罩套
1、导出xcode工程(脚本放在Unity工程根目录下)
#检测必须的环境变量
def checkEnvironVariate(export):
variate = os.environ.get( export )
if variate == None:
print '环境变量没有定义,请先定义它:=> ',export
sys.exit(0)
pass
return variate
#unity 路径
UNITY_PATH = checkEnvironVariate( "UNITY_PATH" )
#导出xcode工程
def exportXcodeProj():
os.system(UNITY_PATH + " -projectPath "+ os.getcwd() + " -executeMethod ProjectBuild.BuildForIPhone -quit")
print "OK!, 导出xcode工程完成"
脚本直接调用静态方法:ProjectBuild类的BuildForIPhone方法(函数名照抄的mono,偷了个懒)
这个脚本随便放入Assets下任意路径都可以,具体方法:
//=============================================
//工程编译平台相关方法类
//=============================================
class ProjectBuild : Editor{
//在这里找出你当前工程所有的场景文件,假设你只想把部分的scene文件打包 那么这里可以写你的条件判断 总之返回一个字符串数组。
static string[] GetBuildScenes(){
List<string> names = new List<string>();
foreach(EditorBuildSettingsScene e in EditorBuildSettings.scenes){
if(e==null)
continue;
if(e.enabled)
names.Add(e.path);
}
return names.ToArray();
}
//shell脚本直接调用这个静态方法
static void BuildForIPhone(){
//解析参数(如果脚本传入参数需要在这边处理,可以写在下面方法中)
OnParseArgv();
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS, "USE_SHARE");
//注意最后一个参数一定不能填别的,否则build xcode工程的时候会被覆盖掉,当前参数代表的是以append的方式编译xcode工程
BuildPipeline.BuildPlayer(GetBuildScenes(), "XcodeProj", BuildTarget.iOS, BuildOptions.AcceptExternalModificationsToPlayer);
}
BuildForIphone方法中最重要的是BuildPipeline.BuildPlayer函数最后一个参数 BuildOptions.AcceptExternalModificationsToPlayer,这个函数作用保证build ios工程的时候是以Append的方式覆盖原来的xcode工程(4.x只设置这一个属性还不行),这样我们在Xcode工程中修改的依赖关系等一些配置在每次编译的时候不会被覆盖。
第一步完成,继续第二步,打包这个xcode工程
Unity提供了build工程之后的回调方法:OnPostProcessBuild 我直接写在上面的类里面,只要是静态的就可以,代码如下:
//=============================================
//以下内容只适用Unity5.x+,4.x版本请使用XUPorter插件
//=============================================
#if UNITY_EDITOR && UNITY_5
[PostProcessBuild(100)]
/// <summary>
/// unity导出xcode结束回调
/// </summary>
/// <param name="buildTarget">Build target.</param>
/// <param name="pathToBuiltProject">Path to built project.</param>
public static void OnPostProcessBuild (BuildTarget buildTarget, string xcodeProjPath){
//判断当前平台是否ios
if (buildTarget != BuildTarget.iOS) {
Debug.LogWarning ("Target is not ios. XCodePostProcess will not run");
return;
}
//获取project.pbxproj路径
string projPath = PBXProject.GetPBXProjectPath(xcodeProjPath);
PBXProject proj = new PBXProject();
proj.ReadFromString(File.ReadAllText(projPath));
// 获取当前target名字
string target = proj.TargetGuidByName(PBXProject.GetUnityTargetName());
// 对所有的编译配置设置选项
proj.SetBuildProperty(target, "ENABLE_BITCODE", "NO");
//添加依赖库
//例如:(系统库添加方法)
//特别注意:系统库建议还是直接在xcode工程中添加引入即可,因为我们目前使用的append方式导出xcode不会覆盖xcode工程系统引入部分(build phases部分)
//下面还是给出例子
//proj.AddFrameworkToProject (target, "Security.framework", false);
//proj.AddFrameworkToProject (target, "libc++.1.tbd", false);
//外部依赖库需要把路径添加到Build Settings中(Frameworkd search path),但是这部分会在每次Unity Build的过程中被清理,所以这部分要手动写入
//other frameworkd库添加方法(比如Umeng framework):
//注意:下面的search路径 完全是按照库工程路径与主工程相对路径编写,一旦修改库工程相关库的路径,下面内容也要修改
proj.AddBuildProperty(target,FRAMEWORK_SEARCH_PATHS_KEY,"$(SRCROOT)/../lib_sg_projects/lib_common_ios/umeng");
proj.AddBuildProperty(target,FRAMEWORK_SEARCH_PATHS_KEY,"$(SRCROOT)/../lib_sg_projects/lib_common_ios/googleMobileAds");
proj.AddBuildProperty(target,FRAMEWORK_SEARCH_PATHS_KEY,"$(SRCROOT)/../lib_sg_projects/lib_common_ios/vungleAds");
//给工程添加头文件搜索路径
//实例:
//proj.AddBuildProperty(target,HEADER_SEARCH_PATHS_KEY,"$(SRCROOT)/../File");
//给工程添加Lib搜索路径
//实例:
//proj.AddBuildProperty(target,LIBRARY_SEARCH_PATHS_KEY,"$(SRCROOT)/../Lib");
//其他引入方式参考
//proj.AddFileToBuild(target, proj.AddFile("Frameworks/mylib.framework", "Frameworks/mylib.framework", PBXSourceTree.Source));
//设置签名
//proj.SetBuildProperty (target, "CODE_SIGN_IDENTITY", "iPhone Distribution: _______________");
//proj.SetBuildProperty (target, "PROVISIONING_PROFILE", "********-****-****-****-************");
// 保存工程
proj.WriteToFile (projPath);
// 修改plist
string plistPath = xcodeProjPath + "/Info.plist";
PlistDocument plist = new PlistDocument();
plist.ReadFromString(File.ReadAllText(plistPath));
PlistElementDict rootDict = plist.root;
// 声明权限(不必要,我们按照append的方式覆盖导出xcode,info.plist中的内容可以保留)
//rootDict.SetString("NSContactsUsageDescription", "是否允许此游戏使用麦克风?");
//rootDict.SetString("NSContactsUsageDescription", "是否允许此App访问您的蓝牙?");
//rootDict.SetString("NSContactsUsageDescription", "是否允许此App使用日历?");
//rootDict.SetString("NSContactsUsageDescription", "是否允许此App访问您的地理位置?");
//rootDict.SetString("NSContactsUsageDescription", "是否允许此App访问您的相册?");
//修改包名:
rootDict.SetString("CFBundleIdentifier", "com.biemore.ios.Carnival");
//可以通过传入脚本参数来修改工程的一些配置(比如说版本号和code值)
//通过脚本传入参数设置版本号和code值
//注意:下面的操作也不是必须的,建议直接放在打包脚本中进行
if(!string.IsNullOrEmpty(VERSION_VALUE)) rootDict.SetString("CFBundleShortVersionString", VERSION_VALUE);
if(!string.IsNullOrEmpty(CODE_VALUE)) rootDict.SetString("CFBundleVersion", CODE_VALUE);
// 保存plist
plist.WriteToFile (plistPath);
}
注释量还可以,就不解释了哈,里面很多注释的代码只是给几个选择,直接改xcode工程也可以,用代码控制也可以,看个人喜好,我倾向于直接修改xcode,反正每次build不会被覆盖(注意不是所有的都不会被覆盖哦:头文件和链接库、framework等search路径一定要代码控制,Icon和launch img也会被强行覆盖等等),直接修改xcode工程不是坏事,太依赖Unity容易不灵活的,等我们需要对Xcode深度功能开发大家就会体会到我的原则--->能修改xcode绝不代码控制。
整个xcode的导出和修改都完成了,下面就是打包签名ipa,又回到脚本的事情,代码(直接把代码全上了):
# -*- coding: utf-8 -*-
import os
import sys
import getopt
import time
import datetime
import shutil
import subprocess
import plistlib
#检测必须的环境变量
def checkEnvironVariate(export):
variate = os.environ.get( export )
if variate == None:
print '环境变量没有定义,请先定义它:=> ',export
sys.exit(0)
pass
return variate
#unity 路径
UNITY_PATH = checkEnvironVariate( "UNITY_PATH" )
GIT_VERSION = 'null'
VERSION_VALUE = '1.0.0'
CODE_VALUE = '0'
BUILDINFO_PATH = os.getcwd()+'/XcodeProj/Info.plist'
IPA_PATH = os.getcwd()+'/ipa'
BUILD_MODE = ["dev","dist"]
BUILD_STATUS = 'Release'
TARGET_NAME = 'Unity-iPhone'
PRODUCE_NAME = 'endlessrunnerbase'
#获取当前版本号和code值
def getBundleVersion():
if (os.path.exists(BUILDINFO_PATH)):
try:
plist = plistlib.readPlist(BUILDINFO_PATH)
VERSION_VALUE = plist["CFBundleShortVersionString"]
CODE_VALUE = plist["CFBundleVersion"]
except Exception as e:
print "info.plist解析失败: " + BUILDINFO_PATH, e
return
else:
print "没有找到info文件: " + BUILDINFO_PATH
#修改当前版本号和code值
def modifyBundleVersion():
if (os.path.exists(BUILDINFO_PATH)):
#用plistlib.writePlist方法必须要重新覆盖整个info.plist,这边用系统的PlistBuddy方法修改
os.system('/usr/libexec/PlistBuddy -c "Set:CFBundleShortVersionString %s" %s'%(VERSION_VALUE,BUILDINFO_PATH))
os.system('/usr/libexec/PlistBuddy -c "Set:CFBundleVersion %s" %s'%(CODE_VALUE,BUILDINFO_PATH))
return
#先获取系统值
getBundleVersion()
#下载库工程
def downLoadSG_iosLib():
cwd = os.getcwd()
#判断库工程是否存在,如果存在执行git pull,否则执行git clone
if (os.path.exists('./lib_sg_projects/')):
os.chdir('./lib_sg_projects/')
os.system('git pull origin xxxxxxx')
else:
#下载库工程
os.system('git clone -b xxxxxxxxx XXXXXXXXXXXX')
os.chdir(cwd)
return
#获取git哈希code
def getGitVersionHashCode():
info = os.popen('git rev-list HEAD -n 1 | cut -c 1-7').readlines()
for line in info:
GIT_VERSION = line.strip('\r\n')
break
#导出xcode工程
def exportXcodeProj():
os.system(UNITY_PATH + " -projectPath "+ os.getcwd() + " -executeMethod ProjectBuild.BuildForIPhone " + VERSION_VALUE + " " + CODE_VALUE + " -quit")
print "OK!, 导出xcode工程完成"
#创建ipa文件夹
def makeIpaFile():
if (os.path.exists(IPA_PATH)):
os.system("find ./ipa -type f -name \"*.ipa\" | xargs rm -rf")
else:
#创建新文件夹
os.system("mkdir "+IPA_PATH)
#编译库工程
def buildLibSg():
tmp = os.getcwd()
libPath = './lib_sg_projects/lib_common_ios/'
if (os.path.exists(libPath)):
os.chdir(libPath)
os.system("xcodebuild -target SG_project_ios -configuration " + BUILD_STATUS + " -sdk iphoneos build")
else:
print "库工程不存在,请确认是否从git中获取成功: "+libPath
system.exit(0)
#回到原目录
os.chdir(tmp)
#编译主工程
def buildTarget(certificate):
tmp = os.getcwd()
os.chdir('./XcodeProj')
# os.system("xcodebuild -target " + TARGET_NAME + " -configuration " + BUILD_STATUS + " -sdk iphoneos build CODE_SIGN_IDENTITY="+certificate)
#字符串尽量使用下面这种方法,特殊符号比较方便处理
cmd = 'xcodebuild -target %s -configuration %s -sdk iphoneos build CODE_SIGN_IDENTITY="%s" ' %(TARGET_NAME,BUILD_STATUS,certificate)
# xcodebuild -target %s -sdk %s -configuration %s GCC_PREPROCESSOR_DEFINITIONS="%s" build' %(project, , SDK, configuration, definitions)
os.system(cmd)
#回到原目录
os.chdir(tmp)
#sign and package
def packageIpa(provisioningProfile,certificate):
ORIIPA_PATH = os.getcwd() + '/XcodeProj/build/'+BUILD_STATUS+"-iphoneos/"+PRODUCE_NAME+".app"
cmd = 'xcrun -sdk iphoneos PackageApplication -v %s -o %s/UnclearRun_%s.ipa --sign "%s" --embed %s ' %(ORIIPA_PATH,IPA_PATH,BUILD_STATUS,certificate,provisioningProfile)
# os.system("xcrun -sdk iphoneos PackageApplication -v "+ORIIPA_PATH+" -o "+IPA_PATH+"/test.ipa"+" --sign "+certificate+" --embed "+provisioningProfile)
os.system(cmd)
#根据打包模式打包
def execute_makeipa(buildmode):
global BUILD_STATUS
CODE_SIGN_IDENTITY = ""
PROVISONNIING_PROFILE = ""
if 'dev' == buildmode:
BUILD_STATUS = 'Debug'
PROVISONNIING_PROFILE = os.popen("find " + os.getcwd() + "/mobileProvision/dev -type f -name \"*.mobileprovision\"").read()
CODE_SIGN_IDENTITY="iPhone Developer: xxxxxx"
else:
BUILD_STATUS = 'Release'
PROVISONNIING_PROFILE = os.popen("find " + os.getcwd() + "/mobileProvision/dist -type f -name \"*.mobileprovision\"").read()
CODE_SIGN_IDENTITY="iPhone Distribution: xxxxxxxx"
#编译库工程
buildLibSg()
#编译主工程
buildTarget(CODE_SIGN_IDENTITY)
#打包主工程
packageIpa(PROVISONNIING_PROFILE,CODE_SIGN_IDENTITY)
#生成ipa
def makeIPA():
#下载库工程
downLoadSG_iosLib()
#修改配置文件
modifyBundleVersion()
#创建包文件
makeIpaFile()
for bm in BUILD_MODE:
execute_makeipa(bm)
return
#usage
def usage():
print 'ipa_Builder.py usage:'
print '-h, --help: 帮助信息.'
print '-v, --version: 打包的版本号,不输入默认按照Unity PlayerSetting中设置并打包'
print '-c, --code: 打包ipa的build值,Unity PlayerSetting中设置并打包'
# -------------- main --------------
if __name__ == '__main__':
opts, args = getopt.getopt(sys.argv[1:], "hv:c:")
for op, value in opts:
if op == "-v":
VERSION_VALUE = value
elif op == "-c":
CODE_VALUE = value
elif op == "-h":
usage()
sys.exit()
# 打包
exportXcodeProj()
makeIPA()
因为我有库工程,所以上面我加入了库工程的版本控制和下载编译等操作,同时将ipa打包成debug和release两个版本方便调试和发布。
上面的脚本可以规整下放入jenkins打包了(里面有公司版权信息内容,用伪代码处理了几个地方,不耽误大家阅读理解),整个一键打包流程结束,后面介绍android的相关内容。
下一篇: 设计模式之状态模式