Scriptable building target with Assemblies

The context

I’ve created a BuildCommand.cs file to set up a CI/CD pipeline, and I also have some tests that require Assemblies to run. However, whenever I try to build the project, I encounter a “BuildTarget not found” error. I’m not sure which Assembly I need to import or if there’s another issue causing this problem.

Error :

Assets\Scripts\Editor\BuildCommand.cs(150,44): error CS0246: The type or namespace name 'BuildTarget' could not be found (are you missing a using directive or an assembly reference?)

Technical informations

I’m with the version 6000.0.19f1
Target : Sandalone Windows x64

Sources

Here is the Assembly containing the BuildCommand.cs

I’ve got the following building script :

using UnityEditor;
using System.Linq;
using System;
using System.IO;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;

// ReSharper disable once UnusedType.Global
// ReSharper disable once ArrangeTypeModifiers
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;
            int length = allOptionVars.Length;

            Console.WriteLine($":: Detecting {BUILD_OPTIONS_ENV_VAR} env var with {length} elements ({envVar})");

            for (int i = 0; i < length; i++) {
                string optionVar = allOptionVars[i];

                if (optionVar.TryConvertToEnum(out BuildOptions 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<TEnum>(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);
        //NamedBuildTarget namedBuildTarget = NamedBuildTarget.FromBuildTargetGroup(targetGroup);
        if (TryGetEnv(SCRIPTING_BACKEND_ENV_VAR, out string scriptingBackend)) {
            if (scriptingBackend.TryConvertToEnum(out ScriptingImplementation backend)) {
                Console.WriteLine($":: Setting ScriptingBackend to {backend}");
                //PlayerSettings.SetScriptingBackend(namedBuildTarget, backend);
                PlayerSettings.SetScriptingBackend(targetGroup, backend);

            } else {
                string possibleValues = string.Join(", ", Enum.GetValues(typeof(ScriptingImplementation)).Cast<ScriptingImplementation>());
                throw new Exception($"Could not find '{scriptingBackend}' in ScriptingImplementation enum. Possible values are: {possibleValues}");
            }
        } else {
            //var defaultBackend = PlayerSettings.GetDefaultScriptingBackend(namedBuildTarget);
            var defaultBackend = PlayerSettings.GetDefaultScriptingBackend(targetGroup);
            Console.WriteLine($":: Using project's configured ScriptingBackend (should be {defaultBackend} for targetGroup {targetGroup}");
        }
    }

    // ReSharper disable once UnusedMember.Local
    // ReSharper disable once ArrangeTypeMemberModifiers
    static void PerformBuild()
    {
        var buildTarget = GetBuildTarget();

        Console.WriteLine(":: Performing build");
        if (TryGetEnv(VERSION_NUMBER_VAR, out string 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();
        }

        string buildPath      = GetBuildPath();
        string buildName      = GetBuildName();
        var buildOptions   = GetBuildOptions();
        string fixedBuildPath = GetFixedBuildPath(buildTarget, buildPath, buildName);

        SetScriptingBackendFromEnv(buildTarget);

        string[] scenes = GetEnabledScenes();
        
        // Log scenes
        Console.WriteLine(":: Enabled scenes:");
        foreach (string scene in scenes)
        {
            Console.WriteLine($":: {scene}");
        }
        
        
        
        if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.Standalone, buildTarget))
            throw new Exception($"Building {buildTarget} target is not supported on this build machine");
        BuildReport buildReport;
        try {
            buildReport = BuildPipeline.BuildPlayer(scenes, fixedBuildPath, buildTarget, buildOptions);
        }
        catch (Exception e) {
            Console.WriteLine(":: Exception caught during build process");
            Console.WriteLine(e.Message);
            throw;
        }

        if (buildReport.summary.result != BuildResult.Succeeded){
            // Debug the build report
            Console.WriteLine(":: Build failed");
            foreach (var step in buildReport.steps)
            {
                Console.WriteLine($":: {step.name} - {step.duration.TotalSeconds} seconds");
                foreach (var message in step.messages)
                {
                    Console.WriteLine($":: {message.content}");
                }
            }

            // Throw an exception to make the GitHub action fail
            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)) return;
#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)) return;
        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))
            throw new ArgumentNullException(nameof(value), $":: Error finding {VERSION_IOS} env var");
        
        
        if (int.TryParse(value, out int version))
        {
            Console.WriteLine($":: {VERSION_IOS} env var detected, set the version to {value}.");
            return version.ToString();
        }
        
        
        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;

        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 string keystorePass)) {
            Console.WriteLine($":: ${KEYSTORE_PASS} env var not set, skipping setup, using Unity's default keystore");
            return;
        }

        if (!TryGetEnv(KEY_ALIAS_PASS, out string 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;
    }



    // ReSharper disable once UnusedMember.Local
    // ReSharper disable once ArrangeTypeMemberModifiers
    static void LoadProject(){
        Console.WriteLine(":: Project loaded");
    }
}

The code uses editor-only code. You need to change your platform settings to only include the editor, otherwise it will try to compile this as part of the player.

It worked, thank you !