avatar

Unity+Jenkins实现自动化打包

引言

两年 两年 你知道这两年我是怎么过的吗?天天打包测试,你知道有多好玩吗!哈哈哈,开个玩笑。作为一名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插件推荐

简要介绍一下我使用到的插件:

  1. Localization:Chinese(Simplified) 简体中文语言包,可以汉化部分说明
  2. Unity3d plugin 增强Unity编辑器打包拓展,默认Unity项目在工作区,可以在视图显示Log
  3. Subversion SVN插件,可以在任务流水线中添加SVN模块,执行项目导出、还原、更新等操作

注意:不要在新手入门时安装任何插件,很容易下载失败,进去后会有错误,建议登录后在Manage jenkin/Manager Plugins中管理插件

五、修改Jenkins插件配置

  1. Unity3d plugin。 需要在Jenkins的Global Tool Configuration设置中添加Unity安装目录
    GiT0xO.png
  2. Subversion。 需要在Jenkins凭证设置中添加SVN用户名、密码
    GiTDMD.png

    六、Unity编辑器拓展打包

    项目设置代码

  • 通用设置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    PlayerSettings.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
    16
    BuildPlayerOptions 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
    20
    BuildPlayerOptions 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工程打包完成时,使用代码进行设置
    [PostProcessBuild(100)]
    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
    14
    public 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.ipa
  • Windows平台:导出为.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
    44
    BuildPlayerOptions 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构建任务

  1. 新建Item,设置General,可以添加描述,选择是否丢弃旧的构建,添加自定义参数等等
    GiTsqH.png
  2. 配置源代码管理,SVN项目地址,连接凭证,我选择的是先还原再更新的方式
    GiT6Zd.png
  3. 构建触发器,当设置的条件满足时会自动进行构建
    GiTdG6.png
  4. 构建,可以选择你想用的方式进行构建,可使用bat命令、Shell命令、Unity编辑器命令等方式进行构建,编写命令时我们可以将General里面自定义的参数的和Jenkins提供给我们的参数添加到命令里面进行使用
    GiTrse.png
    GiTwRK.png
  5. 构建后操作。构建完成后,我们可以归档生成的文件,如果有需要也可以在这里添加邮件通知
    GiTNI1.png
文章作者: tiger
文章链接: https://chenghu.online/posts/a27a69b1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 tiger
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论