引言
两年 两年 你知道这两年我是怎么过的吗?天天打包测试,你知道有多好玩吗!哈哈哈,开个玩笑。作为一名Unity开发工程师,在开发、测试、发布环节都需要我们导出安卓或者苹果安装包,今年我们公司使用的苹果打包机是mac mini(为了省钱,哈哈),本身打包速度就非常慢,更何况安卓平台需要发布多渠道,要经常替换SDK文件,苹果平台则需要先处理XCode工程,才能进行导出,全部手动处理会非常容易出错,简直是雪上加霜。所以今天总结一下我是如何使用Unity+Jenkins来解决我这些烦恼的,希望对你也可以有帮助
原理
实现原理比较简单,主要还是需要依靠操作系统的命令行。大部分功能就算不使用Jenkins,也可以实现。使用Jenkins主要是因为它给我们提供了一套可视化界面还有一系列的插件,帮助我们简化了操作流程,方便维护。
一、如何安装Jenkins
我是在Mac上安装的Jenkins,因为我们在Windows上不可以导出苹果包,但是在Mac上我们可以导出安卓包!
在Mac上我们可以通过.pkg、brew、.war三种方式进行安装
- .pkg安装程序,安装方式:双击安装程序即可安装;不推荐使用。好处:可以通过桌面图标启动Jenkins,坏处:没有文件操作权限!根本就没法用!
- brew命令行,安装方式:在终端输入
brew install jenkins
。推荐使用。好处:安装后可以在终端直接输入Jenkins
来启动Jenkins,坏处:需要安装brew工具 - .war包,推荐使用。好处:方便更新,直接替换.war文件即可,坏处:启动时不方便,每次都需要在终端用
java -jar war所在路径/jenkins.war
启动,当然你可以使用Shell
脚本设置开机自启,但是还是太麻烦了!
注意:Jenkins依赖Java环境,并且Jenkins的下载速度较慢,容易失败,建议翻墙下载
二、如何登录Jenkins
- 首先检查Jenkins是否启动,如果没有启动,需要根据安装方式先进行启动
- 然后打开浏览器输入
localhost:8080(如果修改过输入修改后的ip)
,进入Jenkins操作界面 - 如果是第一次启动,需要先输入密码来解锁Jenkins。然后创建管理员账号后使用管理员账号登录
三、Jenkins插件推荐
简要介绍一下我使用到的插件:
- Localization:Chinese(Simplified) 简体中文语言包,可以汉化部分说明
- Unity3d plugin 增强Unity编辑器打包拓展,默认Unity项目在工作区,可以在视图显示Log
- Subversion SVN插件,可以在任务流水线中添加SVN模块,执行项目导出、还原、更新等操作
注意:不要在新手入门时安装任何插件,很容易下载失败,进去后会有错误,建议登录后在Manage jenkin/Manager Plugins中管理插件
五、修改Jenkins插件配置
- Unity3d plugin。 需要在Jenkins的Global Tool Configuration设置中添加Unity安装目录
- Subversion。 需要在Jenkins凭证设置中添加SVN用户名、密码
六、Unity编辑器拓展打包
项目设置代码
- 通用设置
1
2
3
4
5
6
7
8
9
10PlayerSettings.companyName="公司名称"
PlayerSettings.productName ="产品名";
//允许屏幕上下旋转
PlayerSettings.defaultInterfaceOrientation = UIOrientation.AutoRotation;
PlayerSettings.allowedAutorotateToPortrait = false;
PlayerSettings.allowedAutorotateToPortraitUpsideDown = false;
PlayerSettings.allowedAutorotateToLandscapeRight = true;
PlayerSettings.allowedAutorotateToLandscapeLeft = true;
PlayerSettings.SetApplicationIdentifier(buildTargetGroup, "包名");//根据打包目标平台设置包名
PlayerSettings.SplashScreen.show = false; //是否显示启动Logo - Android平台设置
1
2
3
4
5
6
7
8
9
10
11//设置目标SDK版本
PlayerSettings.Android.targetSdkVersion = AndroidSdkVersions.AndroidApiLevel26;
//设置不导出安卓工程
EditorUserBuildSettings.androidBuildSystem=AndroidBuildSystem.Internal;
DirectoryInfo dir = Directory.GetParent(Application.dataPath);
//配置安卓签名
string keystorePath = dir.ToString() + @"/user.keystore";//签名文件路径
PlayerSettings.Android.keystoreName = keystorePath;
PlayerSettings.Android.keystorePass = "XXX";//文件密码
PlayerSettings.Android.keyaliasName = "XXX";//key的别名
PlayerSettings.Android.keyaliasPass = "XXX";//key的密码 - iOS平台设置
1
2//配置苹果开发账号TeamID
PlayerSettings.iOS.appleDeveloperTeamID = "XXX";
项目打包代码
Android平台:导出为.apk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
string dir = BuildTool.GetBuildPackageRootDir() + "/" + "Android/";
string locationPathName = dir + BuildTool.GetAndroidPackageName();
buildPlayerOptions.locationPathName = locationPathName;
buildPlayerOptions.scenes = BuildTool.GetBuildScene();
buildPlayerOptions.target =BuildTarget.Android;
buildPlayerOptions.options = BuildOptions.None;
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
BuildSummary summary = report.summary;
if (summary.result == BuildResult.Succeeded) {
Debug.Log("Build succeeded: " + summary.totalSize + " bytes");
BuildTool.OpenFolder(dir);
}
if (summary.result == BuildResult.Failed) {
Debug.Log("Build failed");
}iOS平台:使用sh脚本调用XCode命令行导出ipa
导出XCode工程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
string dir = BuildTool.GetBuildPackageRootDir() + "/" + "iOS/";
string locationPathName = dir + BuildTool.GetXcodeProjectName();
buildPlayerOptions.locationPathName = locationPathName;
buildPlayerOptions.scenes = BuildTool.GetBuildScene();
buildPlayerOptions.target = BuildTarget.iOS;
buildPlayerOptions.options = BuildOptions.None;
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
BuildSummary summary = report.summary;
//构建完XCode工程后会走这里
if (summary.result == BuildResult.Succeeded) {
Debug.Log("Build succeeded: " + summary.totalSize + " bytes");
//打开文件夹
BuildTool.OpenFolder(locationPathName);
//导出ipa
ExportIPA(locationPathName, BuildTool.GetIPAExportOptionsFilePath());
}
if (summary.result == BuildResult.Failed) {
Debug.Log("Build failed");
}设置XCode工程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161//XCode工程打包完成时,使用代码进行设置
[ ]
public static void SetXCodeProject(BuildTarget buildTarget, string path) {
if (buildTarget!=BuildTarget.iOS) {
return;
}
string projectPath = PBXProject.GetPBXProjectPath(path);
PBXProject project = new PBXProject();
string fileText = File.ReadAllText(projectPath);
project.ReadFromString((fileText));
//获取targetGuid
string target = project.TargetGuidByName("Unity-iPhone");
//获取plist
string plistPath = path + "/Info.plist";
PlistDocument plist = new PlistDocument();
plist.ReadFromString(File.ReadAllText(plistPath));
//获取plist root
PlistElementDict plistDic = plist.root;
//---业务代码,仅供参考
//拷贝微信分享图标,到Resources目录下
CopyFileToXCodeProject(project, target, path,BuildTool.GetXcodeProDependentRootPath(), "Resources", "Icon.png");
//拷贝UnityAppController脚本
CopyFileToXCodeProject(project, target, path, BuildTool.GetXcodeProDependentRootPath(), "Classes", "UnityAppController.h");
CopyFileToXCodeProject(project, target, path, BuildTool.GetXcodeProDependentRootPath(), "Classes", "UnityAppController.mm");
//设置微信
SetWeiXin(path,plistDic, project, target);
//设置语音
SetGameLink(plistDic,project, target);
//设置其他
SetOther(path, plistDic, project, target);
///---业务代码,仅供参考
//覆盖plist文件
File.WriteAllText(plistPath, plist.WriteToString());
//覆盖Xcode工程
File.WriteAllText(projectPath, project.WriteToString());
}
private static void SetWeiXin(string xcodePath,PlistElementDict plistDic, PBXProject project, string target) {
//1.拷贝wei文件
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "wei", "libWeChatSDK.a");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "wei", "WechatAuthSDK.h");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "wei", "WXApi.h");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "wei", "WXApiObject.h");
//2.Add微信 scheme
AddURLSchemes(plistDic,"weixin", "wx5c9d9308db6097aa");
AddQueriesSchemes(plistDic,"weixin");
//3.添加依赖库
project.AddFrameworkToProject(target, "SystemConfiguration.framework", false);
AddLib(project,target, "libz.tbd");
AddLib(project, target, "libsqlite3.0.tbd");
AddLib(project, target, "libc++.tbd");
project.AddFrameworkToProject(target, "Security.framework", false);
project.AddFrameworkToProject(target, "CoreTelephony.framework", false);
project.AddFrameworkToProject(target, "CFNetwork.framework", false);
project.AddFrameworkToProject(target, "CoreGraphics.framework", false);
//4.buildsettings添加flags
project.AddBuildProperty(target, "OTHER_LDFLAGS", "-Objc");
project.AddBuildProperty(target, "OTHER_LDFLAGS", "-all_load");
//5.buildsettings添加头文件和库文件搜索路径
project.AddBuildProperty(target, "HEADER_SEARCH_PATHS", "$(SRCROOT)/wei");
project.AddBuildProperty(target, "LIBRARY_SEARCH_PATHS", "$(SRCROOT)/wei");
}
private static void SetGameLink(PlistElementDict plistDic, PBXProject project,string target) {
//ENABLE_BITCODE=False 关闭BitCode
project.SetBuildProperty(target, "ENABLE_BITCODE", "false");
//允许http
var atsKey = "NSAppTransportSecurity"; PlistElementDict dictTmp = plistDic.CreateDict(atsKey); dictTmp.SetBoolean("NSAllowsArbitraryLoads", true);
//添加-ObjC
project.AddBuildProperty(target, "OTHER_LDFLAGS", "-ObjC");
//添加库
AddLib(project,target, "libz.tbd");
AddLib(project,target, "libicucore.tbd");
project.AddFrameworkToProject(target, "Contacts.framework", false);
project.AddFrameworkToProject(target, "AddressBook.framework", false);
project.AddFrameworkToProject(target, "CoreTelephony.framework", false);
//权限
plistDic.SetString("NSMicrophoneUsageDescription", "沪乐麻将需要您的同意,才能使用您的麦克风用于游戏内的语音功能。");
}
private static void SetOther(string xcodePath,PlistElementDict plistDic, PBXProject project, string target) {
// 权限
plistDic.SetString("NSContactsUsageDescription", "是否允许此App访问你的通讯录");
plistDic.SetString("NSLocationUsageDescription", "位置权限将会在您创建或加入房间后应用,允许使用后可查自己和房间中其他玩家的位置信息");
plistDic.SetString("NSLocationWhenInUseUsageDescription", "位置权限将会在您创建或加入房间后应用,允许使用后可查自己和房间中其他玩家的位置信息");
plistDic.SetString("NSSiriUsageDescription", "是否允许Siri启动此App?");
//其他功能
//1.appstore内购
project.AddFrameworkToProject(target, "StoreKit.framework", false);
project.AddFrameworkToProject(target, "Foundation.framework", false);
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "IAPInterface.h");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "IAPInterface.m");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "IAPManager.h");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "IAPManager.m");
//2.截图保存到相册并刷新
plistDic.SetString("NSPhotoLibraryAddUsageDescription", "沪乐麻将需要您的同意,才能访问您的相册用来存储游戏截图。");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "PhotoManager.h");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "PhotoManager.m");
//3.网络状态监听
project.AddFrameworkToProject(target, "SystemConfiguration.framework", false);
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "ReachabilityF.h");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "ReachabilityF.m");
//4.应用内评分
//StoreKit.framework Foundation.framework
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "UnityStoreKit.h");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes", "UnityStoreKit.m");
//5.点击空白区域关闭键盘
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes/UI", "UnityView.h");
CopyFileToXCodeProject(project, target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes/UI", "UnityView+iOS.mm");
//7.重写下拉菜单
CopyFileToXCodeProject(project,target, xcodePath, BuildTool.GetXcodeProDependentRootPath(), "Classes/UI", "UnityViewControllerBase+iOS.mm");
//8.通过URL打开APP
AddURLSchemes(plistDic, "fymj", "fengyeshanghaimajiang");
}
private static void AddURLSchemes(PlistElementDict plistDic, string URLName, string URLSchemes) {
PlistElementArray urlTypes = null;
if (plistDic.values.ContainsKey("CFBundleURLTypes")) {
urlTypes = plistDic["CFBundleURLTypes"].AsArray();
}else{
urlTypes= plistDic.CreateArray("CFBundleURLTypes");
}
PlistElementDict itemDict = urlTypes.AddDict();
itemDict.SetString("CFBundleTypeRole", "Editor");
itemDict.SetString("CFBundleURLName", URLName);
PlistElementArray schemesArray1 = itemDict.CreateArray("CFBundleURLSchemes");
schemesArray1.AddString(URLSchemes);
}
private static void AddQueriesSchemes(PlistElementDict plistDic,string urlName) {
PlistElementArray queriesSchemes = null;
if (plistDic.values.ContainsKey("LSApplicationQueriesSchemes")) {
queriesSchemes =plistDic["LSApplicationQueriesSchemes"].AsArray();
} else {
queriesSchemes =plistDic.CreateArray("LSApplicationQueriesSchemes");
}
queriesSchemes.AddString(urlName);
}
// 只支持创建一级目录
private static void CopyFileToXCodeProject(PBXProject project,string target,string xcodePath,string fileRootPath,string floderName,string fileFullName) {
//判断xcode工程是否存在这个目录
if (!Directory.Exists(xcodePath + "/" + floderName)) {
Directory.CreateDirectory(xcodePath + "/" + floderName);
}
File.Copy(fileRootPath + "/" + floderName + "/" + fileFullName, xcodePath + "/" + floderName + "/" + fileFullName,true);
string fileGuid =project.AddFile(xcodePath + "/" + floderName + "/" + fileFullName, floderName +"/" + fileFullName, PBXSourceTree.Source);
project.AddFileToBuild(target, fileGuid);
}
private static void AddLib(PBXProject project,string target,string libName) {
project.AddFileToBuild(target, project.AddFile("usr/lib/"+ libName, libName, PBXSourceTree.Sdk));
}调用sh脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14public static void ExportIPA(string xcodeProjPath,string exportOptionsPath) {
DirectoryInfo dir = Directory.GetParent(Application.dataPath);
string fileName = dir.ToString() + "/" + "BuildTools" + "/" + "ExportIPA.sh";
string arguments = fileName + " " + xcodeProjPath + " " + exportOptionsPath;
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo.FileName = "/bin/bash";
process.StartInfo.Arguments = arguments;
process.StartInfo.CreateNoWindow = false; // 获取或设置指示是否在新窗口中启动该进程的值(不想弹出powershell窗口看执行过程的话,就=true)
process.StartInfo.ErrorDialog = true; // 该值指示不能启动进程时是否向用户显示错误对话框
process.StartInfo.UseShellExecute = false;
process.Start();
process.WaitForExit();
process.Close();
}sh脚本内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17!/bin/sh
参数判断
if [ $# != 2 ];then
echo "需要输入xcode工程文件夹路径和ExportOptions.plist文件路径"
exit
fi
cd $1
xcodebuild clean -project Unity-iPhone.xcodeproj -scheme Unity-iPhone -configuration Release
xcodebuild archive -project Unity-iPhone.xcodeproj -scheme Unity-iPhone -archivePath $1/Unity-iPhone.xcarchive
xcodebuild -exportArchive -exportOptionsPlist $2 -archivePath $1/Unity-iPhone.xcarchive -exportPath $1/Unity-iPhone.ipa
open $1/Unity-iPhone.ipaWindows平台:导出为.zip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
string dir = BuildTool.GetBuildPackageRootDir() + "/" + "PC/";
string locationPathName = dir + BuildTool.GetPCPackageName();
buildPlayerOptions.locationPathName = locationPathName;
buildPlayerOptions.scenes = BuildTool.GetBuildScene();
buildPlayerOptions.target = BuildTarget.StandaloneWindows;
buildPlayerOptions.options = BuildOptions.None;
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
BuildSummary summary = report.summary;
if (summary.result == BuildResult.Succeeded) {
Debug.Log("Build succeeded: " + summary.totalSize + " bytes");
//需要压缩的文件
//pc/ MonoBleedingEdge xxx_Data UnityCrashHandler32.exe UnityPlayer.dll xxx.exe MonoBleedingEdge
string exePath = locationPathName;
string dataPath =locationPathName.Replace(".exe","_Data");
string crashHandlePath =dir+ "UnityCrashHandler32.exe";
string dllPath = dir+ "UnityPlayer.dll";
string monoPath = dir + "MonoBleedingEdge";
// 添加目录下的所有文件
List<string> files = new List<string>();
files.Add(exePath);
files.Add(dataPath);
files.Add(crashHandlePath);
files.Add(dllPath);
files.Add(monoPath);
//zip命名
string archiveName = locationPathName.Replace(".exe", ".zip");
//使用Zip压缩工具压缩
ZipUtility.Zip(files.ToArray(), archiveName);
//删除其他文件
foreach (var file in files) {
if (File.Exists(file)) {
File.Delete(file);
} else if (Directory.Exists(file)) {
Directory.Delete(file,true);
}
}
//打开输出目录
BuildTool.OpenFolder(dir);
}
if (summary.result == BuildResult.Failed) {
Debug.Log("Build failed");
}其他代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29//获取当前编辑器选中的场景
public static string[] GetBuildScene() {
List<string> scenePath = new List<string>();
EditorBuildSettingsScene[] editorBuildSettingsScene = EditorBuildSettings.scenes;
if (editorBuildSettingsScene != null) {
foreach (var scene in editorBuildSettingsScene) {
scenePath.Add(scene.path);
}
}
return scenePath.ToArray();
}
//打开文件夹
public static void OpenFolder(string dir) {
System.Diagnostics.ProcessStartInfo info = new System.Diagnostics.ProcessStartInfo();
info.FileName = dir;//打开工程所在目录
System.Diagnostics.Process.Start(info);
}
//获取sh脚本路径
public static string GetIPAExportOptionsFilePath() {
DirectoryInfo dir = Directory.GetParent(Application.dataPath);
string path = dir.ToString() + "/" + "BuildTools" + "/" + "ExportOptions.plist";
return path;
}
//获取XCode工程依赖文件(需要向新生成的Xcode工程中添加的资源或脚本)路径
public static string GetXcodeProDependentRootPath() {
DirectoryInfo dir = Directory.GetParent(Application.dataPath);
string rootDir = dir.ToString() + "/XCodeProDependent";
return rootDir;
}
七、使用Jenkins构建任务
- 新建Item,设置General,可以添加描述,选择是否丢弃旧的构建,添加自定义参数等等
- 配置源代码管理,SVN项目地址,连接凭证,我选择的是先还原再更新的方式
- 构建触发器,当设置的条件满足时会自动进行构建
- 构建,可以选择你想用的方式进行构建,可使用bat命令、Shell命令、Unity编辑器命令等方式进行构建,编写命令时我们可以将General里面自定义的参数的和Jenkins提供给我们的参数添加到命令里面进行使用
- 构建后操作。构建完成后,我们可以归档生成的文件,如果有需要也可以在这里添加邮件通知
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 tiger!
评论