Building per script for Windows and Android

Hi,
I’m working on Windows to build an application for Windows and Android. So I have a build script, where I can trigger from a menu item a new build for either Windows or Android.
The script uses BuildPipeline.BuildPlayer(buildPlayerOptions), where I set the target in buildPlayerOptions.

However, the behaviour of the editor is rather strange. The platform in “build settings” appears to change to the target, but in my editor (Visual Studio) it does not change preprocessor variables (UNITY_ANDROID or UNITY-STANDALONE, respectively). This makes the whole thing somewhat inconsistent.
Such I introduced an additional call EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows64). Now preprocessor variables are changed correctly, but the app running inside the editor does not work accordingly: it still executes the alternative code defined inside the condition given by the previous preprocessor variable.

However, when I split switching the target and building for this target everything seems to be alright (in a first test). But this defeats the purpose of having a build script in the first place. How is it done right? IMO, the current behaviour is a bug. It happens ever since I tried it, currently in version 2022.3.35f1.

PS: additional question: what’s the difference between EditorUserBuildSettings.SwitchActiveBuildTarget and EditorUserBuildSettings.SwitchActiveBuildTargetAsync? I don’t get it from the docs.

This is a big pain point for scripting player builds. Ideally, you wouldn’t use any build target dependent conditional compilation for your editor code, so that a build could be done for any build target irrespective of the active build target. However, not even Unity is doing this in their own code and are consequently making an active build target switch mandatory before a player build.

The issue is that the editor code has been compiled for the active build target with all the corresponding defines. If you want to switch the active build target, the editor code not only has to be recompiled but a domain reload needs to happen to actually start using the recompiled code. That means your code needs to stop, Unity needs to destroy and serialize all objects, do the domain reload, then deserialize and re-create all objects, and finally you somehow need to continue with your build.

If you just continue with BuildPlayer, the editor never gets a chance to do the domain reload and you’re still using the editor code compiled for the previous active build target with the outdated defines.

The difference between SwitchActiveBuildTarget and SwitchActiveBuildTargetAsync is pretty minor: The first immediately re-imports all assets and then does the domain reload in the next editor update. The async variant does the domain reload in the next editor update and then re-imports all assets. The first one exists forever and the async one was added a bit later (in Unity 2017). I’d recommend using the async one, since you usually should wait for the domain reload anyway. The first one is mostly useful if you want the assets to be re-imported but you don’t care for the domain reload.

Now, to do an active build target switch including domain reload before calling BuildPlayer, you need some Unity object that survives the domain reload. This could either be an open editor window or a scriptable object with its hideFlags set so it doesn’t get unloaded. The data the object can contain is limited to things Unity can serialize, so any delegates or references to non-Unity objects are out. It get’s a bit hairy, I’ve implemented it for my own build framework here.

2 Likes

Thanks a lot! Perfect answer.

Although I’m not quite sure how to wait for the domain reload, I’ll have a look at your build framework.

My build automation was previously running fine but now I’ve hit this issue as well.
No conditional code is loaded after SwitchActiveBuildTarget.
Has this been raised formally as a bug with unity?

This wasn’t a bug, just cross-target building in Unity being difficult to automate.

Better create a new topic instead of hijacking this one. Also please post a detailed explanation of what’s happening and include code where appropriate.

It felt like a bug to me as my automated build code has run for years and suddenly hit this issue.
I upgraded Unity and added a plugin then my XCode project stopped building.
Have wasted time chasing down the issue after bothering the plugin builder only to find his platform specific post build code isn’t being run under these specific circumstances.

At the very least common sense dictates it should be a feature request to Unity that SwitchActiveBuildTarget is blocking until the Editor updates and the preprocessor has run.
What’s the actual use of it otherwise? It returns true and nothing is compiled? Stupid.

I’m not hijacking anything this thread is about SwitchActiveBuildTarget returning before Editor code is compiled and this is my exact problem. I’m here to discuss the issue and potential solutions that’s what a forum is for.

This topic wasn’t about a bug and the issue was resolved. You didn’t read the topic properly and just dumped your issue here. Pretty clear case of hijacking in my opinion.

This is exactly what I explained previously in this thread. Switching build targets requires a domain reload and it’s impossible for scripts to continue executing through that, so the domain reloads happens in the next editor update.

As I said, if you explain what you’re doing exactly and post your code, we can try to help to find what changed and why things aren’t working as you expect.

That’s your assumptions. I read the topic and have the EXACT same issue so I joined the conversation.

I already explained why I thought it was a bug. My build automation has been running for over a decade since my project was started with Unity 5 and then suddenly stopped after an upgrade so I thought it was a bug. Big deal I made a mistake. Have you ever made one?

What solution? You linked to a file with over 500 lines of code!? Who is this a resolution for? I skimmed over it for a while but I started my career as a developer in the 90’s I’m not in the mood to go over your entire project to understand one part I might need. How about sharing the meat of the solution here so we can all fix this and move onto the next issue and get on with our lives?

Look up the definition youself: threadjacking - Wiktionary, the free dictionary
I didn’t change the subject. Now you’ve turned the subject into you talking down to me like I’m a child that needs schooling on web etiquette and coding!? Who’s the highjacker now?

Anyway I appreciate the fact that this made me aware of how useless the blocking version of SwitchActiveBuildTarget is. Knowing this I’ve come up with a quick and dirty solution to using SwitchActiveBuildTargetAsync I will share here soon.

Here are my editor scripts to automate building for multiple platforms using SwitchActiveBuildTargetAsync .

To be able to respond to the build target changing multiple times using SwitchActiveBuildTargetAsync we need to be able to schedule tasks on the Editor thread.

This editor script gives us a task scheduler in the editor:

using System.Collections.Generic;
using UnityEditor;

[InitializeOnLoad]
public class EditorMain 
{
    //Tasks to be executed from editor thread
    public delegate void Task();
    private static Queue<Task> TaskQueue = new Queue<Task>();
    private static object _queueLock = new object();

    static EditorMain()
    {
        EditorApplication.update += Update;
    }

    static void Update()
    {
        //Perform any tasks from other threads
        lock (_queueLock)
        {
            if (TaskQueue.Count > 0)
                TaskQueue.Dequeue()();
        }
    }

    public static void ScheduleTask(Task newTask)
    {
        lock (_queueLock)
        {
            if (TaskQueue.Count < 100)
                TaskQueue.Enqueue(newTask);
        }
    }
}

My editor script for the editor GUI lets me toggle what platforms to build for calling a common build function and switching build target if nessesary. SessionState is used to flag that we are switching targets as part of an automated build:

public override void OnInspectorGUI()
{
    ///...///

    EditorGUILayout.LabelField("Build", EditorStyles.boldLabel);
    buildPC = EditorGUILayout.Toggle("Build PC", buildPC);
    buildAndroid = EditorGUILayout.Toggle("Build Android", buildAndroid);
    ///And so on for other platforms///

    if (GUILayout.Button("Build"))
    {
        //Set state for build options
        SessionState.SetBool("buildPC", buildPC);
        SessionState.SetBool("buildAndroid", buildAndroid);
        ///And so on for other platforms///

        ///...///

        /////////////////////////////////////////////////////////////////
        // PC
        if (buildPC)
        {
            if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.StandaloneWindows)
            {
                EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows);
                return;
            }
            StartBuild(BuildTarget.StandaloneWindows);
        }

        /////////////////////////////////////////////////////////////////
        // Android
        if (buildAndroid)
        {
            if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android)
            {
                EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.Android, BuildTarget.Android);
                return;
            }
            StartBuild(BuildTarget.Android);
        }

        ///And so on for other platforms///

I have a ActiveBuildTargetListener class that needs to modify some objects and settings per platform that will now check the SessionState variables to see if we are in the middle of an automated build and schedule the next build target change if required.

using UnityEditor;
using UnityEditor.Build;
using UnityEngine;

public class ActiveBuildTargetListener : IActiveBuildTargetChanged
{
    public int callbackOrder { get { return 0; } }
    public void OnActiveBuildTargetChanged(BuildTarget previousTarget, BuildTarget newTarget)
    {
        BuildPreProcessor.SetupAppForPlatform(newTarget);

#if UNITY_STANDALONE_WIN
        Debug.Log("OnActiveBuildTargetChanged UNITY_STANDALONE_WIN");
        if (SessionState.GetBool("buildPC", false) == true)
        {
            SessionState.SetBool("buildPC", false); //clear flag
            BuildForPlatformsEditor.StartBuild(BuildTarget.StandaloneWindows);

            //postbuild move onto next
            ScheduleNextBuild();
        }
#elif UNITY_ANDROID
        Debug.Log("OnActiveBuildTargetChanged UNITY_ANDROID");
        if (SessionState.GetBool("buildAndroid", false) == true)
        {
            SessionState.SetBool("buildAndroid", false); //clear flag
            BuildForPlatformsEditor.StartBuild(BuildTarget.Android);

            //postbuild move onto next
            ScheduleNextBuild();
        }
        ///And so on for other platforms///
    }

    void ScheduleNextBuild()
    {
        if (SessionState.GetBool("buildAndroid", false) == true)
        {
            EditorMain.ScheduleTask(new EditorMain.Task(delegate { EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.Android, BuildTarget.Android); }));
            return;
        }

        ///And so on for other platforms///
    }
}

This will automate the build for multiple platforms while preprocessing conditional editor code correctly.

Maybe hijacking wasn’t the right word. The issue is that many people here dig up old topics with similar symptoms and assume it’s the same issue and that it hasn’t been fixed. But the reality is most often that it’s a different issue and a new topic would be a better place to start a fresh analysis and discussion.

You also provided very little information, which is often necessary to narrow down the issue. E.g. Unity made it harder to create corrupted builds but then relaxed the restrictions because many users’ build automation was breaking. If you provided your Unity version, it would have been possible to check if you are using an affected version.

What problem did you run into to add the task queue? Nothing in your code should be running on a non-main thread and the scheduling shouldn’t be necessary.

To kick off the next build calling SwitchActiveBuildTargetAsync from ActiveBuildTargetListener would result in an error as the previous OnActiveBuildTargetChanged event hadn’t completed.