using UnityEditor; using System.Linq; using System; using System.IO; static class BuildCommand { private const string KEYSTORE_PASS = "KEYSTORE_PASS"; private const string KEY_ALIAS_PASS = "KEY_ALIAS_PASS"; private const string KEY_ALIAS_NAME = "KEY_ALIAS_NAME"; private const string KEYSTORE = "keystore.keystore"; private const string BUILD_OPTIONS_ENV_VAR = "BuildOptions"; private const string ANDROID_BUNDLE_VERSION_CODE = "VERSION_BUILD_VAR"; private const string ANDROID_APP_BUNDLE = "BUILD_APP_BUNDLE"; private const string SCRIPTING_BACKEND_ENV_VAR = "SCRIPTING_BACKEND"; private const string VERSION_NUMBER_VAR = "VERSION_NUMBER_VAR"; private const string VERSION_iOS = "VERSION_BUILD_VAR"; static string GetArgument(string name) { string[] args = Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length; i++) { if (args[i].Contains(name)) { return args[i + 1]; } } return null; } static string[] GetEnabledScenes() { return ( from scene in EditorBuildSettings.scenes where scene.enabled where !string.IsNullOrEmpty(scene.path) select scene.path ).ToArray(); } static BuildTarget GetBuildTarget() { string buildTargetName = GetArgument("customBuildTarget"); Console.WriteLine(":: Received customBuildTarget " + buildTargetName); if (buildTargetName.ToLower() == "android") { #if !UNITY_5_6_OR_NEWER // https://issuetracker.unity3d.com/issues/buildoptions-dot-acceptexternalmodificationstoplayer-causes-unityexception-unknown-project-type-0 // Fixed in Unity 5.6.0 // side effect to fix android build system: EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Internal; #endif } if (buildTargetName.TryConvertToEnum(out BuildTarget target)) return target; Console.WriteLine($":: {nameof(buildTargetName)} \"{buildTargetName}\" not defined on enum {nameof(BuildTarget)}, using {nameof(BuildTarget.NoTarget)} enum to build"); return BuildTarget.NoTarget; } static string GetBuildPath() { string buildPath = GetArgument("customBuildPath"); Console.WriteLine(":: Received customBuildPath " + buildPath); if (buildPath == "") { throw new Exception("customBuildPath argument is missing"); } return buildPath; } static string GetBuildName() { string buildName = GetArgument("customBuildName"); Console.WriteLine(":: Received customBuildName " + buildName); if (buildName == "") { throw new Exception("customBuildName argument is missing"); } return buildName; } static string GetFixedBuildPath(BuildTarget buildTarget, string buildPath, string buildName) { if (buildTarget.ToString().ToLower().Contains("windows")) { buildName += ".exe"; } else if (buildTarget == BuildTarget.Android) { #if UNITY_2018_3_OR_NEWER buildName += EditorUserBuildSettings.buildAppBundle ? ".aab" : ".apk"; #else buildName += ".apk"; #endif } return buildPath + buildName; } static BuildOptions GetBuildOptions() { if (TryGetEnv(BUILD_OPTIONS_ENV_VAR, out string envVar)) { string[] allOptionVars = envVar.Split(','); BuildOptions allOptions = BuildOptions.None; BuildOptions option; string optionVar; int length = allOptionVars.Length; Console.WriteLine($":: Detecting {BUILD_OPTIONS_ENV_VAR} env var with {length} elements ({envVar})"); for (int i = 0; i < length; i++) { optionVar = allOptionVars[i]; if (optionVar.TryConvertToEnum(out option)) { allOptions |= option; } else { Console.WriteLine($":: Cannot convert {optionVar} to {nameof(BuildOptions)} enum, skipping it."); } } return allOptions; } return BuildOptions.None; } // https://stackoverflow.com/questions/1082532/how-to-tryparse-for-enum-value static bool TryConvertToEnum(this string strEnumValue, out TEnum value) { if (!Enum.IsDefined(typeof(TEnum), strEnumValue)) { value = default; return false; } value = (TEnum)Enum.Parse(typeof(TEnum), strEnumValue); return true; } static bool TryGetEnv(string key, out string value) { value = Environment.GetEnvironmentVariable(key); return !string.IsNullOrEmpty(value); } static void SetScriptingBackendFromEnv(BuildTarget platform) { var targetGroup = BuildPipeline.GetBuildTargetGroup(platform); if (TryGetEnv(SCRIPTING_BACKEND_ENV_VAR, out string scriptingBackend)) { if (scriptingBackend.TryConvertToEnum(out ScriptingImplementation backend)) { Console.WriteLine($":: Setting ScriptingBackend to {backend}"); PlayerSettings.SetScriptingBackend(targetGroup, backend); } else { string possibleValues = string.Join(", ", Enum.GetValues(typeof(ScriptingImplementation)).Cast()); throw new Exception($"Could not find '{scriptingBackend}' in ScriptingImplementation enum. Possible values are: {possibleValues}"); } } else { var defaultBackend = PlayerSettings.GetDefaultScriptingBackend(targetGroup); Console.WriteLine($":: Using project's configured ScriptingBackend (should be {defaultBackend} for targetGroup {targetGroup}"); } } static void PerformBuild() { var buildTarget = GetBuildTarget(); Console.WriteLine(":: Performing build"); if (TryGetEnv(VERSION_NUMBER_VAR, out var bundleVersionNumber)) { if (buildTarget == BuildTarget.iOS) { bundleVersionNumber = GetIosVersion(); } Console.WriteLine($":: Setting bundleVersionNumber to '{bundleVersionNumber}' (Length: {bundleVersionNumber.Length})"); PlayerSettings.bundleVersion = bundleVersionNumber; } if (buildTarget == BuildTarget.Android) { HandleAndroidAppBundle(); HandleAndroidBundleVersionCode(); HandleAndroidKeystore(); } var buildPath = GetBuildPath(); var buildName = GetBuildName(); var buildOptions = GetBuildOptions(); var fixedBuildPath = GetFixedBuildPath(buildTarget, buildPath, buildName); SetScriptingBackendFromEnv(buildTarget); var buildReport = BuildPipeline.BuildPlayer(GetEnabledScenes(), fixedBuildPath, buildTarget, buildOptions); if (buildReport.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) throw new Exception($"Build ended with {buildReport.summary.result} status"); Console.WriteLine(":: Done with build"); } private static void HandleAndroidAppBundle() { if (TryGetEnv(ANDROID_APP_BUNDLE, out string value)) { #if UNITY_2018_3_OR_NEWER if (bool.TryParse(value, out bool buildAppBundle)) { EditorUserBuildSettings.buildAppBundle = buildAppBundle; Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected, set buildAppBundle to {value}."); } else { Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected but the value \"{value}\" is not a boolean."); } #else Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected but does not work with lower Unity version than 2018.3"); #endif } } private static void HandleAndroidBundleVersionCode() { if (TryGetEnv(ANDROID_BUNDLE_VERSION_CODE, out string value)) { if (int.TryParse(value, out int version)) { PlayerSettings.Android.bundleVersionCode = version; Console.WriteLine($":: {ANDROID_BUNDLE_VERSION_CODE} env var detected, set the bundle version code to {value}."); } else Console.WriteLine($":: {ANDROID_BUNDLE_VERSION_CODE} env var detected but the version value \"{value}\" is not an integer."); } } private static string GetIosVersion() { if (TryGetEnv(VERSION_iOS, out string value)) { if (int.TryParse(value, out int version)) { Console.WriteLine($":: {VERSION_iOS} env var detected, set the version to {value}."); return version.ToString(); } else Console.WriteLine($":: {VERSION_iOS} env var detected but the version value \"{value}\" is not an integer."); } throw new ArgumentNullException(nameof(value), $":: Error finding {VERSION_iOS} env var"); } private static void HandleAndroidKeystore() { #if UNITY_2019_1_OR_NEWER PlayerSettings.Android.useCustomKeystore = false; #endif if (!File.Exists(KEYSTORE)) { Console.WriteLine($":: {KEYSTORE} not found, skipping setup, using Unity's default keystore"); return; } PlayerSettings.Android.keystoreName = KEYSTORE; string keystorePass; string keystoreAliasPass; if (TryGetEnv(KEY_ALIAS_NAME, out string keyaliasName)) { PlayerSettings.Android.keyaliasName = keyaliasName; Console.WriteLine($":: using ${KEY_ALIAS_NAME} env var on PlayerSettings"); } else { Console.WriteLine($":: ${KEY_ALIAS_NAME} env var not set, using Project's PlayerSettings"); } if (!TryGetEnv(KEYSTORE_PASS, out keystorePass)) { Console.WriteLine($":: ${KEYSTORE_PASS} env var not set, skipping setup, using Unity's default keystore"); return; } if (!TryGetEnv(KEY_ALIAS_PASS, out keystoreAliasPass)) { Console.WriteLine($":: ${KEY_ALIAS_PASS} env var not set, skipping setup, using Unity's default keystore"); return; } #if UNITY_2019_1_OR_NEWER PlayerSettings.Android.useCustomKeystore = true; #endif PlayerSettings.Android.keystorePass = keystorePass; PlayerSettings.Android.keyaliasPass = keystoreAliasPass; } }