使用jenkins打包unity工程

Apache配置
  • 安装:arch arm64 brew install httpd
  • 开启:brew services start httpd
  • 重启:brew services restart httpd
  • 停止:brew services stop httpd
  • 配置文件路径:/opt/homebrew/etc/httpd/httpd.conf,默认监听8080端口,更改端口号后,要重启一下brew services restart httpd
  • SSL文件路径 /opt/homebrew/etc/httpd/extra/httpd-ssl.conf,默认监听 8443,Apache 默认使用 443 端口来处理 HTTPS 请求。由于 1024 以下的端口通常需要超级用户权限才能绑定,因此如果您希望 Apache 在没有 `sudo` 权限的情况下运行,您需要将其配置为使用高于 1024 的端口(如 8443)
  • 服务器文件存放目录 /opt/homebrew/var/www,把文件放到这个文件夹下,别人可以通过访问ip+port+相对地址访问
Jenkins配置
Groovy 复制代码
Jenkins.instance.getItemByFullName(jobName).builds.findAll {
  it.number <= maxNumber
}.each {
  it.delete()
}
  • Jenkins的参数一切皆字符串,bool类型参数也是字符串,通过when{expression{return BoolParam.toBoolean()}}判断
  • Unity 访问命令行参数:
cs 复制代码
string[] args = System.Environment.GetCommandLineArgs();    
//每个空格都是一个参数 比如 -project testPath customParam1:111 customParam2:222
static string GetSingleCommandlineArgs(string[] args, string key)
{
        string value = String.Empty;
        foreach (var arg in args)
        {
            Debug.Log("命令行参数:args:" + arg);
            if (arg.Contains(key))
            {
                value = arg;
                break;
            }
        }

        return value;
}
  • Unity iOS后处理方法,包括更改Build Phase的顺序:
cs 复制代码
#if UNITY_IOS

using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using System.IO;
using System;
using System.Collections.Generic;
using System.Reflection;
using AMPSDK.Utils;
using UnityEditor.iOS.Xcode;
using UnityEditor.iOS.Xcode.Extensions;


namespace EditorBuildTool
{
    public static class AviaXCodeSetter
    {
        private const string BtApplePayMerchantId = "";
        private const string UtApplePayMerchantId = "";
        private const string TeamId = "";
        private const string AppGroup = "";
        private const string DeepLinkDomain = "";
        private const string CrashlyticsShellScriptPhaseName = "Crashlytics Run Script";
        private const string EmbedAppExtensionsPhaseName = "Embed App Extensions";

        public static bool IsUploadIpa = false;


        private static string TrustFrameworkPath => Path.Combine(Application.dataPath, "../Archive/iOS/Trustly");

        private static string GoogleInfoPlistPath =>
            Path.Combine(Application.dataPath, "../Archive/iOS/Test/GoogleService-Info.plist");

        [PostProcessBuild(Int32.MaxValue - 1)]
        public static void OnPostProcessBuild(BuildTarget target, string path2BuildProject)
        {
            if (target != BuildTarget.iOS)
                return;

            Debug.LogWarning(path2BuildProject);
            ModifyProjectSettings(path2BuildProject);
            ModifyPodProjectSettings(path2BuildProject);
            Process(path2BuildProject);
        }

        static void ModifyProjectSettings(string path2BuildProject)
        {
            string projPath = path2BuildProject + "/Unity-iPhone.xcodeproj/project.pbxproj";
            PBXProject project = new PBXProject();
            project.ReadFromFile(projPath);

            AddNotificationTargets(project, path2BuildProject); //临时屏蔽

            SubModifyProjectSettings(path2BuildProject, project);

            File.WriteAllText(projPath, project.WriteToString());
        }

        static void AddNotificationTargets(PBXProject pbxProject, string path2BuildProject)
        {
            string notificationTestPath =
                Path.Combine(Application.dataPath, "../Archive/iOS/NotificationTest/NotificationService");

            Directory.CreateDirectory(Path.Combine(path2BuildProject, "NotificationService"));

            CopyFileToPath(path2BuildProject, $"{notificationTestPath}/Info.plist",
                "NotificationService/Info.plist", pbxProject, true);

            CopyFileToPath(path2BuildProject, $"{notificationTestPath}/NotificationService.h",
                "NotificationService/NotificationService.h", pbxProject, true);

            string mPath = CopyFileToPath(path2BuildProject, $"{notificationTestPath}/NotificationService.m",
                "NotificationService/NotificationService.m", pbxProject, true);


            string guid = pbxProject.AddAppExtension(pbxProject.GetUnityMainTargetGuid(), "NotificationService",
                $"{Application.identifier}.NotificationServices", "NotificationService/Info.plist");

            pbxProject.AddFileToBuild(guid, mPath);

            #region 子模块的build setting

            pbxProject.SetBuildProperty(guid, "CODE_SIGN_IDENTITY", "Apple Development");
            pbxProject.SetBuildProperty(guid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");
            pbxProject.SetBuildProperty(guid, "ARCHS", "arm64");
            pbxProject.SetBuildProperty(guid, "GENERATE_INFOPLIST_FILE", "YES");
            pbxProject.SetBuildProperty(guid, "CURRENT_PROJECT_VERSION", "0");
            pbxProject.SetBuildProperty(guid, "MARKETING_VERSION", "1.2");
            pbxProject.SetBuildProperty(guid, "GCC_ENABLE_OBJC_EXCEPTIONS", "YES");
            pbxProject.SetTeamId(guid, TeamId);
            pbxProject.AddFrameworkToProject(guid, "UserNotifications.framework", false);

            CopyFileToPath(path2BuildProject, $"{notificationTestPath}/NotificationService.entitlements",
                "NotificationService/NotificationService.entitlements", pbxProject, true);
            pbxProject.AddBuildPropertyForConfig(pbxProject.BuildConfigByName(guid, "Debug"),
                "CODE_SIGN_ENTITLEMENTS", "NotificationService/NotificationService.entitlements");
            pbxProject.AddBuildPropertyForConfig(pbxProject.BuildConfigByName(guid, "Release"),
                "CODE_SIGN_ENTITLEMENTS", "NotificationService/NotificationService.entitlements");
            pbxProject.AddBuildPropertyForConfig(pbxProject.BuildConfigByName(guid, "ReleaseForRunning"),
                "CODE_SIGN_ENTITLEMENTS", "NotificationService/NotificationService.entitlements");
            pbxProject.AddBuildPropertyForConfig(pbxProject.BuildConfigByName(guid, "ReleaseForProfiling"),
                "CODE_SIGN_ENTITLEMENTS", "NotificationService/NotificationService.entitlements");
            
            if (IsUploadIpa)
            {
                pbxProject.SetBuildProperty(guid, "CODE_SIGN_STYLE", "Manual");
                pbxProject.SetBuildProperty(guid, "CODE_SIGN_IDENTITY[sdk=iphoneos*]", "钥匙串中证书的名字");
                pbxProject.SetBuildProperty(guid, "CODE_SIGN_IDENTITY", "钥匙串中证书的名字");

                pbxProject.SetBuildProperty(guid, "PROVISIONING_PROFILE_SPECIFIER", "Unity SDK Demo Provision Notification");
            }
            else
            {
                pbxProject.SetBuildProperty(guid, "CODE_SIGN_STYLE", "Automatic");
            }

            #endregion


            #region 子模块的entitlement

            string relativeEntitlementFilePath = "NotificationService/NotificationService.entitlements";
            string absoluteEntitlementFilePath = path2BuildProject + "/" + relativeEntitlementFilePath;

            PlistDocument notifyEntitlement = new PlistDocument();
            if (!string.IsNullOrEmpty(AppGroup))
            {
                pbxProject.AddCapability(guid, PBXCapabilityType.AppGroups);
                string appGroupPlist = "com.apple.security.application-groups";
                var appGroupArray = new PlistElementArray();
                appGroupArray.AddString(AppGroup);
                notifyEntitlement.root[appGroupPlist] = appGroupArray;
            }
            else
            {
                AviaLogger.AMPLogWarn("app group 为空");
            }

            notifyEntitlement.WriteToFile(absoluteEntitlementFilePath);
            ModifyEntitlementFile(absoluteEntitlementFilePath);

            #endregion


            #region 子模块的info.plist

            string plistPath = $"{path2BuildProject}/NotificationService/Info.plist";
            PlistDocument plist = new PlistDocument();
            plist.ReadFromString(File.ReadAllText(plistPath));
            PlistElementDict infoDict = plist.root;
            PlistElementDict bmDict;
            if (!infoDict.values.ContainsKey("NSAppTransportSecurity"))
                bmDict = infoDict.CreateDict("NSAppTransportSecurity");
            else
                bmDict = infoDict.values["NSAppTransportSecurity"].AsDict();
            bmDict.SetBoolean("NSAllowsArbitraryLoads", true);

            infoDict.SetString("CFBundleDisplayName", "AviaNotificationServiceExtension");
            infoDict.SetString("CFBundleVersion", PlayerSettings.iOS.buildNumber);
            File.WriteAllText(plistPath, plist.WriteToString());

            #endregion
        }


        private static void ModifyPodProjectSettings(string path2BuildProject)
        {
            string projPath = path2BuildProject + "/Pods/Pods.xcodeproj/project.pbxproj";
            PBXProject project = new PBXProject();
            project.ReadFromFile(projPath);
            SubModifyPodProjectSettings(path2BuildProject, project);

            File.WriteAllText(projPath, project.WriteToString());
        }


        private static string CopyFileToPath(string path2BuildProject, string fileAbsolutePath, string fileReactivePath,
            PBXProject project = null, bool addToProject = true)
        {
            string newPath = Path.Combine(path2BuildProject, fileReactivePath);

            if (File.Exists(fileAbsolutePath))
            {
                if (File.Exists(newPath))
                {
                    File.Delete(newPath);
                }

                File.Copy(fileAbsolutePath, newPath);
                if (addToProject)
                {
                    return project?.AddFile(newPath, fileReactivePath, PBXSourceTree.Source);
                }
            }
            else
            {
                Debug.LogWarning("文件不存在:" + fileAbsolutePath);
            }

            return "";
        }


        private static string CopyDirectoryToPath(string path2BuildProject, string fileAbsolutePath,
            string fileReactivePath,
            PBXProject project = null, bool addToProject = true)
        {
            string newPath = Path.Combine(path2BuildProject, fileReactivePath);

            if (Directory.Exists(fileAbsolutePath))
            {
                if (Directory.Exists(newPath))
                {
                    Directory.Delete(newPath);
                }

                FileUtil.CopyFileOrDirectory(fileAbsolutePath, newPath);
                if (addToProject)
                {
                    return project?.AddFile(newPath, fileReactivePath, PBXSourceTree.Source);
                }
            }
            else
            {
                Debug.LogWarning("文件夹不存在:" + fileAbsolutePath);
            }

            return "";
        }

        private static void SubModifyPodProjectSettings(string path2BuildProject, PBXProject project)
        {
            string brainTreeDropInGuid = project.TargetGuidByName("BraintreeDropIn-BraintreeDropIn-Localization");
            project.SetTeamId(brainTreeDropInGuid, TeamId);

            string checkoutFrameGuid = project.TargetGuidByName("Frames-Frames");
            if (!string.IsNullOrEmpty(checkoutFrameGuid))
            {
                project.SetTeamId(checkoutFrameGuid, TeamId);
            }

            string adyenFrameGuid = project.TargetGuidByName("Adyen-Adyen");
            if (!string.IsNullOrEmpty(adyenFrameGuid))
            {
                project.SetTeamId(adyenFrameGuid, TeamId);
                string adyenActionFrameGuid = project.TargetGuidByName("Adyen-AdyenActions");
                project.SetTeamId(adyenActionFrameGuid, TeamId);
                string adyenCardFrameGuid = project.TargetGuidByName("Adyen-AdyenCard");
                project.SetTeamId(adyenCardFrameGuid, TeamId);
            }


            string awsCoreGuid = project.TargetGuidByName("AWSCore");
            if (!string.IsNullOrEmpty(awsCoreGuid))
                project.SetBuildProperty(awsCoreGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");

            string awsS3Guid = project.TargetGuidByName("AWSS3");
            if (!string.IsNullOrEmpty(awsS3Guid))
                project.SetBuildProperty(awsS3Guid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");

            string masonryGuid = project.TargetGuidByName("Masonry");
            if (!string.IsNullOrEmpty(masonryGuid))
                project.SetBuildProperty(masonryGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");

            string openUdidGuid = project.TargetGuidByName("OpenUDID");
            if (!string.IsNullOrEmpty(openUdidGuid))
                project.SetBuildProperty(openUdidGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");

            string reachabilityGuid = project.TargetGuidByName("Reachability");
            if (!string.IsNullOrEmpty(reachabilityGuid))
                project.SetBuildProperty(reachabilityGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");

            string uicKeyChainStoreGuid = project.TargetGuidByName("UICKeyChainStore");
            if (!string.IsNullOrEmpty(uicKeyChainStoreGuid))
                project.SetBuildProperty(uicKeyChainStoreGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");
        }

        private static void SubModifyProjectSettings(string path2BuildProject, PBXProject project)
        {
            string unityFrameworkTargetGuid = project.GetUnityFrameworkTargetGuid();
            string unityMainTargetGuid = project.GetUnityMainTargetGuid();
            string unityProjectGuid = project.ProjectGuid();
            string iosFolderPath = Path.Combine(Application.dataPath, $"../Archive/iOS/Test");
            if (!Directory.Exists(iosFolderPath))
            {
                Debug.LogError($"haven't found folder:{iosFolderPath}");
                return;
            }

            //var info = new DirectoryInfo(iosFolderPath);
            //iosFolderPath = info.FullName;
            // 拷贝Podfile文件到工程
            //CopyToPath(path2BuildProject, $"{iosFolderPath}/Podfile", "Podfile");
            // 签名信息(可以没有,打包机上ExportOptions里有配置,但不用打包机的情况下,还是可以加一下的)ps:加了也没关系,还是打包机优先级最高

            if (!string.IsNullOrEmpty(TeamId))
                project.SetTeamId(unityMainTargetGuid, TeamId);
            else
                AviaLogger.AMPLogWarn("team id 为空");

            CopyFileToPath(path2BuildProject, GoogleInfoPlistPath, "GoogleService-Info.plist", project, true);

            #region 添加framework

            project.AddFrameworkToProject(unityFrameworkTargetGuid, "AdSupport.framework", false);
            project.AddFrameworkToProject(unityFrameworkTargetGuid, "AppTrackingTransparency.framework", false);
            project.AddFrameworkToProject(unityFrameworkTargetGuid, "Photos.framework", false);
            project.AddFrameworkToProject(unityFrameworkTargetGuid, "UserNotifications.framework", false);

            // 添加非系统框架,及文件
            string payFrameworkGuid = CopyDirectoryToPath(path2BuildProject,
                $"{TrustFrameworkPath}/PayWithMyBank.xcframework",
                "Frameworks/PayWithMyBank.xcframework", project, true);

            // if (!string.IsNullOrEmpty(payFrameworkGuid))
            // {
            //     project.AddFileToEmbedFrameworks(unityIPhoneGUID, payFrameworkGuid);
            //     project.AddFileToEmbedFrameworks(unityFrameworkGUID, payFrameworkGuid);
            // }
            // else
            // {
            //     Debug.LogWarning("trust frame work 不存在");
            // }

            #endregion

            #region 调整BuildSettings

            project.SetBuildProperty(unityMainTargetGuid, "CODE_SIGN_IDENTITY", "Apple Development");
            project.SetBuildProperty(unityMainTargetGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");
            project.SetBuildProperty(unityFrameworkTargetGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");
            project.SetBuildProperty(unityProjectGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");
            project.SetBuildProperty(unityMainTargetGuid, "GCC_C_LANGUAGE_STANDARD", "c99");
            project.SetBuildProperty(unityFrameworkTargetGuid, "GCC_C_LANGUAGE_STANDARD", "c99");
            project.SetBuildProperty(unityProjectGuid, "GCC_C_LANGUAGE_STANDARD", "c99");
            project.SetBuildProperty(unityFrameworkTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "NO");


            Debug.Log("----------build ipa--------------"+IsUploadIpa);
            if (IsUploadIpa)
            {
                project.SetBuildProperty(unityProjectGuid, "CODE_SIGN_STYLE", "Manual");
                project.SetBuildProperty(unityMainTargetGuid, "CODE_SIGN_STYLE", "Manual");
                project.SetBuildProperty(unityMainTargetGuid, "CODE_SIGN_IDENTITY[sdk=iphoneos*]", "钥匙串中证书的名字");
                project.SetBuildProperty(unityMainTargetGuid, "CODE_SIGN_IDENTITY", "钥匙串中证书的名字");
                project.SetBuildProperty(unityMainTargetGuid, "PROVISIONING_PROFILE_SPECIFIER", "provision 文件的名字,不带后缀");
            }
            else
            {
                project.SetBuildProperty(unityMainTargetGuid, "CODE_SIGN_STYLE", "Automatic");
            }
            
            var token = project.GetBuildPropertyForAnyConfig(unityProjectGuid, "USYM_UPLOAD_AUTH_TOKEN");
            if (string.IsNullOrEmpty(token))
            {
                token = "FakeToken";
            }

            project.SetBuildProperty(unityMainTargetGuid, "USYM_UPLOAD_AUTH_TOKEN", token);
            project.SetBuildProperty(unityProjectGuid, "USYM_UPLOAD_AUTH_TOKEN", token);
            project.SetBuildProperty(unityFrameworkTargetGuid, "USYM_UPLOAD_AUTH_TOKEN", token);

            project.SetBuildProperty(unityMainTargetGuid, "GCC_ENABLE_OBJC_EXCEPTIONS", "YES");
            project.SetBuildProperty(unityFrameworkTargetGuid, "GCC_ENABLE_OBJC_EXCEPTIONS", "YES");

            project.SetBuildProperty(unityMainTargetGuid, "ENABLE_BITCODE", "FALSE");
            project.SetBuildProperty(unityFrameworkTargetGuid, "ENABLE_BITCODE", "FALSE");

            project.AddBuildProperty(unityProjectGuid, "OTHER_LDFLAGS", "-ObjC -ld_classic");

            project.SetBuildProperty(unityMainTargetGuid, "CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES",
                "YES");
            project.SetBuildProperty(unityFrameworkTargetGuid, "CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES",
                "YES");

            // entitlements : Apple Pay Keychain ...
            CopyFileToPath(path2BuildProject, $"{iosFolderPath}/Unity-iPhoneDebug.entitlements",
                "Unity-iPhoneDebug.entitlements", project, true);
            project.AddBuildPropertyForConfig(project.BuildConfigByName(unityMainTargetGuid, "Debug"),
                "CODE_SIGN_ENTITLEMENTS", "Unity-iPhoneDebug.entitlements");
            CopyFileToPath(path2BuildProject, $"{iosFolderPath}/Unity-iPhoneRelease.entitlements",
                "Unity-iPhoneRelease.entitlements", project, true);
            project.AddBuildPropertyForConfig(project.BuildConfigByName(unityMainTargetGuid, "Release"),
                "CODE_SIGN_ENTITLEMENTS", "Unity-iPhoneRelease.entitlements");
            project.AddBuildPropertyForConfig(project.BuildConfigByName(unityMainTargetGuid, "ReleaseForProfiling"),
                "CODE_SIGN_ENTITLEMENTS", "Unity-iPhoneRelease.entitlements");
            project.AddBuildPropertyForConfig(project.BuildConfigByName(unityMainTargetGuid, "ReleaseForRunning"),
                "CODE_SIGN_ENTITLEMENTS", "Unity-iPhoneRelease.entitlements");

            #endregion

            // Info.plist
            AddCapability(project, path2BuildProject);
        }

        private static void AddCapability(PBXProject project, string pathToBuiltProject)
        {
            string target = project.GetUnityMainTargetGuid();
            // Need Create entitlements

            #region 修改info.plist

            string plistPath = pathToBuiltProject + "/Info.plist";
            PlistDocument plist = new PlistDocument();
            plist.ReadFromString(File.ReadAllText(plistPath));
            PlistElementDict infoDict = plist.root;

            project.AddCapability(target, PBXCapabilityType.BackgroundModes);
            PlistElementArray bmArray;

            if (!infoDict.values.ContainsKey("UIBackgroundModes"))
                bmArray = infoDict.CreateArray("UIBackgroundModes");
            else
                bmArray = infoDict.values["UIBackgroundModes"].AsArray();
            bmArray.values.Clear();
            bmArray.AddString("remote-notification");

            PlistElementDict bmDict;
            if (!infoDict.values.ContainsKey("NSAppTransportSecurity"))
                bmDict = infoDict.CreateDict("NSAppTransportSecurity");
            else
                bmDict = infoDict.values["NSAppTransportSecurity"].AsDict();

            if (bmDict.values.ContainsKey("NSAllowsArbitraryLoadsInWebContent"))
                bmDict.values.Remove("NSAllowsArbitraryLoadsInWebContent");


            if (AviaLogger.IsOpenLog)
            {
                if (infoDict.values.ContainsKey("UIFileSharingEnabled"))
                    infoDict.values.Remove("UIFileSharingEnabled");
                infoDict.SetBoolean("UIFileSharingEnabled", true);
            }

            infoDict.SetString("NSUserTrackingUsageDescription", "广告追踪权限");
            infoDict.SetString("NSLocationWhenInUseUsageDescription", "地理位置权限");
            infoDict.SetString("NSPhotoLibraryUsageDescription", "相册权限");
            infoDict.SetString("NSCameraUsageDescription", "相机权限");
            infoDict.SetBoolean("ITSAppUsesNonExemptEncryption", false);

            infoDict.CreateDict("NSLocationTemporaryUsageDescriptionDictionary")
                .SetString("GetPreciseLocation", "此应用程序需要临时访问您的位置信息以提供更准确的服务。");

            if (!infoDict.values.ContainsKey("LSApplicationQueriesSchemes"))
                infoDict.CreateArray("LSApplicationQueriesSchemes").AddString("com.venmo.touch.v2");
            else
                infoDict.values["LSApplicationQueriesSchemes"].AsArray().AddString("com.venmo.touch.v2");


            if (!infoDict.values.ContainsKey("CFBundleURLTypes"))
                infoDict.CreateArray("CFBundleURLTypes").AddDict().CreateArray("CFBundleURLSchemes");

            else
                infoDict.values["CFBundleURLTypes"].AsArray().AddDict().CreateArray("CFBundleURLSchemes");

            PlistElementArray urlSchemes =
                infoDict["CFBundleURLTypes"].AsArray().values[1].AsDict()["CFBundleURLSchemes"].AsArray();
            urlSchemes.AddString("${PRODUCT_BUNDLE_IDENTIFIER}.payments");
            urlSchemes.AddString("${PRODUCT_BUNDLE_IDENTIFIER}.trustly");
            urlSchemes.AddString("${PRODUCT_BUNDLE_IDENTIFIER}.adyenCashApp");
            urlSchemes.AddString("${PRODUCT_BUNDLE_IDENTIFIER}.afLink");

            infoDict.SetString("AppIdentifierPrefix", "$(AppIdentifierPrefix)");
            //infoDict.SetString("CFBundleIdentifier", _bundleId);
            File.WriteAllText(plistPath, plist.WriteToString());

            #endregion


            #region 修改entitlement

            string releaseEntitlementFilePath = pathToBuiltProject + "/Unity-iPhoneRelease.entitlements";
            string absoluteEntitlementFilePath = pathToBuiltProject + "/Unity-iPhoneDebug.entitlements";
            PlistDocument tempEntitlements = new PlistDocument();

            string keychainAccessGroups = "keychain-access-groups";
            var arr = new PlistElementArray();
            arr.values.Add(new PlistElementString($"$(AppIdentifierPrefix){Application.identifier}"));
            arr.values.Add(new PlistElementString("$(AppIdentifierPrefix)AviagamesUniqueDevice"));
            tempEntitlements.root[keychainAccessGroups] = arr;

            project.AddCapability(target, PBXCapabilityType.InAppPurchase);
            if (!string.IsNullOrEmpty(BtApplePayMerchantId))
            {
                string applePayments = "com.apple.developer.in-app-payments";
                var payArr = (tempEntitlements.root[applePayments] = new PlistElementArray()) as PlistElementArray;
                payArr?.values.Add(new PlistElementString(BtApplePayMerchantId));
                payArr?.values.Add(new PlistElementString(UtApplePayMerchantId));
            }
            else
            {
                AviaLogger.AMPLogWarn("merchant Id 为空");
            }

            string keyPushNotifications = "aps-environment";
            tempEntitlements.root[keyPushNotifications] = new PlistElementString("development");

            project.AddCapability(target, PBXCapabilityType.PushNotifications);
            project.AddCapability(target, PBXCapabilityType.KeychainSharing);

            project.AddCapability(target, PBXCapabilityType.AccessWiFiInformation);
            tempEntitlements.root["com.apple.developer.networking.wifi-info"] = new PlistElementBoolean(true);


            if (!string.IsNullOrEmpty(DeepLinkDomain))
            {
                project.AddCapability(target, PBXCapabilityType.AssociatedDomains);
                var associateDomains = new PlistElementArray();
                associateDomains.AddString(DeepLinkDomain);
                tempEntitlements.root["com.apple.developer.associated-domains"] = associateDomains;
            }
            else
            {
                AviaLogger.AMPLogWarn("deep link 为空");
            }

            if (!string.IsNullOrEmpty(AppGroup))
            {
                project.AddCapability(target, PBXCapabilityType.AppGroups);
                string appGroupKey = "com.apple.security.application-groups";
                var appGroupArr = new PlistElementArray();
                appGroupArr.values.Add(new PlistElementString(AppGroup));
                tempEntitlements.root[appGroupKey] = appGroupArr;
            }
            else
            {
                AviaLogger.AMPLogWarn("app group 为空");
            }

            tempEntitlements.WriteToFile(absoluteEntitlementFilePath);
            tempEntitlements.root[keyPushNotifications] = new PlistElementString("production");
            tempEntitlements.WriteToFile(releaseEntitlementFilePath);

            ModifyEntitlementFile(absoluteEntitlementFilePath);

            #endregion
        }

        private static void ModifyEntitlementFile(string absoluteEntitlementFilePath)
        {
            if (!File.Exists(absoluteEntitlementFilePath)) return;

            try
            {
                StreamReader reader = new StreamReader(absoluteEntitlementFilePath);
                var content = reader.ReadToEnd().Trim();
                reader.Close();

                var needFindString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
                var changeString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "\n" +
                                   "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">";
                Debug.Log("entitlement更改之前: " + content);
                content = content.Replace(needFindString, changeString);
                Debug.Log("entitlement更改之后: " + content);
                StreamWriter writer = new StreamWriter(new FileStream(absoluteEntitlementFilePath, FileMode.Create));
                writer.WriteLine(content);
                writer.Flush();
                writer.Close();
            }
            catch (Exception e)
            {
                Debug.Log("ModifyEntitlementFile - Failed: " + e.Message);
            }
        }

        #region 修改build phase

        private static void Process(string path)
        {
            string projectPath = PBXProject.GetPBXProjectPath(path);
            PBXProject project = new PBXProject();
            project.ReadFromFile(projectPath);

            string mainTargetGuild = project.GetUnityMainTargetGuid();
            //获取所有的build phase guid
            string[] buildPhases = project.GetAllBuildPhasesForTarget(mainTargetGuild);

            //根据build phase name获取 guid
            string crashlyticsShellScriptPhaseGuid =
                GetBuildPhaseGuid(project, buildPhases, CrashlyticsShellScriptPhaseName);
            if (string.IsNullOrEmpty(crashlyticsShellScriptPhaseGuid))
            {
                DebugLog($"\"{CrashlyticsShellScriptPhaseName}\" phase guid not found.");
                //return;
            }

            string embedAppExtensionsPhaseGuid = GetBuildPhaseGuid(project, buildPhases, EmbedAppExtensionsPhaseName);
            if (string.IsNullOrEmpty(embedAppExtensionsPhaseGuid))
            {
                DebugLog($"\"{EmbedAppExtensionsPhaseName}\" phase guid not found.");
                //return;
            }


            Type projectType = project.GetType();
            //获取nativeTargets属性
            PropertyInfo nativeTargetsProperty =
                projectType.GetProperty("nativeTargets", BindingFlags.NonPublic | BindingFlags.Instance);

            //获取nativeTargets属性值,是个数组
            object nativeTargets = nativeTargetsProperty?.GetValue(project);
            if (nativeTargets == null)
            {
                DebugLog($"属性'nativeTargets'没找到");
                return;
            }

            DebugLog("nativeTargets 值为:" + nativeTargets);


            //获取nativeTargets的类
            Type nativeTargetsType = nativeTargets.GetType();
            DebugLog("nativeTargets 类名为:" + nativeTargetsType.FullName);

            //获取nativeTargets的类的索引器方法
            MethodInfo indexerMethod = nativeTargetsType.GetMethod("get_Item");
            if (indexerMethod == null)
            {
                DebugLog($"Method 'get_Item' of {nativeTargetsType.FullName} type not found.");
                return;
            }

            //调用nativeTargets的类的索引器方法,参数为target guid,返回为PBXNativeTargetData,也就是target的数据,比如Unity-iPhone、UnityFramework
            object pbxNativeTargetData = indexerMethod.Invoke(nativeTargets, new object[] { mainTargetGuild });

            //获取main target(PBXNativeTargetData)的phase 字段,它是GUIDList类型,也就是所有phases的所有GUID
            FieldInfo phasesField = pbxNativeTargetData.GetType().GetField("phases");
            object phases = phasesField?.GetValue(pbxNativeTargetData);

            //获取GUIDList的m_List private字段
            FieldInfo listField = phases?.GetType().GetField("m_List", BindingFlags.NonPublic | BindingFlags.Instance);
            if (!(listField?.GetValue(phases) is List<string> guidList))
            {
                DebugLog($"Field 'm_List' not found.");
                return;
            }

            //------下面开始调整顺序,前面只是做校验多一点-------
            //build phase在xcode中的顺序,就是它在GUIDList中的顺序
            guidList.Remove(crashlyticsShellScriptPhaseGuid);
            guidList.Insert(guidList.IndexOf(embedAppExtensionsPhaseGuid) + 1, crashlyticsShellScriptPhaseGuid);
            DebugLog(
                $"Insert {CrashlyticsShellScriptPhaseName} phase {crashlyticsShellScriptPhaseGuid} after {EmbedAppExtensionsPhaseName} phase {embedAppExtensionsPhaseGuid}");

            project.WriteToFile(projectPath);

            void DebugLog(string message) => Debug.Log($"[更改build phase 顺序] {message}");
        }

        /// <summary>
        /// 根据
        /// </summary>
        /// <param name="project"></param>
        /// <param name="buildPhases"></param>
        /// <param name="buildPhaseName"></param>
        /// <returns></returns>
        static string GetBuildPhaseGuid(PBXProject project, string[] buildPhases, string buildPhaseName)
        {
            foreach (string buildPhaseGuid in buildPhases)
            {
                if (project.GetBuildPhaseName(buildPhaseGuid) == buildPhaseName)
                {
                    return buildPhaseGuid;
                }
            }

            return null;
        }

        #endregion
    }
}
#endif
打包命令行
Groovy 复制代码
def PROJECT_PATH = "${GIT_PRO_PATH}/${UNITY_PROJECT_NAME}"                                                               
def BUILD_IOS_PATH = "${PROJECT_PATH}/BuildTool/buildios.sh"                                   
def XCODE_ARCHIVE_NAME = 'unity_sdk.xcarchive'
def XCODE_ARCHIVE_PROJECT_PATH = "${IPA_PATH}/../archive_proj/${XCODE_ARCHIVE_NAME}"
def XCODE_BUILD_PROJECT_PATH = "${IPA_PATH}/../xcode_proj"
def PACKAGE_SHARE_PATH = "/opt/homebrew/var/www/unity_sdk_output"
def UNITY_LOG_FILE = "${PROJECT_PATH}/Logs/unity_sdk_log.txt"
def ARCHIVE_LOG_FILE = "${PROJECT_PATH}/Logs/archive.txt"
def SHARE_URL = "http://10.240.0.216:8090/unity_sdk_output"
def BUNDLE_NUM_FILE="${PROJECT_PATH}/Archive/Setting/BuildVersion.user"
def FAIRGUARD = "${PROJECT_PATH}/BuildTool/FairGuard_iOS_3.3.4/fairguardbuild"
def user
node {
    //需要安装插件 build var user
    wrap([$class: 'BuildUser']) 
    {
        user = env.BUILD_USER_ID
    }
}
pipeline {
        agent any
        options {
            //lock(label: 'DevLock', quantity: 1)            
            disableConcurrentBuilds()
            timeout(time: 45, unit: 'MINUTES')
        }
        environment 
        {
            BUILD_TIMESTAMP = sh(script: 'echo $(date +"%Y_%m_%d-%H_%M_%S")', returnStdout: true).trim()
            TEST_VALUE = "${Test}" 
        }

        stages {

            stage('git拉取代码更新') {
                steps {
                    sh """
                        cd ${GIT_PRO_PATH}
                        git clean -fd
                        git stash push ${BUNDLE_NUM_FILE}
                        git reset --hard HEAD 
                        git stash pop
                        git checkout ${BRANCH}
                        git clean -fd
                        git pull --force 
                        git submodule foreach --recursive 'git reset HEAD . || :'
                        git submodule foreach --recursive 'git checkout -- . || :'
                        git submodule update --init --recursive
                        git submodule foreach --recursive git clean -d -f -f -x
                    """
                }
            }

            stage('生成共享地址') {
                steps{
                    sh """
                        rm -rf ${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}
                        mkdir ${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}
                        echo ${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}
                    """
                }
            }
            stage('Unity打包') {
                when{
                    expression{return IS_BUILD_IPA.toBoolean()}
                    //expression{return false}
                }
                steps {
                    sh """
                        set -e
                        ${UNITY_2020_3_33} -projectPath $PROJECT_PATH -executeMethod BuildTools.BuildIOS  xcodeProjectPath:${XCODE_BUILD_PROJECT_PATH} env:${ENV} isUploadIpa:${IS_UPLOAD_IPA}  -quit -batchmode -logFile ${UNITY_LOG_FILE} 2>&1 | tee ${UNITY_LOG_FILE} -buildTarget iOS
                        cp ${UNITY_LOG_FILE} ${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}/unity_sdk_log.txt
                    """
                }
            }
           
            stage('Xcode工程 Clean') {
                when{
                  expression{return IS_BUILD_IPA.toBoolean()}
                }
                steps {
                    sh """
                        cd ${XCODE_BUILD_PROJECT_PATH}
                        xcodebuild -workspace Unity-iPhone.xcworkspace -scheme "Unity-iPhone" clean
                    """                    
                }
            }

            stage('XCODE加固') {
                when{
                  expression{return false}
                }
                steps {
                    script {
                                            if("${jiagu}" == 'true'){
                                                        XCODE_BUILD_PATH = "${XCODE_DIR_PATH}/BuildSuccess"
                                                    sh """ 
                                                            ${FAIRGUARD} -p ${XCODE_DIR_PATH}/Unity-iPhone.xcworkspace -s Unity-iPhone
                                                                mv ${XCODE_BUILD_PATH}/RTB-* ${XCODE_BUILD_PATH}/rtb.xcarchive
                                                    """
                                            }
                    }
                }
            }

           stage('Xcode工程 Archive') {
                when{
                  expression{return IS_BUILD_IPA.toBoolean()}
                  //expression{return false}
                }
                steps {
                    sh """
                        set -e
                        cd ${XCODE_BUILD_PROJECT_PATH}  
                        rm -f ${ARCHIVE_LOG_FILE}
                        rm -rf ${XCODE_ARCHIVE_PROJECT_PATH}
                        xcodebuild archive -workspace Unity-iPhone.xcworkspace -scheme "Unity-iPhone" -configuration "Release" -archivePath "${XCODE_ARCHIVE_PROJECT_PATH}" -destination "generic/platform=iOS" -allowProvisioningUpdates -allowProvisioningDeviceRegistration | tee ${ARCHIVE_LOG_FILE}  2>&1 

                        cp ${ARCHIVE_LOG_FILE} ${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}/Archive.txt
                        cat ${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}/Archive.txt
                    """
                }
            }

            stage('Xcode工程 Export') {
                when{
                  expression{return IS_BUILD_IPA.toBoolean()}
                  //expression{return false}
                }
                steps {
                    script{
                       def exportionPlist
                       
                       if (IS_UPLOAD_IPA.toBoolean()) {
                            exportionPlist = "${PROJECT_PATH}/BuildTool/ExportOptions_Upload.plist"
                        } 
                        else {
                            exportionPlist = "${PROJECT_PATH}/BuildTool/ExportOptions_Dev.plist"
                        }
                        
                       sh """
                        set -e
                        cd ${XCODE_BUILD_PROJECT_PATH} 
                        rm -rf "${IPA_PATH}/*"
                        
                        
                        xcodebuild -exportArchive -archivePath  "${XCODE_ARCHIVE_PROJECT_PATH}" -exportPath ${IPA_PATH} -exportOptionsPlist ${exportionPlist} -allowProvisioningUpdates -allowProvisioningDeviceRegistration
                    """ 
                    }
                }
            }

            stage('IPA 拷贝') {
                when{
                  expression{return IS_BUILD_IPA.toBoolean()}
                  //expression{return false}
                }
                steps {
                  script{
                    def ipa = sh(script: 'find ${IPA_PATH} -name "*.ipa" -print -quit', returnStdout: true).trim()
                    sh """
                        echo "${ipa}"
                        cp ${ipa} "${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}/"
                    """
                  }
                }
            }

            stage('IPA 上传TestFlight') {
                when{
                  //expression{return IS_UPLOAD_IPA.toBoolean()}
                  expression{return false}
                }
                steps {
                    script{
                      def sharePath= "${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}"
                      echo "${sharePath}"
                    
                      def ipa = sh(script: "find '${sharePath}' -name '*.ipa' -print -quit", returnStdout: true).trim()
                      sh """ 
                       set -e
                       xcrun altool --validate-app --type ios -f ${ipa} -u zhangruiguo@aviagames.com -p faoy-vcit-xyub-wqmr --verbose | tee "${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}/validate_log.txt"
                      if !xcrun altool --upload-app --type ios -f ${ipa} -u zhangruiguo@aviagames.com -p faoy-vcit-xyub-wqmr --verbose | tee "${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}/upload_log.txt"; then
                         echo "上传TestFlight失败"
                         exit 1
                      fi
                      """ 
                    }
                }
            }
        }
        post {
            always {
                echo "用户信息:${user}"

            }
            success{
                echo "构建成功"
                script{
                    def ipa = sh(script: "find '${PACKAGE_SHARE_PATH}/${BUILD_TIMESTAMP}' -name '*.ipa' -print -quit", returnStdout: true).trim()
                    def ipaUrl = sh(script: "echo '${ipa}' | sed 's|^${PACKAGE_SHARE_PATH}|${SHARE_URL}|'", returnStdout: true).trim()
                    def BUNDLE_NUM=sh(script:"cat ${BUNDLE_NUM_FILE}", returnStdout: true).trim()
                    sh """
                        curl -X POST -H "Content-Type: application/json" \
                        -d '{
                            "msg_type": "post",
                            "content": {
                                "post": {
                                    "zh_cn": {
                                        "title": "构建成功",
                                        "content": [
                                            [{"tag":"a", "text":"包体地址","href":"${ipaUrl}"}],
                                            [{"tag":"text", "text":"构建环境:${ENV}   版本号[1.2(${BUNDLE_NUM})]"}],
                                            [{"tag":"text", "text":"构建人:${user}"}]
                                        ]
                                    }
                                }
                            }
                        }' \
                        https地址
                    """
                }   
            }
            failure {
                echo "构建失败"
                script{
                    sh """
                        curl -X POST -H "Content-Type: application/json" \
                        -d '{
                            "msg_type": "post",
                            "content": {
                                "post": {
                                    "zh_cn": {
                                        "title": "构建失败",
                                        "content": [
                                            [{"tag":"a", "text":"失败详情","href":"${env.BUILD_URL}/console"}],
                                            [{"tag":"text", "text":"构建环境:${ENV}   版本号[1.2(${BUNDLE_NUM})]"}],
                                            [{"tag":"text", "text":"构建人:${user}"}]
                                        ]
                                    }
                                }
                            }
                        }' \
                        https地址
                    """
                }   
            }
        }
}
xcode

ExportOptions.plist文件的作用,就是在export包时的配置,这里面可以配置成导出后,并上传到TestFlight,就不用再单独上传了

XML 复制代码
<?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>
    <!-- 如果不上传,设为export -->
	<key>destination</key>
	<string>upload</string>
	<key>generateAppStoreInformation</key>
	<false/>
	<key>manageAppVersionAndBuildNumber</key>
	<true/>
	<key>method</key>
    <!-- 如果不上传,设为app-store/ad-hoc/enterprise -->
	<string>app-store-connect</string>
	<key>provisioningProfiles</key>
    <dict>
    <key>包的bundle id</key>
    <string>provision名字</string>
    <key>内嵌包的bundle id</key>
    <string>provision名字</string>
    </dict>
	<key>signingStyle</key>
	<string>manual</string>
	<key>stripSwiftSymbols</key>
	<true/>
	<key>teamID</key>
	<string></string>
	<key>testFlightInternalTestingOnly</key>
	<false/>
    <!-- 如果不上传,设为false -->
	<key>uploadSymbols</key>
	<true/>
</dict>
</plist>
Groovy语法

✅ jenkins的bool类型参数,在groovy里面是字符串,如果要判断,使用BoolParam.toBoolean()

✅ when{expression{return BoolParam.toBoolean()}}表示该阶段执不执行

✅def 变量赋值需要双引号"",比如def ttt="${test}"

✅sh""" """,""" """表示可写多行shell,且shell中不能定义变量,需要在script{}里面通过def定义变量,然后在sh里面通过${}引用

✅script里面的脚本,通过双引号包裹命令,单引号引用变量方式

比如 def ipa = sh(script: "find '${sharePath}' -name '*.ipa' -print -quit", returnStdout: true).trim()

✅unity_sdk.xcarchive 是个文件夹,删除用rm -rf

✅git update-index --assume-unchanged <file> 对本地文件忽略跟踪,比如分支上有这个文件,但是忽略本地的跟踪,但是git reset --hard HEAD 之后,还是会更改,所以先stash 再pop

相关推荐
雪儿waii6 小时前
Unity 中的 Resources 详解
unity·游戏引擎
鹿鸣天涯9 小时前
Xftp传输文件时,解决“无法显示远程文件夹”方法
运维·服务器·计算机
unDl IONA10 小时前
服务器部署,用 nginx 部署后页面刷新 404 问题,宝塔面板修改(修改 nginx.conf 配置文件)
运维·服务器·nginx
Web极客码10 小时前
WordPress管理员角色详解及注意事项
运维·服务器·wordpress
geinvse_seg10 小时前
中小团队如何低成本搭建项目管理系统?基于 Ubuntu 的 Dootask 私有化部署实战
linux·运维·ubuntu
星辰徐哥10 小时前
鸿蒙金融理财全栈项目——上线与运维、用户反馈、持续迭代优化
运维·金融·harmonyos
CSCN新手听安10 小时前
【linux】高级IO,以ET模式运行的epoll版本的TCP服务器实现reactor反应堆
linux·运维·服务器·c++·高级io·epoll·reactor反应堆
丶伯爵式10 小时前
Ubuntu 24.04 更换国内软件源指南 | 2026年3月26日
linux·运维·ubuntu·国内源·升级
xingyuzhisuan10 小时前
租用GPU服务器进行深度学习课程教学的实验环境搭建
运维·人工智能·深度学习·gpu算力
Java后端的Ai之路10 小时前
Linux端口进程查找与终止教程
linux·运维·服务器