作者介绍:铸梦xy。IT公司技术合伙人,IT高级讲师,资深Unity架构师,铸梦之路系列课程创始人。
目录
Unity Mac Jenkins+Unity+xCode+fir+钉钉群通知
1.安装Jenkins
这里推荐使用命令行安装,这样基本不会弹出权限问题,如果直接使用dmg包进行安装,在自动化的过程中会弹出很多权限弹窗。
Mac Jenkins下载地址:https://www.jenkins.io/download/lts/macos/
进入以后出现以下界面
打开终端,复制命令 安装Jenkisn
brew install jenkins-lts
等待结束之后就安装成功了。
2.启动Jenkins
然后输入命令:
brew services start jenkins-lts
启动Jenkisn
启动完成后打开浏览器 输入:http://localhost:8080 进入Jenkins后台。
在进入之前需要输入管理员密码以及下载一堆插件,需要等待些时间。
等插件安装完成之后我们的Jenkins就算跑起来了。
3.Jenkins网页配置
然后点击 系统管理-插件管理 进入插件下载界面
接着在可选插件中 搜索Unity 安装一下Unity3d Plugins ,我这里已经安装过了。
Unity3d Plugins 安装完成之后 点击 系统管理-全局工具配置
进入之后配置一下我们打包的Unity引擎路径以及版本号
配置完成之后,我们就可以去创建打包工程了。
4.Jenkins打包工程创建
1.新建任务
工程创建完成之后,根据项目需求配置一下Unity打包时需要的参数,一般来说常用的是布尔,选项参数(枚举),和String 字符
工程路径一定要配上
其余的参数跟据自己的需求随意配置。
配置完成之后,我们就可以把参数传给Unity了。
5.写入参数给Unity打包使用
这里笔者把参数写成了一个txt文件,放到了Unity Asset同级的目录下,以便Unity在打包时,能够得到参数。
#写入参数
echo "Version:${Version}:VersionCode:${VersionCode}:Name:${Name}:MulRedering:${MulRedering}:Release:${Release}:OpenLog:${OpenLog}:isShowVersion:${isShowVersion}:isConfusion:${isConfusion}:teamCode:${teamCode}"> ${WorkPath}/jenkinsParam.txt
${version} 表示取version的值
以:进行拼接,key为Version,value为Version的值。
拼接完成后写入 workPath工程目录下,名字为JenkinsParam.txt
配置如下:
6.调用Unity打包方法进行打包
#调用Unity方法进行打包
/Applications/Unity/Unity.app/Contents/MacOS/Unity -projectPath ${WorkPath} -executeMethod BuildApp.BuildiOS -quit -batchmode
cd ${WorkPath}
在使用Jenkins打包时要确保该工程已经关闭,否则打包会出错。
上面方法表面调用 Unity工程下的BuildApp.BuildiOS方法
7.Unity C# 打包方法
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using LitJson;
/// <summary>
/// 发布APP
/// </summary>
public class BuildApp
{
public static string m_AndroidPath = Application.dataPath + "/../BuildTarget/Android/";
public static string m_IOSPath = Application.dataPath + "/../BuildTarget/IOS/";
public static string m_JenkinsParamPath = Application.dataPath + "/../jenkinsParam.txt";
[MenuItem("Build/IOS")]
public static void BuildiOS()
{
PlayerSettings.iOS.buildNumber = "1.0";
#if UNITY_WINDOW
BuildSeting buildSeting = GetAndroidBuildSetring();
#else
BuildSeting_iOS buildSeting = MacGetiOSSeting();
#endif
string suffx = iOSSeting(buildSeting);
AssetDatabase.Refresh();
//绝对储存路径
//string savePath = m_IOSPath + buildSeting.Name + "_iOS" + suffx + string.Format("_{0:yyyy_MM_dd_HH_mm}", DateTime.Now);
string savePath = m_IOSPath;
if (File.Exists(savePath))
{
JenkinsTools.DeleteDir(savePath);
}
Debug.Log(savePath);
BuildPipeline.BuildPlayer(FindEnableEditorrScenes(), savePath, EditorUserBuildSettings.activeBuildTarget, BuildOptions.None);
}
private static string[] FindEnableEditorrScenes()
{
List<string> editorScenes = new List<string>();
foreach (EditorBuildSettingsScene scene in EditorBuildSettings.scenes)
{
if (!scene.enabled) continue;
editorScenes.Add(scene.path);
}
return editorScenes.ToArray();
}
[MenuItem("Build/打开日志")]
public static void LoadReprot()
{
GameObject obj = GameObject.Find("Reporter");
if (obj == null)
{
GameObject reportObj = GameObject.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/ThirdParty/Unity-Logs-Viewer/Reporter.prefab"));
reportObj.name = "Reporter";
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
[MenuItem("Build/关闭日志")]
public static void DestoryReprot()
{
GameObject obj = GameObject.Find("Reporter");
if (obj != null)
{
GameObject.DestroyImmediate(obj);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
[MenuItem("Build/读取IOS参数")]
public static BuildSeting_iOS MacGetiOSSeting()
{
BuildSeting_iOS seting = new BuildSeting_iOS();
string jenkinsParam = File.ReadAllText(m_JenkinsParamPath);
Debug.Log(jenkinsParam);
string[] Params = jenkinsParam.Split(':');
for (int i = 0; i < Params.Length; i++)
{
Debug.Log(Params[i]);
string key = Params[i];
string value = "";
if (i < Params.Length - 1)// key vlaue Out index
{
value = Params[i + 1];// Get Value
}
if (string.Equals(key, "Version"))
{
seting.Version = value;
}
else if (string.Equals(key, "VersionCode"))
{
seting.VersionCode = value;
}
else if (string.Equals(key, "Name"))
{
seting.Name = value;
}
else if (string.Equals(key, "MulRedering"))
{
bool.TryParse(value, out seting.MulRedering);
}
else if (string.Equals(key, "Release"))
{
bool.TryParse(value, out seting.Release);
}
else if (string.Equals(key, "OpenLog"))
{
bool.TryParse(value, out seting.OpenLog);
}
else if (string.Equals(key, "isShowVersion"))
{
bool.TryParse(value, out seting.isShowVersion);
}
else if (string.Equals(key, "isConfusion"))
{
bool.TryParse(value, out seting.isConfusion);
}
else if (string.Equals(key, "teamCode"))
{
seting.teamCode = value;
}
}
Debug.Log("seting BuildCode: " + seting.VersionCode);
Debug.Log("seting Version:" + seting.Version);
Debug.Log("seting Name:" + seting.Name);
Debug.Log("seting MulRedering:" + seting.MulRedering);
Debug.Log("seting Debug:" + seting.Release);
Debug.Log("seting OpenLog:" + seting.OpenLog);
Debug.Log("seting UpdateFishAB:" + seting.isConfusion);
return seting;
}
/// 根据读取的数据 在Unity中设置对应的参数
private static string iOSSeting(BuildSeting_iOS seting)
{
string suffx = "_";
Debug.unityLogger.logEnabled = Debuger.EnableLog = seting.OpenLog;
if (seting.OpenLog)
{
LoadReprot();
}
PlayerSettings.MTRendering = seting.MulRedering == true ? true : false;
if (seting.Release == true) //测试包 处理测试配置
{
CustomTools.SwitchReleaseServer();
suffx += "Release";
}
else
{
CustomTools.SwitchTestServer();
// 其他渠道包待定
suffx += "Test";
}
if (!string.IsNullOrEmpty(seting.Version))
{
PlayerSettings.bundleVersion = seting.Version;
suffx += "_" + seting.Version;
}
if (!string.IsNullOrEmpty(seting.VersionCode))
{
PlayerSettings.iOS.buildNumber = seting.VersionCode;
suffx += "_" + seting.VersionCode;
}
if (!string.IsNullOrEmpty(seting.Name))
PlayerSettings.productName = seting.Name; //包名
if (seting.Release)
{
EditorUserBuildSettings.development = false;
suffx += "_Release";
}
else
{
EditorUserBuildSettings.development = true;
EditorUserBuildSettings.connectProfiler = true;
}
if (seting.isShowVersion)
{
string saveStrs = ConvertToJson(seting.isShowVersion);
Utils.WriteFileWithEnding(Application.dataPath + "/Resources/" + ResourcesFolder.Txts, ResourcesFile.buildInfo, saveStrs);
}
else
{
Utils.DeleteFile(Application.dataPath + "/Resources/" + ResourcesFolder.Txts + ResourcesFile.buildInfo + ".txt");
}
if (seting.teamCode.Trim() == "teamcpdexxx")//主账号
{
Debug.LogError("设置子账号包名:" + "com.baoming.xx");
#if UNITY_IOS
BL_BuildPostProcess.bundleIdentifier = "com.baoming.xx";
#endif
}
else
{
Debug.LogError("设置子账号包名:" + "com.xxxx.xx");
#if UNITY_IOS
BL_BuildPostProcess.bundleIdentifier = "com.xxxx.xx";
#endif
}
AssetDatabase.Refresh();
return suffx;
}
}
public class BuildSeting_iOS
{
//版本
public string Version = "";
//版本🐎
public string VersionCode = "";
//
public string Name = "";
//
public bool MulRedering = false;
//
public bool Release = false;
//
public bool OpenLog = false;
//是否把代码编译成C++
public bool isShowVersion = false;
//
public bool isConfusion = true;
public string teamCode = "";
}
到这里就已经能正常导出xcode工程了
xcode工程导出路径已经声明好了: Application.dataPath + “/…/BuildTarget/IOS/”;
导出xcode工程后确保手动使用xcode在不修改配置的情况下能正常导出ipa,然后在进行下面的操作。
8.调用xcode归档 ipa
导出ipa为们需要两个文件,一个是export.sh 一个是自己归档时的plist文件
这里都会提供。
首先创建一个export.sh 和plist文件,按照以下目录存放 ,路径是在自己项目的Asset同级下,文件名不要搞错。
1.export.sh 文件
M_XCODE_PATH="../IOS"
M_XCODE_NAME="Unity-iPhone"
M_ARCHIVE_PATH="../ArchiveTemp/Output/$2/archive"
M_EXPORT_PATH="../ArchiveTemp/Output/$2/app"
M_ExportOptionsPlist="../IOSBuildTools/export.plist"
M_DEVELOPMENT_TEAM=$1
echo "M_ARCHIVE_PATH=${M_ARCHIVE_PATH} M_EXPORT_PATH=${M_EXPORT_PATH} M_ExportOptionsPlist=${M_ExportOptionsPlist} M_DEVELOPMENT_TEAM=${M_DEVELOPMENT_TEAM}"
cd ${M_XCODE_PATH}
echo $PWD
echo $M_TIME
echo $M_ARCHIVE_PATH
echo $M_EXPORT_PATH
#remove plist
echo "Remove file${M_ExportOptionsPlist}"
rm -rf ${M_ExportOptionsPlist}
#new plist
echo ${M_ExportOptionsPlist}
echo "<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>compileBitcode</key>
<false/>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>development</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>"${M_DEVELOPMENT_TEAM}"</string>
<key>thinning</key>
<string><none></string>
</dict>
</plist>
" > ${M_ExportOptionsPlist}
xcodebuild -workspace ${M_XCODE_NAME}.xcworkspace -scheme ${M_XCODE_NAME} -configuration Release DEVELOPMENT_TEAM="${M_DEVELOPMENT_TEAM}" CODE_SIGN_IDENTITY="iPhone Developer" CODE_SIGN_STYLE="Automatic" clean archive -archivePath "${M_ARCHIVE_PATH}" -allowProvisioningUpdates
2.export.plist 文件
<?xml version=1.0 encoding=UTF-8?>
<!DOCTYPE plist PUBLIC -//Apple//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd>
<plist version=1.0>
<dict>
<key>compileBitcode</key>
<false/>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>development</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>9XXXXXXXC</string>
<key>thinning</key>
<string><none></string>
</dict>
</plist>
接下来回到Jenkisn配置一下 export.sh脚本的调用
如下:
#!/bin/sh -e
#运行xcode导出ipa
chmod +x export.sh
cd ${WorkPath}/BuildTarget/IOSBuildTools
sh export.sh ${teamCode} Target
上面命令主要意思,首先给 export.sh 予权限 然后cd到该文件所在目录下,执行该sh文件并传入参数
${teamCode} 是自己打包证书的证书码,取自jenkins配置参数,target表示归档出的ipa的文件夹,如果没有则自动创建。
jenkins构建名配置一览:
到了这一步,如果流程配置以及路径没有问题,那么就能正常的导出一个Ipa包了。
有两个重点需要强调一下:
1.首先要保证通过Jenkins能正常导出Xcode工程。
2.其次保证手动使用导出的Xcode能够在不修改xcode工程下能正常归档出ipa
注意:Xcode相关的配置可以放到Unity导出Xcode时[PostProcessBuild]标记的静态方法下进行配置。如果不知道如何配置,可以百度一下,这里就不细讲了。
下面介绍下如果把ipa上传到fir并进行下载
9.上传ipa到fir分发平台
下面是笔者上传fir的配置
上传命令:
#!/bin/bash --login
#上传到fir分发平台
cd ${WorkPath}/BuildTarget/ArchiveTemp/Output/Target/app
mv "xxxxxxxx.ipa" "${AppName}.ipa" #修改ipa名字
echo "开始登陆fir"
fir login ${FirToken}
echo "登陆完成"
echo "开始上传到fir"
fir publish ${WorkPath}/BuildTarget/ArchiveTemp/Output/Target/app/${AppName}.ipa
echo "上传fir完成"
这里有一点需要注意 ${FirToken} 是取自jenkins配置好的参数,因登陆fir需要token验证,所以笔者就直接把token配置到了参数中,firToken可以到fir官网,个人信息内找到。
温馨提示:可以先手动尝试输入命令进行上传,上传成功后,在布置到jenkins中。
下面是笔者手动上传的演示:
第一个框 是cd到ipa所在的目录下
然后修改ipa的名字
修改完成后,登陆fir
第二个框是把Ipa上传到fir,需要传ipa的绝对路径。
上传过程中如下图。
上传完成后我们就拿到了一个下载地址,这个地址我们可以直接放到浏览器中打开,下载下来的ipa就可以直接装到手机上。
我们也可以拿该链接做顶顶通知。
注意:该地址为固定地址,不管我们这个包上传多少次,该地址都不会变。
10.Jenkins集成钉钉通知流程
继承钉钉通知之前需要先在jenkins中安装一些插件:
DingTalk
Environment Injector Plugin
Parameterized Trigger plugin
如下:
1.
2.
3.
在安装插件期间,我们可以打开钉钉,创建一个打包通知群 创建教程:
https://blog.csdn.net/qq_41980563/article/details/107045894
我们的钉钉群创建完成后和插件安装完成后,点击 系统管理-系统设置,添加一下钉钉的配置id 不用填,jenkins会自动生成,
我们要填的只有名称和webhook
钉钉信息配置完成后,我们回到jenkins工程配置里添加一下构建操作和构建后操作:
我们在这里注入一下环境变量,供我们钉钉通知时使用取出APkName
设置环境变量
11.创建钉钉通知工程
这里需要创建一个流水线工程
设置两个参数
配置工作流水线
流水线源码:
pipeline {
agent any
environment {
version = '0.1'
}
stages {
stage('link'){
steps {
echo "${env.version}"
echo '测试 LINK 消息...'
echo "${params.apkName}"
}
post {
success {
script {
if(params.buildType=='APkName-IOS'){
dingtalk (
robot: '55fd7ff3-1f55-499d-ba4d-1500d25f702b',
type: 'LINK',
title: 'APKName',
text: [
'点击标题下载',
"${params.apkName}",
],
messageUrl: '自己的fir下载地址',
picUrl: '自己的钉钉通知显示的应用图标下载地址'
)
}
else if(params.buildType=='YallaLudoHD-Android'){
dingtalk (
robot: '55fd7ff3-1f55-499d-ba4d-1500d25f702b',
type: 'LINK',
title: 'APKname-Android',
text: [
'点击标题下载',
"${params.apkName}",
],
messageUrl: '自己的fir下载地址',
picUrl: '自己的钉钉通知显示的应用图标下载地址'
)
}
}
}
}
}
}
}
根据自己的需求进行修改。
12.增加构建后通知
回到自己的Jenkins打包项目内 在增加一个构建后操作,操作的就是刚刚配置好的流水线。
然后就可以通过Jenkins打包进行测试了,如果要测试钉钉通知,可以先把前面的步骤给注视掉,只保留钉钉通知,测试即可。
整个过程是没有讲的特别细,但是都是核心,只要流程上不出问题,就成功了。
温馨提示:Jenkins第一次打包期间最好是能够翻墙,不然有的配置Jenkins下载不到,他自己就会包错。
13.集成完成
觉得有用的话,动动小手点个关注,更多好用工具、干货文章着你!