jenkins keypoint - woodelf-treetop/rcwiki GitHub Wiki

整体思路:

1. 在unity上添加一个菜单,点击菜单选项,在弹窗上配置打包需要的选项数据。

2. 写Jenkins打包的入口方法,调用unity的打包方法,传入data。

3. Jenkins命令行调用unity上Jenkins打包的入口方法,进行打包。

这次将非渠道、国内渠道、海外渠道分三个任务构建的。因为,国内和海外渠道用的是不同的Android工程。而非渠道不需要进行gradle的配置。

第一部分 unity上进行打包

思路:打包分为 : 非渠道包(unity直接打包,出apk包)、渠道包(接入SDK之后的,点击打包之后是导出Android工程)、审核包(导出Android工程)、 热更资源(修改文件,点打包之后,显示修改的文件夹)

具体实现:

  1. 绘制弹窗的界面。
    思路:写一个类,继承自EditorWindow类。主要用到了MenuItem: unity editor上创建菜单;之后的静态方法,会在点击菜单的时候被执行。进行窗口的初始化和显示。
//通过点击MenuItem按钮来创建对话框
    [MenuItem("Tools/PackagingTool/AutoPackaging")]
    static void Init()
    {
        //第二个参数是设置窗口是否浮动,默认是false,可省。第三个参数不设置默认是类名。
        AutoPackagingWindow window = (AutoPackagingWindow)EditorWindow.GetWindow(typeof(AutoPackagingWindow), false, "打包配置", true);
        //窗口的最大尺寸
        window.maxSize = new Vector2(500, 600);
        window.Show();
    }

之后,在unity的OnGUI方法里进行具体控件的绘制。

 //绘制
    private void OnGUI()
    {
        tabSelected = GUILayout.Toolbar(tabSelected, types, GUILayout.Width(495));
        
        if (tabSelected == 0)
        {
            currentData = noChannelData;
            DrawPanel();
        }
        else if(tabSelected == 1)
        {
            currentData = channelData;
            DrawPanel();
        }
        else if(tabSelected == 2)
        {
            currentData = officialData;
            DrawPanel();
        }
        else
        {
            hotUpdateServerData.packageType = tabSelected;
            DrawHotupdateResPanel();
        }
    }

绘制每个控件,主要用到了EditorGUI这个类的方法。

  1. 配置界面显示需要的数据:
    思路:下拉框中的需要配置的数据,如服务器地址、热更地址、渠道等,配置在xml文件里。之后,写一个单独的解析数据的单例类(数据配置只有一份,不需要创建多个对象。),将XML解析的数据分别存到字典里,便于找到一一对应关系。之后,再把字典的数据,分别存到数组里(EditorGUI.Popup方法需要的参数为数组类型)。
    注意:channeldata、serverdata、hotupdatedata分别写三个类。这样易于以后修改扩展。再写一个类,用来保存打包工具所有需要配置选项的数据。

  2. 修改AppConst的原有配置:
    由于需要配置两份数据,一份是unity上运行的情况,一份是打包工具需要的数据, 在AppConst里用宏命令做区分。unity编辑器运行的时候,还是取原有的值。Android打包的时候,取打包工具数据类的数据。
    思路:由于打包工具的数据类是在Editor文件夹下,只有在打包工具运行的时候,才能取到值,而运行游戏包的时候,不会运行打包工具,是取不到值的。思路是:写一个生成文件的类,根据每次unity弹窗上选择的配置不同,每次都生成一个新的CS脚本(这里当时是用文件写入一行行写入的。应该还有更好的方法)。

  3. 重置按钮:
    点击重置按钮将设置恢复默认值。
    思路:初始状态先将data的值设为默认值。然后将currentData的值,根据不同packageType设置为不同的默认值。在currentData的值不断发生变化的同时,因为引用的是同一个对象,默认值也发生变化。在点击恢复默认按钮的时候,先将被修改的默认值重置回来。再将默认值赋值给currentData.

private OptionData noChannelData = OptionData.SetDefaultNoChannelPanel();
private OptionData channelData = OptionData.SetDefaultChannelPanel();
private OptionData officialData = OptionData.SetDefaultOfficialPanel();
private void OnGUI()
{
    tabSelected = GUILayout.Toolbar(tabSelected, types, GUILayout.Width(495));
        
    if (tabSelected == 0)
    {
        currentData = noChannelData;
        DrawPanel();
    }
    else if(tabSelected == 1)
    {
        currentData = channelData;
        DrawPanel();
    }
        .....
}

private void ResetToDefault()
{
    if(currentData.packageType == 0)
    {
        noChannelData = OptionData.SetDefaultNoChannelPanel();
        currentData = noChannelData;
    }else if(currentData.packageType == 1)
    {
        channelData = OptionData.SetDefaultChannelPanel();
        currentData = channelData;
    }
    else
    {
        officialData = OptionData.SetDefaultOfficialPanel();
        currentData = officialData;
    }
}

注意:这里即使不在重置方法里对currentData重新赋值,依然可以恢复默认。可能原因是,OnGUI方法在点击按钮的时候,被执行了。

  1. 出包前修改配置和文件(playersettings, gameConfig, fastAddress文件)
    思路:playersettings的设置主要通过PlaySettings这个类的方法来设置。文件的修改用文件读写操作即可。
    注意:这里修改加速点文件的时候,因为是json字符串,用到了LitJson来解析。这里可以通过SetJsonType方法来设置数据类型。
//修改FastAddress文件的配置
    public static void SetFastAddressFile(string serverName)
    {
        StreamWriter stream;
        string fastAddressFilePath = Application.dataPath + "/StreamingAssets/fastAddress.txt";
        FileStream fs = CreateFile(fastAddressFilePath);
        JsonData data = new JsonData();
        JsonData httpList = new JsonData();
        httpList.SetJsonType(JsonType.Array);
        JsonData tcpList = new JsonData();
        foreach(string tcpStr in ParseConfigData.Instance.GetServerDataByName(serverName).TcpList)
        {
            tcpList.Add(tcpStr);
        }
        JsonData givenTcp = new JsonData();
        foreach(KeyValuePair<string,string> kvp in ParseConfigData.Instance.GetServerDataByName(serverName).GivenTcp)
        {
            givenTcp[kvp.Key] = kvp.Value;
        }
        data["http"] = httpList;
        data["tcp"] = tcpList;
        data["giventcp"] = givenTcp;
        using (stream = new StreamWriter(fs))
        {
            stream.WriteLine(data.ToJson());
        }
        stream.Close();
    }
  1. 打包操作
    流程:生成配置和协议
    执行buildassetsbundle
    执行buildAndroidresource
    执行出包操作(Android工程或是apk包)

注意:因为unity在每执行完一步操作之后,都会编译,这个时候,如果执行下一步,会导致unity崩溃掉。因而需要在unity编译的回调里去调用打包的方法。并且用if else区分判断每个步骤。这样,确保每次编译完成之后再执行下一步。在执行打包操作之前,先修改配置和文件。这之后,unity并没有自动编译,因而需要手动去执行第一步。
另外需要注意的是,unity editor window静态的变量在编译的时候,会被销毁掉。而序列化的对象不会,因而,可以在类上加上 [System.Serializable]。
还有就是,需要延时调用,使用delayCall的时候,不能带参数。可以用无参数的方法再封装一层,内部调用有参数的方法。通过静态的全局变量把值传进去。
如果是没法通过传参获取到的数据的话,可以将值存在EditorPrefs里。然后通过这里取值。 OnScriptsReloaded()这个方法是unity的编译回调,必须为static的,去掉会报错。

//执行android打包
        public static void BuildAndroidPlayer()
        {
            Debug.Log(System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));

            // Set luajit path
            if (AppConst.LuaByteMode) // 加密模式
            {
                string luajitPath = "/Users/comic/littleTools/Luajits/204_luajit32/bin/luajit-2.0.4";
                if (!luajitPath.Equals(Packager.pathLuajitAndroid))
                {
                    Packager.pathLuajitAndroid = luajitPath;
                }
            }
            if (EditorApplication.isCompiling)
            {
                Debug.Log("Editor is compiling scripts, please wait. Current method name: 'BuildAndroidPlayer'.");
                return;
            }
            Debug.Log(System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
            Debug.Log("BuildAndroidPlayer start");
            string[] scenes = PerformBuild.GetBuildScenes();
            string path = "";
            if (EditorPrefs.GetInt("CurrentPackageType") == 0)
            {
                path = GetPath() + "/" 
                    + "/demon-release";
            }
            else
            {
                path = GetPath() +"/" + ChangeChannelName(EditorPrefs.GetString("CurrentChannelId"));
            }
            if (Directory.Exists(path))
            {
                Directory.Delete(path, true);
            }
            Directory.CreateDirectory(path);
            if (scenes == null || scenes.Length == 0 || path == null)
                return;
            Debug.Log(string.Format("Path: \"{0}\"", path));
            for (int i = 0; i < scenes.Length; ++i)
            {
                Debug.Log(string.Format("Scene[{0}]: \"{1}\"", i, scenes[i]));
            }
            string error = ExportAndroidProject(path, EditorPrefs.GetInt("CurrentPackageType"));
            if (!string.IsNullOrEmpty(error))
            {
                if (UnityEditorInternal.InternalEditorUtility.inBatchMode) // 如果处于Jenkins打包状态,则终止打包流程。
                {
                    EditorApplication.Exit(1);
                }
                else // 如果处于Editor状态,则直接返回。
                {
                    return;
                }
            }
            ShowFileInExploer(path);
        }

第二部分 Jenkins打包配置

  1. Jenkins下载安装和环境配置:
    mac环境下的安装,可以从网上下载Jenkins的war包,将war包放在Tomcat的webapp下面。需要安装Tomcat7。也可以在linux环境下安装,详见:https://blog.csdn.net/qq_35868412/article/details/89475386
  2. 浏览器启动Jenkins。如果是Jenkins的war包放到Tomcat上。访问网址:http://192.168.1.24:8080/jenkins 192.168.1.24是Mac电脑的IP地址,jenkins是webapp下放的war包的名称。由于这样的方式安装,默认是将workspace放在.jenkins文件夹下。unity打不开,无法识别这个路径。后重新安装jenkins,地址:http://192.168.1.102:8080
  3. 进入之后会要求设置初始密码。按照提示的路径,输入初始密码即可。之后的界面会提示让修改密码,设置新密码。
  4. 之后会提示下载插件。一般情况下,按照推荐的,下载推荐的插件即可。这步跳过之后,也可以在系统管理-> 插件管理的界面上下载或是上传插件。
  5. 如果进入界面后提示缺少一些插件,可以在国内的镜像网站下下载,之后上传到Jenkins上。镜像网站:https://mirrors.tuna.tsinghua.edu.cn/jenkins/plugins/注意:安装完推荐插件,如果有些插件提示安装失败了,可以先跳过。进入主界面之后,再手动将插件上传到Jenkins上。
    上传方法:主界面系统管理 -> 插件管理 -> 高级 -> 上传插件
  6. 进入主界面之后就可以新建项目了。点击新建任务,之后输入项目的名称,选择构建一个自由风格的软件项目。
  7. 项目配置:
    General选项卡,描述可以随便填写。之后,如果需要配置动态参数,就勾选参数化构建过程这一项。
    如果需要配置动态参数,需要安装插件 DynamicParameter和role-strategy。在插件管理那里上传即可。之后便可以配置动态参数。选择DynamicChoiceParameter这一项,Name为变量名。以配置选择服务器为例:serverName。ChoicesScript这里写脚本。
def server=["dev","elite","prod"]

其它配置选项,同理。这样配置后,在构建时,会出现下拉框,让选择参数,再构建。
以TapTap渠道包为例,需要配置的参数有服务器地址、热更地址、是否执行build assets bundle 、是否执行build android resource等。还有执行Android打包时,需要配置的出包类型、渠道等。
在Description处,写对配置参数的描述。便于理解参数含义。
8、勾选限制项目运行节点一项。可以选择在哪个节点运行Jenkins。这里填beijing_mac。表示在Mac电脑上运行。即使在其它电脑访问Jenkins,如果是这样配置,也是在Mac上打包。
9、源码管理选项卡,配置Git仓库。
如果需要配置多个仓库,安装插件MultipleSCMsPlugin。之后,选择MultipleSCM选项,添加Git仓库。
RepositoryURL: 输入Git仓库地址:例如:[email protected]:demon-college/client.git
Credentials: 这个是用于验证的。需要在凭据选项界面,添加凭据。类型选择private key那一项,添加配置Git仓库的私钥。之后,选择添加好的那一项。
关于查看私钥:https://blog.csdn.net/iceking66/article/details/80563716
Branches to build:填写打包的分支,如:*/dev
源码库浏览器:选择gitlab。URL填写gitlab的地址:如:https://gitlabbj.dragonest.com:10443/demon-college/client.git Version:填写对应的版本号。
后面,AdditionalBehaviours: 这里可以配置检出的子目录。选择检出到子目录一项。在里面填入对应的子目录,如:client。
注意:这里配置的是相对路径。Mac的工作目录配置的是:/Users/comic/JenkinHome/。则检出的工程会放在:/Users/comic/JenkinsHome/workspace这个路径下。这个时候,配置子目录,会将对应的工程放在子目录下。如:client、common、client-android
这里不需要勾选使用自定义工作空间那一项。
这里,非渠道包只需要配置client和common。海外和国内渠道需要配置client-android。
注意:构建环境标签页这里,不要勾选Delete workspace before build starts这一项。如果勾选,每次都会重新检出一遍项目整个代码。

第三部分 Jenkins执行的脚本

选择执行shell那一项,在输入框中输入需要执行的shell脚本。
这里需要在unity的C#脚本里写Jenkins的入口方法。Jenkins需要向unity传参数。以渠道包为例:

        //jenkins打审核包
        public static void ExecutePackagingMethod()
        {
            string[] args = Environment.GetCommandLineArgs();
            CommandLineBuildAndroidPackage(int.Parse("2"), args[6], args[7],args[8], bool.Parse(args[9]), bool.Parse(args[10]));
        }
 //jenkins打包方法
        public static void CommandLineBuildAndroidPackage(int packageType, string channelName, string serverName, string hotUpdateServerName, bool executeBuildAndroidResource, bool executeBuildAssetsBundle)
        {
            OptionData data = new OptionData();
            if(channelName != null)
            {
                data.channelData = ParseConfigData.Instance.GetChannelDataByName(channelName);
            }
            if(hotUpdateServerName != null)
            {
                data.updateData = ParseConfigData.Instance.GetHotupdateDataByName(hotUpdateServerName);
            }
            data.serverData = ParseConfigData.Instance.GetServerDataByName(serverName);
            data.executeBuildAndroidResource = executeBuildAndroidResource;
            data.executeBuildAssetsBundle = executeBuildAssetsBundle;
            if (packageType == 0)
            {
                data.packageType = packageType;
                data.useSettingsFromCS = false;
                data.sdkEnabled = false;
                data.updateModeEnabled = false;
                data.isDeveloperMode = true;
                data.isDevloginMode = true;
                data.isNoLog = false;
            }
            else if (packageType == 2)
            { 
                data.packageType = packageType;
                data.useSettingsFromCS = true;
                data.sdkEnabled = true;
                data.updateModeEnabled = true;
                data.isDeveloperMode = false;
                data.isDevloginMode = false;
                data.isNoLog = true;
            }  
            if (channelName.Equals("Google"))
            {
                data.hasObbFile = true;
            }
            else
            {
                data.hasObbFile = false; 
            }
            GenerateFile.WriteDataToCSFile(data);
            EditorPrefs.SetString("CurrentServerName", data.serverData.ServerBrench);
            EditorPrefs.SetInt("CurrentPackageType", data.packageType);
            EditorPrefs.SetString("CurrentChannelId", data.channelData.ChannelId);
            SetPlayerSettings(data);
            StartBuildForAndroid(data);
        }

Jenkins调用unity的方法时需要通过Environment.GetCommandLineArgs这个方法,得到Jenkins命令行出入的参数。注意这里传入的参数是字符串类型。调用其它方法需要转换类型。将Jenkins传入的参数,拼成一个data,传入打包方法,执行打包操作。

用到的shell命令:
1、执行生成配置和协议:
sh /Users/comic/JenkinsHome/workspace/TTCollege/client/configs_and_protocols_builder/gen.sh
2、Jenkins调用unity的方法:

/Applications/Unity2017.4.35/Unity.app/Contents/MacOS/Unity -batchmode -logFile /Users/comic/JenkinsHome/workspace/logger/log.txt -executeMethod autopackaging.BuildAndroidProject.ExecutePackagingMethod "taptap" "$serverName" "$hotUpdateServerName" "$executeBuildAssetsBundle" "$executeBuildAndroidResource" -nographics -projectPath /Users/comic/JenkinsHome/workspace/TTCollege/client/mainProject  

其中,-batchmode : 以批处理模式运行unity
-logFile : 指定unity输出日志的文件
-executeMethod:Jenkins调用的方法。注意:方法要是静态的,且类必须位于Editor文件夹下。如果有命名空间,需要加上命名空间。否则会找不到方法。方法如果带参数,需要取得Jenkins配置的动态参数的值。可以通过"$serverName",这种方式来取值。相当于读取环境变量的值。而在unity的C#脚本里,就可以通过GetCommandLine获取到这些参数。但注意参数对应的下标。获取到的args是包含了整个命令行里所有的命令参数,不只是传给unity的参数。
-nographics:一般配合-batchmode参数一起使用。命令行执行的时候,不会出现unity的窗口。
-projectPath: 设置项目工程所在的路径。
执行完这个操作之后,需要关闭unity。否则unity一直处于运行状态,Jenkins会一直在这里,不会执行后续操作。

if (UnityEditorInternal.InternalEditorUtility.inBatchMode)
{
    EditorApplication.Exit(0);
}

非渠道包流程到这里。后面操作只有渠道包需要。

3、执行删除和复制文件操作:

  if [ ! -f "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main/assets" ]; then
  	rm -rf "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main/assets"
  fi
  if [ ! -f "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main/jniLibs" ]; then
  	rm -rf "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main/jniLibs"
  fi
  if [ ! -f "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main/res" ]; then
  	rm -rf "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main/res"
  fi
  if [ ! -f "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/platform/libs/unity-classes.jar" ]; then
  	rm -rf "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/platform/libs/unity-classes.jar"
  fi

判断文件是否存在,存在就删除。删除用的是rm -rf命令。
需要注意:写shell脚本的时候,if后面的[] , 两边都要有空格。最后,要有; 不然执行会报错。

 cp -R "/Users/comic/JenkinsHome/workspace/TTCollege/taptap/Demon/src/main/assets" "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main"
  cp -R "/Users/comic/JenkinsHome/workspace/TTCollege/taptap/Demon/src/main/jniLibs" "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main"
  cp -R "/Users/comic/JenkinsHome/workspace/TTCollege/taptap/Demon/src/main/res" "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/gameresource/src/main"
  cp -R "/Users/comic/JenkinsHome/workspace/TTCollege/taptap/Demon/libs/unity-classes.jar" "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/platform/libs"

复制的命令:cp -R 源路径 目标路径 路径是否加引号都可以,但如果有“-”的,不加""会识别成其它。
*注意Mac的shell和Linux的shell有所不同。

第四部分 Android命令行打包

1、jenkins上配置jdk: 主页系统管理-> 全局工具配置 -> 添加JDK 配置相应的JAVA_HOME路径。jdk的安装路径。
2、jenkins上配置gradle:主页系统管理-> 全局工具配置 -> 添加gradle 配置相应的GRADLE_HOME路径。gradle所在路径。注意:这里要配置到bin文件所在那一层。例如:/Users/comic/.gradle/wrapper/dists/gradle-5.4.1-all/3221gyojl5jsh0helicew7rwx/gradle-5.4.1。
3、jenkins上配置AndroidSDK:主页系统管理 -> 系统配置 -> 全局属性 -> 环境变量 这里设置Android的环境变量。填写SDK所在路径。
4、写脚本
主界面 -> 配置 -> 添加InvokeGradleScript -> InvokeGradle 选择对应的gradle版本。
Tasks一项,写命令。Android打包命令:
clean assemble${productFlavors}${buildTypes} --stacktrace --debug
productFlavors是之前配置的渠道;buildTypes是出包的类型。
5、对Android包进行签名:
V1签名:使用jarsigner

cd /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/bin/
jarsigner -verbose -keystore /Users/comic/JenkinsHome/workspace/TTCollege/client/mainProject/Assets/rcstudiotest.keystore -storepass 123456 -signedjar "/Users/comic/JenkinsHome/workspace/TTCollege/taptap/demon-taptap-release.apk" "/Users/comic/JenkinsHome/workspace/TTCollege/client_android/trunk/Voez_Android_20190531_yuyou_002/app/build/outputs/apk/ly/release/app-ly-release-unsigned.apk" "rcstudiotest" 

V1和V2签名: 使用apksigner

/Users/comic/Library/Android/sdk/build-tools/28.0.3/apksigner sign --ks /Users/comic/JenkinsHome/workspace/TTCollege_Foreign/client/mainProject/Assets/rcstudiotest.keystore --ks-pass pass:123456 --ks-key-alias rcstudiotest --out "/Users/comic/JenkinsHome/workspace/TTCollege_Foreign/google/demon-google-release.apk" "/Users/comic/JenkinsHome/workspace/TTCollege_Foreign/client_android_foreign/trunk/Voez_Android_20190531_yuyou_002/app/build/outputs/apk/google/release/app-google-release-unsigned.apk"

注意:google包出包时,需要同时选V1和V2。命令行输入 --ks-pass 输入签名文件密码时,要输入pass:,再输入密码,不然会报错。还有,最后两个路径不要写反,前面是签名之后的包所在路径。后面是未签名的包所在路径。
另外,如果使用记事本编辑写shell脚本。要修改编码格式为utf-8。不然会出现乱码。

⚠️ **GitHub.com Fallback** ⚠️