欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Unity 构建IOS和ANDROID工程 (二)

程序员文章站 2024-03-24 19:25:04
...

之前记录了下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的话),我这边再引用下上次文章的框架结构图

Unity 构建IOS和ANDROID工程 (二)

        我希望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的相关内容。