每一份努力都是有一份期盼,每一份付出都是为了有更多的收获。
本文记录一次搭建Jenkins自动参数化打包APK的实现过程和碰到的问题,实现了在Windows和Mac系统下的自动化打包流程。
因为Jenkins的安装过程在网上的教程很多,这里就不在赘述。
介绍
准备工作,因为要实现自动化打包APK,所以就需要从Jenkins调动对应系统的执行文件然后通过命令行调用Unity中的静态方法,因此首先我们首先需要一个Unity里面的静态方法来调用构建方法,具体脚本内容参考下面代码BuildTools.cs,因为是在Windows和Mac都可以自动构建,这时候在Windows系统下我们还需要准备一个bat文件来执行Unity命令行调用构建方法(具体内容参考下面脚本BuildClient.bat),在Mac系统下需要一个sh文件来执行Unity命令行调用构建方法(具体内容参考下面脚本BuildClient.sh)
BuildTools.cs
针对这篇代码这里进行一下说明:
一开始我们的代码入口是BuildApk方法,通过上述Bat或者sh脚本调进来的Unity的方法,在这个方法里面我们通过System.Environment.GetCommandLineArgs()获取到了当前Jenkins运行环境下的环境变量,来实现参数的传递,之后我们调用了ExecuteBuild方法,这个方法里面我们实现了AB包的构建,针对代码中自己的AB包构建方式,可以考虑删除这几个方式替换为自己的或者直接删除这几个方法(ExecuteBuild,GetBuildPackageVersion,CreateEncryptionServicesInstance)
在构建完成之后我们跳转到下一个方法StartBuildApk方法,这个方法里面就是主要构建的内容了,首先我们定义一个BuildPlayerOptions结构体,这个结构体里面的参数就是控制我们打包的一些参数,在当前脚本中我们分别设置了需要打包的场景列表,打包之后的位置,这次构建的目标平台,是否为开发包,之后调用BuildPipeline.BuildPlayer方式去构建,这构建之后注释的这些代码为在实现过程中碰到打包失败问题的时候打出Log来定位问题(这个过程碰到的问题太多,且往下看)。最后一个方法GetPathName就简单了,根据当前的时间创建对应的文件,来设置对应打好之后的包的目录。
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using System.IO;
using System;
using YooAsset;
using YooAsset.Editor;
using System.Linq;
public class BuildTools
{
[MenuItem("Build/Build APK")]
public static void BuildApk()
{
string[] args= System.Environment.GetCommandLineArgs();
bool isDebug = false;
foreach (string arg in args)
{
if (arg.Contains("--productName:"))
{
string productName = arg.Split(':')[1];
PlayerSettings.productName = productName;
}
if (arg.Contains("--version:"))
{
string version = arg.Split(':')[1];
PlayerSettings.bundleVersion = version;
}
if (arg.Contains("--isDebug:"))
{
string debug = arg.Split(':')[1];
isDebug = bool.Parse(debug);
}
}
ExecuteBuild(isDebug);
}
/// <summary>
///
/// </summary>
private static void ExecuteBuild(bool isDebug)
{
BuildParameters buildParameters = new BuildParameters();
buildParameters.StreamingAssetsRoot = AssetBundleBuilderHelper.GetDefaultStreamingAssetsRoot();
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildTarget = EditorUserBuildSettings.activeBuildTarget;
buildParameters.BuildPipeline = AssetBundleBuilderSettingData.Setting.BuildPipeline;
buildParameters.BuildMode = AssetBundleBuilderSettingData.Setting.BuildMode;
buildParameters.PackageName = AssetBundleBuilderSettingData.Setting.BuildPackage;
buildParameters.PackageVersion = GetBuildPackageVersion();
buildParameters.VerifyBuildingResult = true;
buildParameters.SharedPackRule = new ZeroRedundancySharedPackRule();
buildParameters.EncryptionServices = CreateEncryptionServicesInstance();
buildParameters.CompressOption = AssetBundleBuilderSettingData.Setting.CompressOption;
buildParameters.OutputNameStyle = AssetBundleBuilderSettingData.Setting.OutputNameStyle;
buildParameters.CopyBuildinFileOption = AssetBundleBuilderSettingData.Setting.CopyBuildinFileOption;
buildParameters.CopyBuildinFileTags = AssetBundleBuilderSettingData.Setting.CopyBuildinFileTags;
if (AssetBundleBuilderSettingData.Setting.BuildPipeline == EBuildPipeline.ScriptableBuildPipeline)
{
buildParameters.SBPParameters = new BuildParameters.SBPBuildParameters();
buildParameters.SBPParameters.WriteLinkXML = true;
}
var builder = new AssetBundleBuilder();
var buildResult = builder.Run(buildParameters);
if (buildResult.Success)
{
//EditorUtility.RevealInFinder(buildResult.OutputPackageDirectory);
StartBuildApk(isDebug);
}
}
private static string GetBuildPackageVersion()
{
int totalMinutes = DateTime.Now.Hour * 60 + DateTime.Now.Minute;
return DateTime.Now.ToString("yyyy-MM-dd") + "-" + totalMinutes;
}
//private static List<Type> GetEncryptionServicesClassTypes()
//{
// return EditorTools.GetAssignableTypes(typeof(IEncryptionServices));
//}
private static IEncryptionServices CreateEncryptionServicesInstance()
{
//var classType = GetEncryptionServicesClassTypes()[0];
return (IEncryptionServices)Activator.CreateInstance(typeof(EncryptionNone));
}
private static void StartBuildApk(bool isDebug)
{
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
// 获取已配置的场景列表
string[] scenes = EditorBuildSettings.scenes
.Where(scene => scene.enabled)
.Select(scene => scene.path)
.ToArray();
buildPlayerOptions.scenes = scenes;
//buildPlayerOptions.locationPathName = "D:\\XXX\\XXX\\ClientTest.apk";
//因为Windows和Mac平台下的路径不一样,这里进行区别对待
buildPlayerOptions.locationPathName = GetPathName(isDebug);
buildPlayerOptions.target = BuildTarget.Android;
buildPlayerOptions.options = isDebug ? BuildOptions.Development : BuildOptions.None;
UnityEditor.Build.Reporting.BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
//Debug.LogError("Start Build App Done!");
//UnityEditor.Build.Reporting.BuildSummary summary = report.summary;
//var a = report.steps;
//for (int i = 0; i < a.Length; i++)
//{
// var b = a[i].messages;
// for (int j = 0; j < b.Length; j++)
// {
// Debug.LogError("Build Step = " + b[j].content);
// }
//}
//Debug.LogError("Build Error count = " + summary.totalErrors);
//if (summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded)
//{
// EditorUtility.RevealInFinder("/XXX/XXX/XXX/DoneProject");
// Debug.LogError("Build App Done!");
//}
}
/// <summary>
/// mac path name
/// </summary>
/// <returns></returns>
private static string GetPathName(bool isDebug)
{
string path = "/XXX/XXX/XXX/DoneProject";
if (isDebug)
{
path = string.Format("{0}/Debug",path);
}
DateTime nowTime = DateTime.Now;
string dayName = nowTime.Month + "_" + nowTime.Day;
path = string.Format("{0}/{1}", path, dayName);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
path = string.Format("{0}/Client{1}.apk", path, nowTime.ToLongTimeString());
return path;
}
}
BuildClient.bat
bat文件说明
Windows系统下命令行模式运行Unity调用Unity方法的脚本,这里有些问题比如项目的运行路径其实应该为Jenkins的工作空间下创建的临时项目路径,所以这个脚本里面的项目路径和Log路径应该通过环境变量从Jenkins那边传过来,具体实现方式可以参考下面Mac环境下传参的逻辑(大家自己动手实现Windows版本哦)
- -quit :在其他命令执行完毕后退出 Unity Editor。这可能导致错误消息被隐藏(但是,它们仍会出现在 Editor.log 文件中)。
- -batchmode:以批处理模式运行 Unity。请始终将此命令与其他命令行参数结合使用,从而确保不会出现弹出窗口且无需任何人为干预。在执行脚本代码期间发生异常时,资源服务器更新失败时,或其他操作失败时,Unity 将立即退出并返回代码 1。请注意,在批处理模式下,Unity 会将其日志输出的最小版本发送到控制台。但是,日志文件仍包含完整的日志信息。当 Editor 打开某个项目时,您无法以批处理模式打开相同的项目;一次只能运行一个 Unity 实例。要检查是否正在以批处理模式运行 Editor 或独立平台播放器,请使用 Application.isBatchMode 运算符。如果在使用
-batchmode
时还没有导入项目,则目标平台为默认平台。要强制选择其他平台,请使用-buildTarget
选项。 - -buildTarget Android:在加载项目之前选择有效的构建目标。可能的选项包括:
Standalone、Win、Win64、OSXUniversal、Linux64、iOS、Android、WebGL、XboxOne、PS4、WindowsStoreApps、Switch、tvOS。 - -projectPath:在指定路径下打开项目。如果路径名包含空格,请将其用引号引起来。
- -executeMethod:执行的Unity的静态方法
- -logFile:Log文件路径
- --productName:项目名称,传过来的参数
- --version:版本号,传过来的参数
这些命令的意义和其他更多命令行参数可以参考Unity官方文档。
"D:\XXX\Unity 2020.3.29f1\Editor\Unity.exe" ^
-quit ^
-batchmode ^
-buildTarget Android ^
-projectPath "D:\XXX\XXX\XXX" ^
-executeMethod BuildTools.BuildApk ^
-logFile "D:\XXX\XXX\XXX\Logs\AssetImportWorker0.log" ^
--productName:%1 ^
--version:%2
BuildClient.sh
sh文件说明:
Mac系统下命令行模式运行Unity调用Unity方法的脚本,因为在Jenkins中设置了Svn拉取项目,所以这里面用到的项目路径和Log文件路径都需要通过Jenkins那边传过来,这里用到了环境变量来传参,更多可控参数打包大家自己也可以通过相同的方式举一反三哦。具体的命令和含义参考上面Windows的解释。
#!/bin/bash
UNITY_PATH="/Applications/Unity/Hub/Editor/2020.3.29f1/Unity.app/Contents/MacOS/Unity"
PROJECT_PATH=${WORKSPACE}
LOG_FILE="${PROJECT_PATH}/Logs/AssetImportWorker0.log"
EXECUTE_METHOD="BuildTools.BuildApk"
PRODUCTNAME=${PRODUCT_NAME}
VERSION=${PRODUCT_VERSION}
ISDEBUG=${IS_DEBUG}
${UNITY_PATH} -quit -batchmode -buildTarget Android -projectPath ${PROJECT_PATH} -executeMethod ${EXECUTE_METHOD} -logFile ${LOG_FILE} --productName:${PRODUCTNAME} --version:${VERSION} --isDebug:${ISDEBUG}
好了现在准备工作做好了,让我们开始配置一个Jenkins工程,打开Jenkins的管理网址,一般为:你部署Jenkins的IP:8080
我们这里选择一个自由风格的项目,
到这个界面
选择参数化构建过程,这里面定义的参数最后打包的时候可以选择传递给脚本去,就是我们上面用到的
因为我们的每一次打包都需要将项目更到最新,因为项目使用的是Svn这里选择源码管理,Subversion这里配置自己的仓库地址和验证信息,如果没有这个选项,需要在Jenkins插件管理里面下载。
下面在构建步骤里面我们开始增加构建步骤选择执行Shell或者执行Window批处理命令,类似于bat脚本
在Shell里面增加环境变量传递参数,这里面说明一下$ProductName这种方式是获取我们上面配置的参数化构建的参数的名字,然后通过环境变量的方式传给执行脚本里面去。
再添加一个Shell,将生成的APK文件上传到对应的局域网服务器中去,这里有多种方式选择,比如FTP文件服务器,rsync命令等。
到此整个流程已经完成。
问题
部署的过程是艰辛的碰到的问题也是多种多样的,这里记录一下:
1.设置平台
刚开始的时候执行Unity命令行的时候设置的项目是本地固定文件的项目,结果在构建的过程中发现无论怎么修改都不生效,后面才知道这个Jenkins是会从在自己的工作空间下通过版本控制器拉取一份新的代码,这时候也随之衍生了很多问题,比如项目是Android平台的,但是从Svn拉取下来的项目默认是Windows平台的,对于这种问题找到了两种解决方案:
方案一:通过Unity打开Jenkins工作空间下的项目,选择Android平台,但是这样做有一个缺点,就是只要Jenkins中配置了下图这个,那么每次都会重新拉一个新项目,那我们这种方式也就失效了,不过一般应该没人这么做
方案二:使用Unity命令行的时候Unity提供了一个命令来选择有效的构建目标
2.包体大小不对
构建出来的包的大小始终跟用手点击打出来的包的大小不一样,后面发现项目中有的文件使用了SVN外链的形式,正常情况下Jenkins下载出来的项目是不完全的,这个问题的解决方案也是很简单的,在工程配置界面找到下图这个,不要勾选就行啦(这个可能默认是不勾选的,但是我在配置过程中可能手贱勾选了,后面导致出问题了)
3.Jenkins用户不同于登录用户
这里记录一个致命问题,刚开始的时候在配置的过程中一切都挺顺利,也能出包,后面因为修改了Jenkins运行的用户,导致始终无法正常打包,不是长时间卡在了Unity构建的时候,就是在构建的时候报错等等问题,刚开始还想着去解决这些问题,经过了大量时间的验证,后面发现这条路走不通,还是将Jenkins的运行用户修改为当前登录账户,这里面的原因可以参考这个老哥的博客,感觉很有道理。