diff --git a/UnityProjects/.gitignore b/UnityProjects/.gitignore
index f5b5994154f..9a482daae2e 100644
--- a/UnityProjects/.gitignore
+++ b/UnityProjects/.gitignore
@@ -30,6 +30,9 @@
# Visual Studio Code cache directory
.vscode/
+# Rider cache directory
+.idea/
+
# Gradle cache directory
.gradle/
diff --git a/UnityProjects/MRTKDevTemplate/Assets/XR/Settings/OpenXR Package Settings.asset b/UnityProjects/MRTKDevTemplate/Assets/XR/Settings/OpenXR Package Settings.asset
index 44a9005260e..ec8c4be060d 100644
--- a/UnityProjects/MRTKDevTemplate/Assets/XR/Settings/OpenXR Package Settings.asset
+++ b/UnityProjects/MRTKDevTemplate/Assets/XR/Settings/OpenXR Package Settings.asset
@@ -478,11 +478,12 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 9f0ebc320a151d3408ea1e9fce54d40e, type: 3}
m_Name: OpenXR Package Settings
m_EditorClassIdentifier:
- Keys: 010000000e00000007000000
+ Keys: 010000000e000000070000000d000000
Values:
- {fileID: -7232978043429515688}
- {fileID: -3301589181217746260}
- {fileID: -5356075571387229412}
+ - {fileID: 1803865306266780183}
--- !u!114 &668207626733321193
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -543,6 +544,21 @@ MonoBehaviour:
company: Microsoft
priority: 0
required: 0
+--- !u!114 &1803865306266780183
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: b5a1f07dc5afe854f9f12a4194aca3fb, type: 3}
+ m_Name: WebGL
+ m_EditorClassIdentifier:
+ features: []
+ m_renderMode: 1
+ m_depthSubmissionMode: 0
--- !u!114 &2243842767272741898
MonoBehaviour:
m_ObjectHideFlags: 0
diff --git a/UnityProjects/MRTKDevTemplate/Packages/manifest.json b/UnityProjects/MRTKDevTemplate/Packages/manifest.json
index b45e02b3a0b..528df20c879 100644
--- a/UnityProjects/MRTKDevTemplate/Packages/manifest.json
+++ b/UnityProjects/MRTKDevTemplate/Packages/manifest.json
@@ -6,6 +6,7 @@
"com.microsoft.mixedreality.visualprofiler": "https://github.com/microsoft/VisualProfiler-Unity.git#v2.2.0",
"com.microsoft.mrtk.accessibility": "file:../../../com.microsoft.mrtk.accessibility",
"com.microsoft.mrtk.audio": "file:../../../com.microsoft.mrtk.audio",
+ "com.microsoft.mrtk.buildwindow": "file:../../../com.microsoft.mrtk.buildwindow",
"com.microsoft.mrtk.core": "file:../../../com.microsoft.mrtk.core",
"com.microsoft.mrtk.data": "file:../../../com.microsoft.mrtk.data",
"com.microsoft.mrtk.diagnostics": "file:../../../com.microsoft.mrtk.diagnostics",
diff --git a/UnityProjects/MRTKDevTemplate/Packages/packages-lock.json b/UnityProjects/MRTKDevTemplate/Packages/packages-lock.json
index 73e66495078..3f54c63c1ba 100644
--- a/UnityProjects/MRTKDevTemplate/Packages/packages-lock.json
+++ b/UnityProjects/MRTKDevTemplate/Packages/packages-lock.json
@@ -58,6 +58,12 @@
"com.microsoft.mrtk.core": "3.0.0-development"
}
},
+ "com.microsoft.mrtk.buildwindow": {
+ "version": "file:../../../com.microsoft.mrtk.buildwindow",
+ "depth": 0,
+ "source": "local",
+ "dependencies": {}
+ },
"com.microsoft.mrtk.core": {
"version": "file:../../../com.microsoft.mrtk.core",
"depth": 0,
diff --git a/UnityProjects/MRTKDevTemplate/ProjectSettings/ProjectSettings.asset b/UnityProjects/MRTKDevTemplate/ProjectSettings/ProjectSettings.asset
index 942fb689015..8a823be042d 100644
--- a/UnityProjects/MRTKDevTemplate/ProjectSettings/ProjectSettings.asset
+++ b/UnityProjects/MRTKDevTemplate/ProjectSettings/ProjectSettings.asset
@@ -156,7 +156,7 @@ PlayerSettings:
androidMaxAspectRatio: 2.1
applicationIdentifier:
Android: com.Microsoft.MRTK3Sample
- Standalone: com.Microsoft.MRTK3Sample
+ Standalone: com.Microsoft.MRTK3-Sample
buildNumber:
Standalone: 0
iPhone: 0
diff --git a/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs b/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs
new file mode 100644
index 00000000000..8d6af93d0ec
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Reflection;
+
+[assembly: AssemblyProduct("Microsoft® Mixed Reality Toolkit Build Window")]
+[assembly: AssemblyCopyright("Copyright © Microsoft Corporation")]
+
+// The AssemblyVersion attribute is checked-in and is recommended not to be changed often.
+// https://docs.microsoft.com/troubleshoot/visualstudio/general/assembly-version-assembly-file-version
+// AssemblyFileVersion and AssemblyInformationalVersion are added by pack-upm.ps1 to match the current MRTK build version.
+[assembly: AssemblyVersion("3.0.0.0")]
diff --git a/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs.meta b/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs.meta
new file mode 100644
index 00000000000..1c44e72fd5a
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cd04de1cc836d51438711f0910137444
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs b/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs
new file mode 100644
index 00000000000..485245ed264
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs
@@ -0,0 +1,175 @@
+// MIT License
+
+// Copyright(c) 2016 Modest Tree Media Inc
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using UnityEngine;
+
+[assembly: InternalsVisibleTo("Microsoft.MixedReality.Toolkit.Tests.PlayModeTests")]
+namespace Microsoft.MixedReality.Toolkit.Utilities
+{
+ ///
+ /// This Async Coroutine Runner is just an object to
+ /// ensure that coroutines run properly with async/await.
+ ///
+ ///
+ /// The object that this MonoBehavior is attached to must be a root object in the
+ /// scene, as it will be marked as DontDestroyOnLoad (so that when scenes are changed,
+ /// it will persist instead of being destroyed). The runner will force itself to
+ /// the root of the scene if it's rooted elsewhere.
+ ///
+ [AddComponentMenu("Scripts/MRTK/Core/AsyncCoroutineRunner")]
+ internal sealed class AsyncCoroutineRunner : MonoBehaviour
+ {
+ private static AsyncCoroutineRunner instance;
+
+ private static bool isInstanceRunning = false;
+
+ private static readonly Queue Actions = new Queue();
+
+ internal static AsyncCoroutineRunner Instance
+ {
+ get
+ {
+ if (instance == null)
+ {
+ instance = FindObjectOfType();
+ }
+
+ // FindObjectOfType() only search for objects attached to active GameObjects. The FindObjectOfType(bool includeInactive) variant is not available to Unity 2019.4 and earlier so cannot be used.
+ // We instead search for GameObject called AsyncCoroutineRunner and see if it has the component attached.
+ if (instance == null)
+ {
+ var instanceGameObject = GameObject.Find("AsyncCoroutineRunner");
+
+ if (instanceGameObject != null)
+ {
+ instance = instanceGameObject.GetComponent();
+
+ if (instance == null)
+ {
+ Debug.Log("[AsyncCoroutineRunner] Found a \"AsyncCoroutineRunner\" GameObject but didn't have the AsyncCoroutineRunner component attached. Attaching the script.");
+ instance = instanceGameObject.AddComponent();
+ }
+ }
+ }
+
+ if (instance == null)
+ {
+ Debug.Log("[AsyncCoroutineRunner] There is no AsyncCoroutineRunner in the scene. Adding a GameObject with AsyncCoroutineRunner attached at the root of the scene.");
+ instance = new GameObject("AsyncCoroutineRunner").AddComponent();
+ }
+ else if (!instance.isActiveAndEnabled)
+ {
+ if (!instance.enabled)
+ {
+ Debug.LogWarning("[AsyncCoroutineRunner] Found a disabled AsyncCoroutineRunner component. Enabling the component.");
+ instance.enabled = true;
+ }
+ if (!instance.gameObject.activeSelf)
+ {
+ Debug.LogWarning("[AsyncCoroutineRunner] Found an AsyncCoroutineRunner attached to an inactive GameObject. Setting the GameObject active.");
+ instance.gameObject.SetActive(true);
+ }
+ }
+
+ instance.gameObject.hideFlags = HideFlags.None;
+
+ // AsyncCoroutineRunner must be at the root so that we can call DontDestroyOnLoad on it.
+ // This is ultimately to ensure that it persists across scene loads/unloads.
+ if (instance.transform.parent != null)
+ {
+ Debug.LogWarning($"[AsyncCoroutineRunner] AsyncCoroutineRunner was found as a child of another GameObject {instance.transform.parent}, " +
+ "it must be a root object in the scene. Moving the AsyncCoroutineRunner to the root.");
+ instance.transform.parent = null;
+ }
+
+#if !UNITY_EDITOR
+ DontDestroyOnLoad(instance);
+#endif
+ return instance;
+ }
+ }
+
+ internal static void Post(Action task)
+ {
+ lock (Actions)
+ {
+ Actions.Enqueue(task);
+ }
+ }
+
+ internal static bool IsInstanceRunning => isInstanceRunning;
+
+ private void Update()
+ {
+ if (Instance != this)
+ {
+ Debug.Log("[AsyncCoroutineRunner] Multiple active AsyncCoroutineRunners is present in the scene. Disabling duplicate ones.");
+ enabled = false;
+ return;
+ }
+ isInstanceRunning = true;
+
+ int actionCount;
+
+ lock (Actions)
+ {
+ actionCount = Actions.Count;
+ }
+
+ for (int i = 0; i < actionCount; i++)
+ {
+ Action next;
+
+ lock (Actions)
+ {
+ next = Actions.Dequeue();
+ }
+
+ next();
+ }
+ }
+
+ private void OnDisable()
+ {
+ if (instance == this)
+ {
+ isInstanceRunning = false;
+ }
+ }
+
+ private void OnEnable()
+ {
+ if (Instance != this)
+ {
+ Debug.Log("[AsyncCoroutineRunner] Multiple active AsyncCoroutineRunners is present in the scene. Disabling duplicate ones.");
+ enabled = false;
+ }
+ else
+ {
+ isInstanceRunning = true;
+ }
+ }
+ }
+}
diff --git a/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs.meta b/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs.meta
new file mode 100644
index 00000000000..44fb96f1828
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 3bcd4a2fcc384f158dcbe58dcac06f64
+timeCreated: 1663172198
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy.meta
new file mode 100644
index 00000000000..027a685d2ed
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b7605e91495148abb508e5e56e62f9d8
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs
new file mode 100644
index 00000000000..7a76fbc70cc
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.MixedReality.Toolkit.Utilities.Editor;
+using System;
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ ///
+ /// Build and Deploy Specific Editor Preferences for the Build and Deploy Window.
+ ///
+ public static class BuildDeployPreferences
+ {
+ // Constants
+ private const string EDITOR_PREF_BUILD_DIR = "BuildDeployWindow_BuildDir";
+ private const string EDITOR_PREF_INCREMENT_BUILD_VERSION = "BuildDeployWindow_IncrementBuildVersion";
+ private const string EDITOR_PREF_3D_APP_LAUNCHER_MODEL_LOCATION = "BuildDeployWindow_AppLauncherModelLocation";
+
+ ///
+ /// The Build Directory that the Mixed Reality Toolkit will build to.
+ ///
+ ///
+ /// This is a root build folder path. Each platform build will be put into a child directory with the name of the current active build target.
+ ///
+ public static string BuildDirectory
+ {
+ get => $"{EditorPreferences.Get(EDITOR_PREF_BUILD_DIR, "Builds")}/{EditorUserBuildSettings.activeBuildTarget}";
+ set => EditorPreferences.Set(EDITOR_PREF_BUILD_DIR, value.Replace($"/{EditorUserBuildSettings.activeBuildTarget}", string.Empty));
+ }
+
+ ///
+ /// The absolute path to
+ ///
+ public static string AbsoluteBuildDirectory
+ {
+ get
+ {
+ string rootBuildDirectory = BuildDirectory;
+ int dirCharIndex = rootBuildDirectory.IndexOf("/", StringComparison.Ordinal);
+
+ if (dirCharIndex != -1)
+ {
+ rootBuildDirectory = rootBuildDirectory.Substring(0, dirCharIndex);
+ }
+
+ return Path.GetFullPath(Path.Combine(Path.Combine(Application.dataPath, ".."), rootBuildDirectory));
+ }
+ }
+
+ ///
+ /// Current setting to increment build visioning.
+ ///
+ public static bool IncrementBuildVersion
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_INCREMENT_BUILD_VERSION, true);
+ set => EditorPreferences.Set(EDITOR_PREF_INCREMENT_BUILD_VERSION, value);
+ }
+
+ ///
+ /// The location in Assets of the 3D app launcher model for an AppX build.
+ ///
+ /// See 3D app launcher design guidance for more information.
+ public static string AppLauncherModelLocation
+ {
+ get => ProjectPreferences.Get(EDITOR_PREF_3D_APP_LAUNCHER_MODEL_LOCATION, string.Empty);
+ set => ProjectPreferences.Set(EDITOR_PREF_3D_APP_LAUNCHER_MODEL_LOCATION, value);
+ }
+ }
+}
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs.meta
new file mode 100644
index 00000000000..c1ddd5c3e40
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 4d89548688b44f84850275a398d097c8
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs
new file mode 100644
index 00000000000..70e3e1a5a24
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEditor.Build.Reporting;
+using UnityEngine;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ public class BuildInfo : IBuildInfo
+ {
+ public BuildInfo(bool isCommandLine = false)
+ {
+ IsCommandLine = isCommandLine;
+ BuildSymbols = string.Empty;
+ BuildTarget = EditorUserBuildSettings.activeBuildTarget;
+ Scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(scene => scene.path);
+ }
+
+ ///
+ public virtual BuildTarget BuildTarget { get; }
+
+ ///
+ public bool IsCommandLine { get; }
+
+ private string outputDirectory;
+
+ ///
+ public string OutputDirectory
+ {
+ get => string.IsNullOrEmpty(outputDirectory) ? outputDirectory = BuildDeployPreferences.BuildDirectory : outputDirectory;
+ set => outputDirectory = value;
+ }
+
+ ///
+ public IEnumerable Scenes { get; set; }
+
+ ///
+ public Action PreBuildAction { get; set; }
+
+ ///
+ public Action PostBuildAction { get; set; }
+
+ ///
+ public BuildOptions BuildOptions { get; set; }
+
+ ///
+ public ColorSpace? ColorSpace { get; set; }
+
+ ///
+ public ScriptingImplementation? ScriptingBackend { get; set; }
+
+ ///
+ public bool AutoIncrement { get; set; } = false;
+
+ ///
+ public string BuildSymbols { get; set; }
+
+ ///
+ public string BuildPlatform { get; set; }
+
+ ///
+ public string Configuration
+ {
+ get
+ {
+ if (!this.HasConfigurationSymbol())
+ {
+ return UnityPlayerBuildTools.BuildSymbolMaster;
+ }
+
+ return this.HasAnySymbols(UnityPlayerBuildTools.BuildSymbolDebug)
+ ? UnityPlayerBuildTools.BuildSymbolDebug
+ : this.HasAnySymbols(UnityPlayerBuildTools.BuildSymbolRelease)
+ ? UnityPlayerBuildTools.BuildSymbolRelease
+ : UnityPlayerBuildTools.BuildSymbolMaster;
+ }
+ set
+ {
+ if (this.HasConfigurationSymbol())
+ {
+ this.RemoveSymbols(new[]
+ {
+ UnityPlayerBuildTools.BuildSymbolDebug,
+ UnityPlayerBuildTools.BuildSymbolRelease,
+ UnityPlayerBuildTools.BuildSymbolMaster
+ });
+ }
+
+ this.AppendSymbols(value);
+ }
+ }
+
+ ///
+ public string LogDirectory { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs.meta
new file mode 100644
index 00000000000..f4a5228d341
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b23ec68cb2414db4ae5ddb5f0961e041
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs
new file mode 100644
index 00000000000..7fd2ae461af
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ public static class BuildInfoExtensions
+ {
+ ///
+ /// Append symbols to the end of the 's.
+ ///
+ /// The string array to append.
+ public static void AppendSymbols(this IBuildInfo buildInfo, params string[] symbol)
+ {
+ buildInfo.AppendSymbols((IEnumerable)symbol);
+ }
+
+ ///
+ /// Append symbols to the end of the 's .
+ ///
+ /// The string collection to append.
+ public static void AppendSymbols(this IBuildInfo buildInfo, IEnumerable symbols)
+ {
+ string[] toAdd = symbols.Except(buildInfo.BuildSymbols.Split(';'))
+ .Where(symbol => !string.IsNullOrEmpty(symbol)).ToArray();
+
+ if (!toAdd.Any())
+ {
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(buildInfo.BuildSymbols))
+ {
+ buildInfo.BuildSymbols += ";";
+ }
+
+ buildInfo.BuildSymbols += string.Join(";", toAdd);
+ }
+
+ ///
+ /// Remove symbols from the 's .
+ ///
+ /// The string collection to remove.
+ public static void RemoveSymbols(this IBuildInfo buildInfo, IEnumerable symbolsToRemove)
+ {
+ string[] toKeep = buildInfo.BuildSymbols.Split(';').Except(symbolsToRemove).ToArray();
+
+ if (!toKeep.Any())
+ {
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(buildInfo.BuildSymbols))
+ {
+ buildInfo.BuildSymbols = string.Empty;
+ }
+
+ buildInfo.BuildSymbols += string.Join(";", toKeep);
+ }
+
+ ///
+ /// Does the contain any of the provided symbols in the ?
+ ///
+ /// The string array of symbols to match.
+ /// True, if any of the provided symbols are in the
+ public static bool HasAnySymbols(this IBuildInfo buildInfo, params string[] symbols)
+ {
+ if (string.IsNullOrEmpty(buildInfo.BuildSymbols)) { return false; }
+
+ return buildInfo.BuildSymbols.Split(';').Intersect(symbols).Any();
+ }
+
+ ///
+ /// Does the contain any of the provided symbols in the ?
+ ///
+ /// The string collection of symbols to match.
+ /// True, if any of the provided symbols are in the
+ public static bool HasAnySymbols(this IBuildInfo buildInfo, IEnumerable symbols)
+ {
+ if (string.IsNullOrEmpty(buildInfo.BuildSymbols)) { return false; }
+
+ return buildInfo.BuildSymbols.Split(';').Intersect(symbols).Any();
+ }
+
+ ///
+ /// Checks if the has any configuration symbols (i.e. debug, release, or master).
+ ///
+ /// True, if the contains debug, release, or master.
+ public static bool HasConfigurationSymbol(this IBuildInfo buildInfo)
+ {
+ return buildInfo.HasAnySymbols(
+ UnityPlayerBuildTools.BuildSymbolDebug,
+ UnityPlayerBuildTools.BuildSymbolRelease,
+ UnityPlayerBuildTools.BuildSymbolMaster);
+ }
+
+ ///
+ /// Appends the 's without including debug, release or master.
+ ///
+ /// Symbols to append.
+ public static void AppendWithoutConfigurationSymbols(this IBuildInfo buildInfo, string symbols)
+ {
+ buildInfo.AppendSymbols(symbols.Split(';').Except(new[]
+ {
+ UnityPlayerBuildTools.BuildSymbolDebug,
+ UnityPlayerBuildTools.BuildSymbolRelease,
+ UnityPlayerBuildTools.BuildSymbolMaster
+ }).ToArray());
+ }
+
+ ///
+ /// Gets the BuildTargetGroup for the 's BuildTarget
+ ///
+ /// The BuildTargetGroup for the 's BuildTarget
+ public static BuildTargetGroup GetGroup(this BuildTarget buildTarget)
+ {
+ switch (buildTarget)
+ {
+ case BuildTarget.WSAPlayer:
+ return BuildTargetGroup.WSA;
+ case BuildTarget.StandaloneWindows:
+ case BuildTarget.StandaloneWindows64:
+ return BuildTargetGroup.Standalone;
+ default:
+ return BuildTargetGroup.Unknown;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs.meta
new file mode 100644
index 00000000000..ba0ecab2b3f
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e365115c037641efb578a2cc713156ab
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs
new file mode 100644
index 00000000000..bad5de5ea07
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEditor.Build.Reporting;
+using UnityEngine;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ ///
+ /// The Build Info defines common properties for a build.
+ ///
+ public interface IBuildInfo
+ {
+ ///
+ /// Is this build being issued from the command line?
+ ///
+ bool IsCommandLine { get; }
+
+ ///
+ /// The directory to put the final build output.
+ ///
+ ///
+ /// Defaults to "Application.dataPath/Builds/Platform Target/"
+ ///
+ string OutputDirectory { get; set; }
+
+ ///
+ /// The list of scenes to include in the build.
+ ///
+ IEnumerable Scenes { get; set; }
+
+ ///
+ /// A pre-build action to raise before building the Unity player.
+ ///
+ Action PreBuildAction { get; set; }
+
+ ///
+ /// A post-build action to raise after building the Unity player.
+ ///
+ Action PostBuildAction { get; set; }
+
+ ///
+ /// Build options to include in the Unity player build pipeline.
+ ///
+ BuildOptions BuildOptions { get; set; }
+
+ ///
+ /// The build target.
+ ///
+ BuildTarget BuildTarget { get; }
+
+ ///
+ /// Optional parameter to set the player's
+ ///
+ ColorSpace? ColorSpace { get; set; }
+
+ ///
+ /// Optional parameter to set the scripting backend
+ ///
+ ScriptingImplementation? ScriptingBackend { get; set; }
+
+ ///
+ /// Should the build auto increment the build version number?
+ ///
+ bool AutoIncrement { get; set; }
+
+ ///
+ /// The symbols associated with this build.
+ ///
+ string BuildSymbols { get; set; }
+
+ ///
+ /// The build configuration (i.e. debug, release, or master)
+ ///
+ string Configuration { get; set; }
+
+ ///
+ /// The build platform (i.e. x86, x64)
+ ///
+ string BuildPlatform { get; set; }
+
+ ///
+ /// The default location of log files generated by sub-processes of the build system.
+ ///
+ ///
+ /// Note that this different from the Unity flag -logFile, which controls the location
+ /// of the Unity log file. This is specifically for logs generated by other processes
+ /// that the MRTK build tools produces (for example, when msbuild.exe is involved)
+ ///
+ string LogDirectory { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs.meta
new file mode 100644
index 00000000000..a3b2989fe92
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 886f0edadb5c41269107dd3159a7a065
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs
new file mode 100644
index 00000000000..594edd93451
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs
@@ -0,0 +1,207 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using UnityEditor;
+using UnityEditor.Build;
+using UnityEditor.Build.Reporting;
+using UnityEngine;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ using GLTFast;
+
+ ///
+ /// Settings provider for build-specific settings, like the 3D app launcher model for Windows builds.
+ ///
+ public class MixedRealityBuildPreferences : IPreprocessBuildWithReport, IPostprocessBuildWithReport
+ {
+ private const string AppLauncherPath = @"Assets\AppLauncherModel.glb";
+ private static readonly GUIContent AppLauncherModelLabel = new GUIContent("3D App Launcher Model", "Location of .glb model to use as a 3D App Launcher");
+ private static UnityEditor.Editor gameObjectEditor = null;
+ private static GUIStyle appLauncherPreviewBackgroundColor = null;
+ private static bool isBuilding = false;
+
+ // Arbitrary callback order, chosen to be larger so that it runs after other things that
+ // a developer may have already.
+ int IOrderedCallback.callbackOrder => 100;
+
+ [SettingsProvider]
+ private static SettingsProvider BuildPreferences()
+ {
+ var provider = new SettingsProvider("Project/Mixed Reality Toolkit/Build Settings", SettingsScope.Project)
+ {
+ guiHandler = GUIHandler,
+
+ keywords = new HashSet(new[] { "Mixed", "Reality", "Toolkit", "Build" })
+ };
+
+ void GUIHandler(string searchContext)
+ {
+ EditorGUILayout.HelpBox("These settings are serialized into ProjectPreferences.asset in the MixedRealityToolkit-Generated folder.\nThis file can be checked into source control to maintain consistent settings across collaborators.", MessageType.Info);
+ DrawAppLauncherModelField();
+ }
+
+ return provider;
+ }
+
+ ///
+ /// Helper script for rendering an object field to set the 3D app launcher model in an editor window.
+ ///
+ /// See 3D app launcher design guidance for more information.
+ public static void DrawAppLauncherModelField(bool showInteractivePreview = true)
+ {
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ GltfAsset newGlbModel;
+ bool appLauncherChanged = false;
+
+ // 3D launcher model
+ string curAppLauncherModelLocation = BuildDeployPreferences.AppLauncherModelLocation;
+ var curGlbModel = AssetDatabase.LoadAssetAtPath(curAppLauncherModelLocation, typeof(GltfAsset));
+
+ using (new EditorGUILayout.VerticalScope())
+ {
+ EditorGUILayout.LabelField(AppLauncherModelLabel);
+ newGlbModel = EditorGUILayout.ObjectField(curGlbModel, typeof(GltfAsset), false, GUILayout.MaxWidth(256)) as GltfAsset;
+ string newAppLauncherModelLocation = AssetDatabase.GetAssetPath(newGlbModel);
+ if (newAppLauncherModelLocation != curAppLauncherModelLocation)
+ {
+ BuildDeployPreferences.AppLauncherModelLocation = newAppLauncherModelLocation;
+ appLauncherChanged = true;
+ }
+ }
+
+ // The preview GUI has a problem during the build, so we don't render it
+ if (newGlbModel != null && newGlbModel.sceneInstance != null && showInteractivePreview && !isBuilding)
+ {
+ if (gameObjectEditor == null || appLauncherChanged)
+ {
+ gameObjectEditor = UnityEditor.Editor.CreateEditor(newGlbModel.gameObject);
+ }
+
+ if (appLauncherPreviewBackgroundColor == null)
+ {
+ appLauncherPreviewBackgroundColor = new GUIStyle();
+ appLauncherPreviewBackgroundColor.normal.background = EditorGUIUtility.whiteTexture;
+ }
+
+ gameObjectEditor.OnInteractivePreviewGUI(GUILayoutUtility.GetRect(128, 128), appLauncherPreviewBackgroundColor);
+ }
+ }
+ }
+
+ void IPreprocessBuildWithReport.OnPreprocessBuild(BuildReport report)
+ {
+ if (report.summary.platformGroup == BuildTargetGroup.WSA && !string.IsNullOrEmpty(BuildDeployPreferences.AppLauncherModelLocation))
+ {
+ isBuilding = true;
+ // Sets the editor to null. On a build, Unity reloads the object preview
+ // in a seemingly unexpected way, so it starts rendering a null texture.
+ // This refreshes the preview window instead.
+ gameObjectEditor = null;
+ }
+ }
+
+ void IPostprocessBuildWithReport.OnPostprocessBuild(BuildReport report)
+ {
+ if (report.summary.platformGroup == BuildTargetGroup.WSA && !string.IsNullOrEmpty(BuildDeployPreferences.AppLauncherModelLocation))
+ {
+ string appxPath = $"{report.summary.outputPath}/{PlayerSettings.productName}";
+
+ Debug.Log($"3D App Launcher: {BuildDeployPreferences.AppLauncherModelLocation}, Destination: {appxPath}/{AppLauncherPath}");
+
+ FileUtil.ReplaceFile(BuildDeployPreferences.AppLauncherModelLocation, $"{appxPath}/{AppLauncherPath}");
+ AddAppLauncherModelToProject($"{appxPath}/{PlayerSettings.productName}.vcxproj");
+ AddAppLauncherModelToFilter($"{appxPath}/{PlayerSettings.productName}.vcxproj.filters");
+ UpdateManifest($"{appxPath}/Package.appxmanifest");
+
+ isBuilding = false;
+ }
+ }
+
+ private static void AddAppLauncherModelToProject(string filePath)
+ {
+ var text = File.ReadAllText(filePath);
+ var doc = new XmlDocument();
+ doc.LoadXml(text);
+ var root = doc.DocumentElement;
+
+ // Check to see if model has already been added
+ XmlNodeList nodes = root.SelectNodes($"//None[@Include = \"{AppLauncherPath}\"]");
+ if (nodes.Count > 0)
+ {
+ return;
+ }
+
+ var newNodeDoc = new XmlDocument();
+ newNodeDoc.LoadXml($"" +
+ "true" +
+ "");
+ var newNode = doc.ImportNode(newNodeDoc.DocumentElement, true);
+ var list = doc.GetElementsByTagName("ItemGroup");
+ var items = list.Item(1);
+ items.AppendChild(newNode);
+ doc.Save(filePath);
+ }
+
+ private static void AddAppLauncherModelToFilter(string filePath)
+ {
+ var text = File.ReadAllText(filePath);
+ var doc = new XmlDocument();
+ doc.LoadXml(text);
+ var root = doc.DocumentElement;
+
+ // Check to see if model has already been added
+ XmlNodeList nodes = root.SelectNodes($"//None[@Include = \"{AppLauncherPath}\"]");
+ if (nodes.Count > 0)
+ {
+ return;
+ }
+
+ var newNodeDoc = new XmlDocument();
+ newNodeDoc.LoadXml($"" +
+ "Assets" +
+ "");
+ var newNode = doc.ImportNode(newNodeDoc.DocumentElement, true);
+ var list = doc.GetElementsByTagName("ItemGroup");
+ var items = list.Item(0);
+ items.AppendChild(newNode);
+ doc.Save(filePath);
+ }
+
+ private static void UpdateManifest(string filePath)
+ {
+ var text = File.ReadAllText(filePath);
+ var doc = new XmlDocument();
+ doc.LoadXml(text);
+ var root = doc.DocumentElement;
+
+ // Check to see if the element exists already
+ XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
+ nsmgr.AddNamespace("uap5", "http://schemas.microsoft.com/appx/manifest/uap/windows10/5");
+ XmlNodeList nodes = root.SelectNodes("//uap5:MixedRealityModel", nsmgr);
+ foreach (XmlNode node in nodes)
+ {
+ if (node.Attributes != null && node.Attributes["Path"].Value == AppLauncherPath)
+ {
+ return;
+ }
+ }
+ root.SetAttribute("xmlns:uap5", "http://schemas.microsoft.com/appx/manifest/uap/windows10/5");
+
+ var ignoredValue = root.GetAttribute("IgnorableNamespaces");
+ root.SetAttribute("IgnorableNamespaces", ignoredValue + " uap5");
+
+ var newElement = doc.CreateElement("uap5", "MixedRealityModel", "http://schemas.microsoft.com/appx/manifest/uap/windows10/5");
+ newElement.SetAttribute("Path", AppLauncherPath);
+ var list = doc.GetElementsByTagName("uap:DefaultTile");
+ var items = list.Item(0);
+ items.AppendChild(newElement);
+
+ doc.Save(filePath);
+ }
+ }
+}
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs.meta
new file mode 100644
index 00000000000..419cc5ef032
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 6de56e5071ad49b297f4bcbd5194fe14
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs
new file mode 100644
index 00000000000..31441cbb338
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs
@@ -0,0 +1,312 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.MixedReality.Toolkit.Utilities.Editor;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using UnityEditor;
+using UnityEditor.Build.Reporting;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ ///
+ /// Cross platform player build tools
+ ///
+ public static class UnityPlayerBuildTools
+ {
+ // Build configurations. Exactly one of these should be defined for any given build.
+ public const string BuildSymbolDebug = "debug";
+ public const string BuildSymbolRelease = "release";
+ public const string BuildSymbolMaster = "master";
+
+ ///
+ /// Starts the build process
+ ///
+ /// The BuildReport from Unity's BuildPipeline
+ public static BuildReport BuildUnityPlayer(IBuildInfo buildInfo)
+ {
+ EditorUtility.DisplayProgressBar("Build Pipeline", "Gathering Build Data...", 0.25f);
+
+ // Call the pre-build action, if any
+ buildInfo.PreBuildAction?.Invoke(buildInfo);
+
+ BuildTargetGroup buildTargetGroup = buildInfo.BuildTarget.GetGroup();
+ string playerBuildSymbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
+
+ if (!string.IsNullOrEmpty(playerBuildSymbols))
+ {
+ if (buildInfo.HasConfigurationSymbol())
+ {
+ buildInfo.AppendWithoutConfigurationSymbols(playerBuildSymbols);
+ }
+ else
+ {
+ buildInfo.AppendSymbols(playerBuildSymbols.Split(';'));
+ }
+ }
+
+ if (!string.IsNullOrEmpty(buildInfo.BuildSymbols))
+ {
+ PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, buildInfo.BuildSymbols);
+ }
+
+ if ((buildInfo.BuildOptions & BuildOptions.Development) == BuildOptions.Development &&
+ !buildInfo.HasConfigurationSymbol())
+ {
+ buildInfo.AppendSymbols(BuildSymbolDebug);
+ }
+
+ if (buildInfo.HasAnySymbols(BuildSymbolDebug))
+ {
+ buildInfo.BuildOptions |= BuildOptions.Development | BuildOptions.AllowDebugging;
+ }
+
+ if (buildInfo.HasAnySymbols(BuildSymbolRelease))
+ {
+ // Unity automatically adds the DEBUG symbol if the BuildOptions.Development flag is
+ // specified. In order to have debug symbols and the RELEASE symbols we have to
+ // inject the symbol Unity relies on to enable the /debug+ flag of csc.exe which is "DEVELOPMENT_BUILD"
+ buildInfo.AppendSymbols("DEVELOPMENT_BUILD");
+ }
+
+ var oldColorSpace = PlayerSettings.colorSpace;
+
+ if (buildInfo.ColorSpace.HasValue)
+ {
+ PlayerSettings.colorSpace = buildInfo.ColorSpace.Value;
+ }
+
+ if (buildInfo.ScriptingBackend.HasValue)
+ {
+ PlayerSettings.SetScriptingBackend(buildTargetGroup, buildInfo.ScriptingBackend.Value);
+ }
+
+ BuildTarget oldBuildTarget = EditorUserBuildSettings.activeBuildTarget;
+ BuildTargetGroup oldBuildTargetGroup = oldBuildTarget.GetGroup();
+
+ if (EditorUserBuildSettings.activeBuildTarget != buildInfo.BuildTarget)
+ {
+ EditorUserBuildSettings.SwitchActiveBuildTarget(buildTargetGroup, buildInfo.BuildTarget);
+ }
+
+ switch (buildInfo.BuildTarget)
+ {
+ case BuildTarget.Android:
+ buildInfo.OutputDirectory = $"{buildInfo.OutputDirectory}/{PlayerSettings.productName}.apk";
+ break;
+ case BuildTarget.StandaloneWindows:
+ case BuildTarget.StandaloneWindows64:
+ buildInfo.OutputDirectory = $"{buildInfo.OutputDirectory}/{PlayerSettings.productName}.exe";
+ break;
+ }
+
+ BuildReport buildReport = default;
+
+ try
+ {
+ buildReport = BuildPipeline.BuildPlayer(
+ buildInfo.Scenes.ToArray(),
+ buildInfo.OutputDirectory,
+ buildInfo.BuildTarget,
+ buildInfo.BuildOptions);
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"{e.Message}\n{e.StackTrace}");
+ }
+
+ PlayerSettings.colorSpace = oldColorSpace;
+
+ if (EditorUserBuildSettings.activeBuildTarget != oldBuildTarget)
+ {
+ EditorUserBuildSettings.SwitchActiveBuildTarget(oldBuildTargetGroup, oldBuildTarget);
+ }
+
+ // Call the post-build action, if any
+ buildInfo.PostBuildAction?.Invoke(buildInfo, buildReport);
+
+ EditorUtility.ClearProgressBar();
+
+ return buildReport;
+ }
+
+ ///
+ /// Force Unity To Write Project Files
+ ///
+ public static void SyncSolution()
+ {
+ var syncVs = Type.GetType("UnityEditor.SyncVS,UnityEditor");
+ var syncSolution = syncVs.GetMethod("SyncSolution", BindingFlags.Public | BindingFlags.Static);
+ syncSolution.Invoke(null, null);
+ }
+
+ ///
+ /// Start a build using Unity's command line.
+ ///
+ public static async void StartCommandLineBuild()
+ {
+ var success = await BuildUnityPlayerSimplified();
+ Debug.Log($"Exiting build...");
+ EditorApplication.Exit(success ? 0 : 1);
+ }
+
+ public static async Task BuildUnityPlayerSimplified()
+ {
+ // We don't need stack traces on all our logs. Makes things a lot easier to read.
+ Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None);
+ Debug.Log($"Starting command line build for {EditorUserBuildSettings.activeBuildTarget}...");
+ EditorAssemblyReloadManager.LockReloadAssemblies = true;
+
+ bool success;
+ try
+ {
+ SyncSolution();
+ switch (EditorUserBuildSettings.activeBuildTarget)
+ {
+ case BuildTarget.WSAPlayer:
+ success = await UwpPlayerBuildTools.BuildPlayer(new UwpBuildInfo(true));
+ break;
+ default:
+ var buildInfo = new BuildInfo(true) as IBuildInfo;
+ ParseBuildCommandLine(ref buildInfo);
+ var buildResult = BuildUnityPlayer(buildInfo);
+ success = buildResult.summary.result == BuildResult.Succeeded;
+ break;
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"Build Failed!\n{e.Message}\n{e.StackTrace}");
+ success = false;
+ }
+
+ Debug.Log($"Finished build... Build success? {success}");
+ return success;
+ }
+
+ internal static bool CheckBuildScenes()
+ {
+ if (EditorBuildSettings.scenes.Length == 0)
+ {
+ return EditorUtility.DisplayDialog("Attention!",
+ "No scenes are present in the build settings.\n" +
+ "The current scene will be the one built.\n\n" +
+ "Do you want to cancel and add one?",
+ "Continue Anyway", "Cancel Build");
+ }
+
+ return true;
+ }
+
+ ///
+ /// Get the Unity Project Root Path.
+ ///
+ /// The full path to the project's root.
+ public static string GetProjectPath()
+ {
+ return Path.GetDirectoryName(Path.GetFullPath(Application.dataPath));
+ }
+
+ public static void ParseBuildCommandLine(ref IBuildInfo buildInfo)
+ {
+ string[] arguments = Environment.GetCommandLineArgs();
+
+ // Boolean used to track whether builfInfo contains scenes that are not specified by command line arguments.
+ // These non command line arugment scenes should be overwritten by those specified in the command line.
+ bool buildInfoContainsNonCommandLineScene = buildInfo.Scenes.Count() > 0;
+
+ for (int i = 0; i < arguments.Length; ++i)
+ {
+ switch (arguments[i])
+ {
+ case "-autoIncrement":
+ buildInfo.AutoIncrement = true;
+ break;
+ case "-sceneList":
+ if (buildInfoContainsNonCommandLineScene)
+ {
+ buildInfo.Scenes = SplitSceneList(arguments[++i]);
+ buildInfoContainsNonCommandLineScene = false;
+ }
+ else
+ {
+ buildInfo.Scenes = buildInfo.Scenes.Union(SplitSceneList(arguments[++i]));
+ }
+ break;
+ case "-sceneListFile":
+ string path = arguments[++i];
+ if (File.Exists(path))
+ {
+ if (buildInfoContainsNonCommandLineScene)
+ {
+ buildInfo.Scenes = SplitSceneList(File.ReadAllText(path));
+ buildInfoContainsNonCommandLineScene = false;
+ }
+ else
+ {
+ buildInfo.Scenes = buildInfo.Scenes.Union(SplitSceneList(File.ReadAllText(path)));
+ }
+ }
+ else
+ {
+ Debug.LogWarning($"Scene list file at '{path}' does not exist.");
+ }
+ break;
+ case "-buildOutput":
+ buildInfo.OutputDirectory = arguments[++i];
+ break;
+ case "-colorSpace":
+ buildInfo.ColorSpace = (ColorSpace)Enum.Parse(typeof(ColorSpace), arguments[++i]);
+ break;
+ case "-scriptingBackend":
+ buildInfo.ScriptingBackend = (ScriptingImplementation)Enum.Parse(typeof(ScriptingImplementation), arguments[++i]);
+ break;
+ case "-x86":
+ case "-x64":
+ case "-arm":
+ case "-arm64":
+ buildInfo.BuildPlatform = arguments[i].Substring(1);
+ break;
+ case "-debug":
+ case "-master":
+ case "-release":
+ buildInfo.Configuration = arguments[i].Substring(1).ToLower();
+ break;
+ case "-logDirectory":
+ buildInfo.LogDirectory = arguments[++i];
+ break;
+ }
+ }
+ }
+
+ private static IEnumerable SplitSceneList(string sceneList)
+ {
+ return from scene in sceneList.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ select scene.Trim();
+ }
+
+ ///
+ /// Restores any nuget packages at the path specified.
+ ///
+ /// True, if the nuget packages were successfully restored.
+ public static async Task RestoreNugetPackagesAsync(string nugetPath, string storePath)
+ {
+ Debug.Assert(File.Exists(nugetPath));
+ Debug.Assert(Directory.Exists(storePath));
+
+ string projectJSONPath = Path.Combine(storePath, "project.json");
+ string projectJSONLockPath = Path.Combine(storePath, "project.lock.json");
+
+ await new Process().StartProcessAsync(nugetPath, $"restore \"{projectJSONPath}\"");
+
+ return File.Exists(projectJSONLockPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs.meta
new file mode 100644
index 00000000000..c5ee311a389
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 828a69fe27da4beca840ed94a6d2f6e5
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs
new file mode 100644
index 00000000000..b59f1da3d13
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs
@@ -0,0 +1,706 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.MixedReality.Toolkit.Utilities.Editor;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using UnityEditor;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ public static class UwpAppxBuildTools
+ {
+ ///
+ /// Query the build process to see if we're already building.
+ ///
+ public static bool IsBuilding { get; private set; } = false;
+
+ ///
+ /// The list of filename extensions that are valid VCProjects.
+ ///
+ private static readonly string[] VcProjExtensions = { "vcsproj", "vcxproj" };
+
+ ///
+ /// Build the UWP appx bundle for this project. Requires that has already be run or a user has
+ /// previously built the Unity Player with the WSA Player as the Build Target.
+ ///
+ /// True, if the appx build was successful.
+ public static async Task BuildAppxAsync(UwpBuildInfo buildInfo, CancellationToken cancellationToken = default)
+ {
+ if (!EditorAssemblyReloadManager.LockReloadAssemblies)
+ {
+ Debug.LogError("Lock Reload assemblies before attempting to build appx!");
+ return false;
+ }
+
+ if (IsBuilding)
+ {
+ Debug.LogWarning("Build already in progress!");
+ return false;
+ }
+
+ if (Application.isBatchMode)
+ {
+ // We don't need stack traces on all our logs. Makes things a lot easier to read.
+ Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None);
+ }
+
+ Debug.Log("Starting Unity Appx Build...");
+
+ IsBuilding = true;
+ string slnFilename = Path.Combine(buildInfo.OutputDirectory, $"{PlayerSettings.productName}.sln");
+
+ if (!File.Exists(slnFilename))
+ {
+ Debug.LogError("Unable to find Solution to build from!");
+ return IsBuilding = false;
+ }
+
+ // Get and validate the msBuild path...
+ var msBuildPath = await FindMsBuildPathAsync();
+
+ if (!File.Exists(msBuildPath))
+ {
+ Debug.LogError($"MSBuild.exe is missing or invalid!\n{msBuildPath}");
+ return IsBuilding = false;
+ }
+
+ // Ensure that the generated .appx version increments by modifying Package.appxmanifest
+ try
+ {
+ if (!UpdateAppxManifest(buildInfo))
+ {
+ throw new Exception();
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"Failed to update appxmanifest!\n{e.Message}");
+ return IsBuilding = false;
+ }
+
+ string storagePath = Path.GetFullPath(Path.Combine(Path.Combine(Application.dataPath, ".."), buildInfo.OutputDirectory));
+ string solutionProjectPath = Path.GetFullPath(Path.Combine(storagePath, $@"{PlayerSettings.productName}.sln"));
+
+ int exitCode;
+
+ // Building the solution requires first restoring NuGet packages - when built through
+ // Visual Studio, VS does this automatically - when building via msbuild like we're doing here,
+ // we have to do that step manually.
+ // We use msbuild for nuget restore by default, but if a path to nuget.exe is supplied then we use that executable
+ if (string.IsNullOrEmpty(buildInfo.NugetExecutablePath))
+ {
+ exitCode = await Run(msBuildPath,
+ $"\"{solutionProjectPath}\" /t:restore {GetMSBuildLoggingCommand(buildInfo.LogDirectory, "nugetRestore.log")}",
+ !Application.isBatchMode,
+ cancellationToken);
+ }
+ else
+ {
+ exitCode = await Run(buildInfo.NugetExecutablePath,
+ $"restore \"{solutionProjectPath}\"",
+ !Application.isBatchMode,
+ cancellationToken);
+ }
+
+ if (exitCode != 0)
+ {
+ IsBuilding = false;
+ return false;
+ }
+
+ // Need to add ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch to MixedRealityToolkit.vcxproj
+ if (buildInfo.BuildPlatform == "arm64")
+ {
+ if (!UpdateVSProj(buildInfo))
+ {
+ return IsBuilding = false;
+ }
+ }
+
+ // Now that NuGet packages have been restored, we can run the actual build process.
+ exitCode = await Run(msBuildPath,
+ $"\"{solutionProjectPath}\" {(buildInfo.Multicore ? "/m /nr:false" : "")} /t:{(buildInfo.RebuildAppx ? "Rebuild" : "Build")} /p:Configuration={buildInfo.Configuration} /p:Platform={buildInfo.BuildPlatform} {(string.IsNullOrEmpty(buildInfo.PlatformToolset) ? string.Empty : $"/p:PlatformToolset={buildInfo.PlatformToolset}")} {GetMSBuildLoggingCommand(buildInfo.LogDirectory, "buildAppx.log")}",
+ !Application.isBatchMode,
+ cancellationToken);
+ AssetDatabase.SaveAssets();
+
+ IsBuilding = false;
+ return exitCode == 0;
+ }
+
+ private static async Task Run(string fileName, string args, bool showDebug, CancellationToken cancellationToken)
+ {
+ Debug.Log($"Running command: {fileName} {args}");
+
+ var processResult = await new Process().StartProcessAsync(
+ fileName, args, !Application.isBatchMode, cancellationToken);
+
+ switch (processResult.ExitCode)
+ {
+ case 0:
+ Debug.Log($"Command successful");
+
+ if (Application.isBatchMode)
+ {
+ Debug.Log(string.Join("\n", processResult.Output));
+ }
+ break;
+ case -1073741510:
+ Debug.LogWarning("The build was terminated either by user's keyboard input CTRL+C or CTRL+Break or closing command prompt window.");
+ break;
+ default:
+ {
+ if (processResult.ExitCode != 0)
+ {
+ Debug.Log($"Command failed, errorCode: {processResult.ExitCode}");
+
+ if (Application.isBatchMode)
+ {
+ var output = "Command output:\n";
+
+ foreach (var message in processResult.Output)
+ {
+ output += $"{message}\n";
+ }
+
+ output += "Command errors:";
+
+ foreach (var error in processResult.Errors)
+ {
+ output += $"{error}\n";
+ }
+
+ Debug.LogError(output);
+ }
+ }
+ break;
+ }
+ }
+ return processResult.ExitCode;
+ }
+
+ private static async Task FindMsBuildPathAsync()
+ {
+ // Finding msbuild.exe involves different work depending on whether or not users
+ // have VS2017 or VS2019 installed.
+ foreach (VSWhereFindOption findOption in VSWhereFindOptions)
+ {
+ string arguments = findOption.arguments;
+ if (string.IsNullOrWhiteSpace(EditorUserBuildSettings.wsaUWPVisualStudioVersion))
+ {
+ arguments += " -latest";
+ }
+ else
+ {
+ // Add version number with brackets to find only the specified version
+ arguments += $" -version [{EditorUserBuildSettings.wsaUWPVisualStudioVersion}]";
+ }
+
+ var result = await new Process().StartProcessAsync(
+ new ProcessStartInfo
+ {
+ FileName = "cmd.exe",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ Arguments = arguments,
+ WorkingDirectory = @"C:\Program Files (x86)\Microsoft Visual Studio\Installer"
+ });
+
+ foreach (var path in result.Output)
+ {
+ if (!string.IsNullOrEmpty(path))
+ {
+ string[] paths = path.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+
+ if (paths.Length > 0)
+ {
+ // if there are multiple visual studio installs,
+ // prefer enterprise, then pro, then community
+ string bestPath = paths.OrderByDescending(p => p.ToLower().Contains("enterprise"))
+ .ThenByDescending(p => p.ToLower().Contains("professional"))
+ .ThenByDescending(p => p.ToLower().Contains("community")).First();
+
+ string finalPath = $@"{bestPath}{findOption.pathSuffix}";
+ if (File.Exists(finalPath))
+ {
+ return finalPath;
+ }
+ }
+ }
+ }
+ }
+
+ return string.Empty;
+ }
+
+ private static bool UpdateVSProj(IBuildInfo buildInfo)
+ {
+ // For ARM64 builds we need to add ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch
+ // to vcxproj file in order to ensure that the build passes
+ string projectFilePath = GetProjectFilePath(buildInfo);
+ if (projectFilePath == null)
+ {
+ return false;
+ }
+
+ var rootNode = XElement.Load(projectFilePath);
+ var defaultNamespace = rootNode.GetDefaultNamespace();
+ var propertyGroupNode = rootNode.Element(defaultNamespace + "PropertyGroup");
+
+ if (propertyGroupNode == null)
+ {
+ propertyGroupNode = new XElement(defaultNamespace + "PropertyGroup", new XAttribute("Label", "Globals"));
+ rootNode.Add(propertyGroupNode);
+ }
+
+ var newNode = propertyGroupNode.Element(defaultNamespace + "ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch");
+ if (newNode != null)
+ {
+ // If this setting already exists in the project, ensure its value is "None"
+ newNode.Value = "None";
+ }
+ else
+ {
+ propertyGroupNode.Add(new XElement(defaultNamespace + "ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch", "None"));
+ }
+
+ rootNode.Save(projectFilePath);
+
+ return true;
+ }
+
+ ///
+ /// Given the project name and build path, resolves the valid VcProject file (i.e. .vcsproj, vcxproj)
+ ///
+ /// A valid path if the project file exists, null otherwise
+ private static string GetProjectFilePath(IBuildInfo buildInfo)
+ {
+ string projectName = PlayerSettings.productName;
+ foreach (string extension in VcProjExtensions)
+ {
+ string projectFilePath = Path.Combine(Path.GetFullPath(buildInfo.OutputDirectory), projectName, $"{projectName}.{extension}");
+ if (File.Exists(projectFilePath))
+ {
+ return projectFilePath;
+ }
+ }
+
+ string projectDirectory = Path.Combine(Path.GetFullPath(buildInfo.OutputDirectory), projectName);
+ string combinedExtensions = String.Join("|", VcProjExtensions);
+ Debug.LogError($"Cannot find project file {projectDirectory} given names {projectName}.{combinedExtensions}");
+ return null;
+ }
+
+ private static bool UpdateAppxManifest(IBuildInfo buildInfo)
+ {
+ string manifestFilePath = GetManifestFilePath(buildInfo);
+ if (manifestFilePath == null)
+ {
+ // Error has already been logged
+ return false;
+ }
+
+ var rootNode = XElement.Load(manifestFilePath);
+ var identityNode = rootNode.Element(rootNode.GetDefaultNamespace() + "Identity");
+
+ if (identityNode == null)
+ {
+ Debug.LogError($"Package.appxmanifest for build (in path - {manifestFilePath}) is missing an node");
+ return false;
+ }
+
+ var dependencies = rootNode.Element(rootNode.GetDefaultNamespace() + "Dependencies");
+
+ if (dependencies == null)
+ {
+ Debug.LogError($"Package.appxmanifest for build (in path - {manifestFilePath}) is missing node.");
+ return false;
+ }
+
+ UpdateDependenciesElement(dependencies, rootNode.GetDefaultNamespace());
+ AddCapabilities(buildInfo, rootNode);
+
+ // We use XName.Get instead of string -> XName implicit conversion because
+ // when we pass in the string "Version", the program doesn't find the attribute.
+ // Best guess as to why this happens is that implicit string conversion doesn't set the namespace to empty
+ var versionAttr = identityNode.Attribute(XName.Get("Version"));
+
+ if (versionAttr == null)
+ {
+ Debug.LogError($"Package.appxmanifest for build (in path - {manifestFilePath}) is missing a Version attribute in the node.");
+ return false;
+ }
+
+ // Assume package version always has a '.' between each number.
+ // According to https://msdn.microsoft.com/library/windows/apps/br211441.aspx
+ // Package versions are always of the form Major.Minor.Build.Revision.
+ // Note: Revision number reserved for Windows Store, and a value other than 0 will fail WACK.
+ var version = PlayerSettings.WSA.packageVersion;
+ var newVersion = new Version(version.Major, version.Minor, buildInfo.AutoIncrement ? version.Build + 1 : version.Build, version.Revision);
+
+ PlayerSettings.WSA.packageVersion = newVersion;
+ versionAttr.Value = newVersion.ToString();
+ rootNode.Save(manifestFilePath);
+ return true;
+ }
+
+ ///
+ /// Gets the AppX manifest path in the project output directory.
+ ///
+ private static string GetManifestFilePath(IBuildInfo buildInfo)
+ {
+ var fullPathOutputDirectory = Path.GetFullPath(buildInfo.OutputDirectory);
+ Debug.Log($"Searching for appx manifest in {fullPathOutputDirectory}...");
+
+ // Find the manifest, assume the one we want is the first one
+ string[] manifests = Directory.GetFiles(fullPathOutputDirectory, "Package.appxmanifest", SearchOption.AllDirectories);
+
+ if (manifests.Length == 0)
+ {
+ Debug.LogError($"Unable to find Package.appxmanifest file for build (in path - {fullPathOutputDirectory})");
+ return null;
+ }
+
+ if (manifests.Length > 1)
+ {
+ Debug.LogWarning("Found more than one appxmanifest in the target build folder!");
+ }
+
+ return manifests[0];
+ }
+
+ ///
+ /// Updates 'Assembly-CSharp.csproj' file according to the values set in buildInfo.
+ ///
+ /// An IBuildInfo containing a valid OutputDirectory
+ /// Only used with the .NET backend in Unity 2018 or older, with Unity C# Projects enabled.
+ public static void UpdateAssemblyCSharpProject(IBuildInfo buildInfo)
+ {
+#if !UNITY_2019_1_OR_NEWER
+ if (!EditorUserBuildSettings.wsaGenerateReferenceProjects ||
+ PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) != ScriptingImplementation.WinRTDotNET)
+ {
+ // Assembly-CSharp.csproj is only generated when the above is true
+ return;
+ }
+
+ string projectFilePath = GetAssemblyCSharpProjectFilePath(buildInfo);
+ if (projectFilePath == null)
+ {
+ throw new FileNotFoundException("Unable to find 'Assembly-CSharp.csproj' file.");
+ }
+
+ var rootElement = XElement.Load(projectFilePath);
+ var uwpBuildInfo = buildInfo as UwpBuildInfo;
+ Debug.Assert(uwpBuildInfo != null);
+
+ if (uwpBuildInfo.AllowUnsafeCode)
+ {
+ AllowUnsafeCode(rootElement);
+ }
+
+ rootElement.Save(projectFilePath);
+#endif // !UNITY_2019_1_OR_NEWER
+ }
+
+ ///
+ /// Gets the 'Assembly-CSharp.csproj' files path in the project output directory.
+ ///
+ private static string GetAssemblyCSharpProjectFilePath(IBuildInfo buildInfo)
+ {
+ var fullPathOutputDirectory = Path.GetFullPath(buildInfo.OutputDirectory);
+ Debug.Log($"Searching for 'Assembly-CSharp.csproj' in {fullPathOutputDirectory}...");
+
+ // Find the manifest, assume the one we want is the first one
+ string[] manifests = Directory.GetFiles(fullPathOutputDirectory, "Assembly-CSharp.csproj", SearchOption.AllDirectories);
+
+ if (manifests.Length == 0)
+ {
+ Debug.LogError($"Unable to find 'Assembly-CSharp.csproj' file for build (in path - {fullPathOutputDirectory})");
+ return null;
+ }
+
+ if (manifests.Length > 1)
+ {
+ Debug.LogWarning("Found more than one 'Assembly-CSharp.csproj' in the target build folder!");
+ }
+
+ return manifests[0];
+ }
+
+ private static void UpdateDependenciesElement(XElement dependencies, XNamespace defaultNamespace)
+ {
+ var values = (PlayerSettings.WSATargetFamily[])Enum.GetValues(typeof(PlayerSettings.WSATargetFamily));
+
+ if (string.IsNullOrWhiteSpace(EditorUserBuildSettings.wsaUWPSDK))
+ {
+ var windowsSdkPaths = Directory.GetDirectories(@"C:\Program Files (x86)\Windows Kits\10\Lib");
+
+ for (int i = 0; i < windowsSdkPaths.Length; i++)
+ {
+ windowsSdkPaths[i] = windowsSdkPaths[i].Substring(windowsSdkPaths[i].LastIndexOf(@"\", StringComparison.Ordinal) + 1);
+ }
+
+ EditorUserBuildSettings.wsaUWPSDK = windowsSdkPaths[windowsSdkPaths.Length - 1];
+ }
+
+ string maxVersionTested = EditorUserBuildSettings.wsaUWPSDK;
+
+ if (string.IsNullOrWhiteSpace(EditorUserBuildSettings.wsaMinUWPSDK))
+ {
+ EditorUserBuildSettings.wsaMinUWPSDK = UwpBuildDeployPreferences.MIN_PLATFORM_VERSION.ToString();
+ }
+
+ string minVersion = EditorUserBuildSettings.wsaMinUWPSDK;
+
+ // Clear any we had before.
+ dependencies.RemoveAll();
+
+ foreach (PlayerSettings.WSATargetFamily family in values)
+ {
+ if (PlayerSettings.WSA.GetTargetDeviceFamily(family))
+ {
+ dependencies.Add(
+ new XElement(defaultNamespace + "TargetDeviceFamily",
+ new XAttribute("Name", $"Windows.{family}"),
+ new XAttribute("MinVersion", minVersion),
+ new XAttribute("MaxVersionTested", maxVersionTested)));
+ }
+ }
+
+ if (!dependencies.HasElements)
+ {
+ dependencies.Add(
+ new XElement(defaultNamespace + "TargetDeviceFamily",
+ new XAttribute("Name", "Windows.Universal"),
+ new XAttribute("MinVersion", minVersion),
+ new XAttribute("MaxVersionTested", maxVersionTested)));
+ }
+ }
+
+ /// Gets the subpart of the msbuild.exe command to save log information
+ /// in the given logFileName.
+ ///
+ ///
+ /// Will return an empty string if logDirectory is not set.
+ ///
+ private static string GetMSBuildLoggingCommand(string logDirectory, string logFileName)
+ {
+ if (String.IsNullOrEmpty(logDirectory))
+ {
+ Debug.Log($"Not logging {logFileName} because no logDirectory was provided");
+ return "";
+ }
+
+ return $"-fl -flp:logfile={Path.Combine(logDirectory, logFileName)};verbosity=detailed";
+ }
+
+ ///
+ /// Adds capabilities according to the values in the buildInfo to the manifest file.
+ ///
+ /// An IBuildInfo containing a valid OutputDirectory and all capabilities
+ public static void AddCapabilities(IBuildInfo buildInfo, XElement rootElement = null)
+ {
+ var manifestFilePath = GetManifestFilePath(buildInfo);
+ if (manifestFilePath == null)
+ {
+ throw new FileNotFoundException("Unable to find manifest file");
+ }
+
+ rootElement = rootElement ?? XElement.Load(manifestFilePath);
+ var uwpBuildInfo = buildInfo as UwpBuildInfo;
+
+ Debug.Assert(uwpBuildInfo != null);
+
+ // Here, ResearchModeCapability must come first, in order to avoid schema errors
+ // See https://docs.microsoft.com/windows/uwp/packaging/app-capability-declarations#restricted-capabilities
+ if (uwpBuildInfo.ResearchModeCapabilityEnabled
+#if !UNITY_2021_2_OR_NEWER
+ && EditorUserBuildSettings.wsaSubtarget == WSASubtarget.HoloLens
+#endif // !UNITY_2021_2_OR_NEWER
+ )
+ {
+ AddResearchModeCapability(rootElement);
+ }
+
+ if (uwpBuildInfo.DeviceCapabilities != null)
+ {
+ AddCapabilities(rootElement, uwpBuildInfo.DeviceCapabilities);
+ }
+ if (uwpBuildInfo.GazeInputCapabilityEnabled)
+ {
+ AddGazeInputCapability(rootElement);
+ }
+
+ rootElement.Save(manifestFilePath);
+ }
+
+ ///
+ /// Adds a capability to the given rootNode, which must be the read AppX manifest from
+ /// the build output.
+ ///
+ /// An XElement containing the AppX manifest from
+ /// the build output
+ /// The added capabilities tag as XName
+ /// Value of the Name-XAttribute of the added capability
+ public static void AddCapability(XElement rootNode, XName capability, string value)
+ {
+ // If the capabilities container tag is missing, make sure it gets added.
+ var capabilitiesTag = rootNode.GetDefaultNamespace() + "Capabilities";
+ XElement capabilitiesNode = rootNode.Element(capabilitiesTag);
+ if (capabilitiesNode == null)
+ {
+ capabilitiesNode = new XElement(capabilitiesTag);
+ rootNode.Add(capabilitiesNode);
+ }
+
+ XElement existingCapability = capabilitiesNode.Elements(capability)
+ .FirstOrDefault(element => element.Attribute("Name")?.Value == value);
+
+ // Only add the capability if it isn't there already.
+ if (existingCapability == null)
+ {
+ capabilitiesNode.Add(
+ new XElement(capability, new XAttribute("Name", value)));
+ }
+ }
+
+ ///
+ /// Adds the 'Gaze Input' capability to the manifest.
+ ///
+ ///
+ /// This is a workaround for versions of Unity which don't have native support
+ /// for the 'Gaze Input' capability in its Player Settings preference location.
+ /// Note that this function is only public to poke a hole for testing - do not
+ /// take a dependency on this function.
+ ///
+ public static void AddGazeInputCapability(XElement rootNode)
+ {
+ AddCapability(rootNode, rootNode.GetDefaultNamespace() + "DeviceCapability", "gazeInput");
+ }
+
+ ///
+ /// Adds the given capabilities to the manifest.
+ ///
+ public static void AddCapabilities(XElement rootNode, List capabilities)
+ {
+ foreach (string capability in capabilities)
+ {
+ AddCapability(rootNode, rootNode.GetDefaultNamespace() + "DeviceCapability", capability);
+ }
+ }
+
+ ///
+ /// Adds the 'Research Mode' capability to the manifest.
+ ///
+ ///
+ /// This is only for research projects and should not be used in production.
+ /// For further information take a look at https://docs.microsoft.com/windows/mixed-reality/research-mode.
+ /// Note that this function is only public to poke a hole for testing - do not
+ /// take a dependency on this function.
+ ///
+ public static void AddResearchModeCapability(XElement rootNode)
+ {
+ // Add rescap Namespace to package tag
+ XNamespace rescapNs = "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities";
+ var rescapAttribute = rootNode.Attribute(XNamespace.Xmlns + "rescap");
+ if (rescapAttribute == null)
+ {
+ rescapAttribute = new XAttribute(XNamespace.Xmlns + "rescap", rescapNs);
+ rootNode.Add(rescapAttribute);
+ }
+
+ // Add rescap to IgnorableNamespaces
+ var ignNsAttribute = rootNode.Attribute("IgnorableNamespaces");
+ if (ignNsAttribute == null)
+ {
+ ignNsAttribute = new XAttribute("IgnorableNamespaces", "rescap");
+ rootNode.Add(ignNsAttribute);
+ }
+
+ if (!ignNsAttribute.Value.Contains("rescap"))
+ {
+ ignNsAttribute.Value += " rescap";
+ }
+
+ AddCapability(rootNode, rescapNs + "Capability", "perceptionSensorsExperimental");
+ }
+
+ ///
+ /// Enables unsafe code in the generated Assembly-CSharp project.
+ ///
+ ///
+ /// This is not required by the research mode, but not using unsafe code with
+ /// direct memory access results in poor performance. So it is recommended
+ /// to use unsafe code to an extent.
+ /// For further information take a look at https://docs.microsoft.com/windows/mixed-reality/research-mode.
+ /// Note that this function is only public to poke a hole for testing - do not
+ /// take a dependency on this function.
+ ///
+ public static void AllowUnsafeCode(XElement rootNode)
+ {
+ foreach (XElement propertyGroupNode in rootNode.Descendants(rootNode.GetDefaultNamespace() + "PropertyGroup"))
+ {
+ if (propertyGroupNode.Attribute("Condition") != null)
+ {
+ var allowUnsafeBlocks = propertyGroupNode.Element(propertyGroupNode.GetDefaultNamespace() + "AllowUnsafeBlocks");
+ if (allowUnsafeBlocks == null)
+ {
+ allowUnsafeBlocks = new XElement(propertyGroupNode.GetDefaultNamespace() + "AllowUnsafeBlocks");
+ propertyGroupNode.Add(allowUnsafeBlocks);
+ }
+ allowUnsafeBlocks.Value = "true";
+ }
+ }
+ }
+
+ ///
+ /// This struct controls the behavior of the arguments that are used
+ /// when finding msbuild.exe.
+ ///
+ private struct VSWhereFindOption
+ {
+ public VSWhereFindOption(string args, string suffix)
+ {
+ arguments = args;
+ pathSuffix = suffix;
+ }
+
+ ///
+ /// Used to populate the Arguments of ProcessStartInfo when invoking
+ /// vswhere.
+ ///
+ public string arguments;
+
+ ///
+ /// This string is added as a suffix to the result of the vswhere path
+ /// search.
+ ///
+ public string pathSuffix;
+ }
+
+ private static readonly VSWhereFindOption[] VSWhereFindOptions =
+ {
+ // This find option corresponds to the version of vswhere that ships with VS2019.
+ new VSWhereFindOption(
+ @"/C vswhere -all -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe",
+ ""),
+ // This find option corresponds to the version of vswhere that ships with VS2017 - this doesn't have
+ // support for the -find command switch.
+ new VSWhereFindOption(
+ @"/C vswhere -all -products * -requires Microsoft.Component.MSBuild -property installationPath",
+ "\\MSBuild\\15.0\\Bin\\MSBuild.exe"),
+ };
+ }
+}
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs.meta
new file mode 100644
index 00000000000..a995e769ae3
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 14ddd58f496c4152b9b8c8c995d89443
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs
new file mode 100644
index 00000000000..cbddf8d9d94
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs
@@ -0,0 +1,208 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.MixedReality.Toolkit.Utilities.Editor;
+using Microsoft.MixedReality.Toolkit.WindowsDevicePortal;
+using System;
+using UnityEditor;
+using UnityEngine;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ public static class UwpBuildDeployPreferences
+ {
+ ///
+ /// The minimum Windows SDK that must be present on the build machine in order
+ /// for a build to be successful.
+ ///
+ ///
+ /// This controls the version of the Windows SDK that is build against on the local
+ /// machine, NOT the version of the OS that must be present on the device that
+ /// the built application is deployed to (this other aspect is controlled by
+ /// MIN_PLATFORM_VERSION)
+ ///
+ public static Version MIN_SDK_VERSION = new Version("10.0.18362.0");
+
+ ///
+ /// The minimum version of the OS that must exist on the device that the application
+ /// is deployed to.
+ ///
+ ///
+ /// This is intentionally set to a very low version, so that the application can be
+ /// deployed to variety of different devices which may be on older OS versions.
+ ///
+ public static Version MIN_PLATFORM_VERSION = new Version("10.0.10240.0");
+
+ private const string EDITOR_PREF_BUILD_CONFIG = "BuildDeployWindow_BuildConfig";
+ private const string EDITOR_PREF_PLATFORM_TOOLSET = "BuildDeployWindow_PlatformToolset";
+ private const string EDITOR_PREF_FORCE_REBUILD = "BuildDeployWindow_ForceRebuild";
+ private const string EDITOR_PREF_CONNECT_INFOS = "BuildDeployWindow_DeviceConnections";
+ private const string EDITOR_PREF_LOCAL_CONNECT_INFO = "BuildDeployWindow_LocalConnection";
+ private const string EDITOR_PREF_FULL_REINSTALL = "BuildDeployWindow_FullReinstall";
+ private const string EDITOR_PREF_USE_SSL = "BuildDeployWindow_UseSSL";
+ private const string EDITOR_PREF_VERIFY_SSL = "BuildDeployWindow_VerifySSL";
+ private const string EDITOR_PREF_PROCESS_ALL = "BuildDeployWindow_ProcessAll";
+ private const string EDITOR_PREF_GAZE_INPUT_CAPABILITY_ENABLED = "BuildDeployWindow_GazeInputCapabilityEnabled";
+ private const string EDITOR_PREF_MULTICORE_APPX_BUILD_ENABLED = "BuildDeployWindow_MulticoreAppxBuildEnabled";
+ private const string EDITOR_PREF_RESEARCH_MODE_CAPABILITY_ENABLED = "BuildDeployWindow_ResearchModeCapabilityEnabled";
+ private const string EDITOR_PREF_ALLOW_UNSAFE_CODE = "BuildDeployWindow_AllowUnsafeCode";
+ private const string EDITOR_PREF_NUGET_EXECUTABLE_PATH = "BuildDeployWindow_NugetExecutablePath";
+
+ ///
+ /// The current Build Configuration. (Debug, Release, or Master)
+ ///
+ public static string BuildConfig
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_BUILD_CONFIG, "master");
+ set => EditorPreferences.Set(EDITOR_PREF_BUILD_CONFIG, value.ToLower());
+ }
+
+ ///
+ /// Gets the build configuration type as a WSABuildType enum
+ ///
+ public static WSABuildType BuildConfigType
+ {
+ get
+ {
+ string curBuildConfigString = BuildConfig;
+ if (curBuildConfigString.Equals("master", StringComparison.OrdinalIgnoreCase))
+ {
+ return WSABuildType.Master;
+ }
+ else if (curBuildConfigString.Equals("release", StringComparison.OrdinalIgnoreCase))
+ {
+ return WSABuildType.Release;
+ }
+ else
+ {
+ return WSABuildType.Debug;
+ }
+ }
+ }
+
+ ///
+ /// The current Platform Toolset. (Solution, v141, or v142)
+ ///
+ public static string PlatformToolset
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_PLATFORM_TOOLSET, string.Empty);
+ set => EditorPreferences.Set(EDITOR_PREF_PLATFORM_TOOLSET, value.ToLower());
+ }
+
+ ///
+ /// Current setting to force rebuilding the appx.
+ ///
+ public static bool ForceRebuild
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_FORCE_REBUILD, false);
+ set => EditorPreferences.Set(EDITOR_PREF_FORCE_REBUILD, value);
+ }
+
+ ///
+ /// Current setting to fully uninstall and reinstall the appx.
+ ///
+ public static bool FullReinstall
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_FULL_REINSTALL, true);
+ set => EditorPreferences.Set(EDITOR_PREF_FULL_REINSTALL, value);
+ }
+
+ ///
+ /// The current device portal connections.
+ ///
+ public static string DevicePortalConnections
+ {
+ get => EditorPreferences.Get(
+ EDITOR_PREF_CONNECT_INFOS,
+ JsonUtility.ToJson(
+ new DevicePortalConnections(
+ new DeviceInfo(DeviceInfo.LocalIPAddress, string.Empty, string.Empty, DeviceInfo.LocalMachine))));
+ set => EditorPreferences.Set(EDITOR_PREF_CONNECT_INFOS, value);
+ }
+
+ ///
+ /// The current device portal connections.
+ ///
+ public static string LocalConnectionInfo
+ {
+ get => EditorPreferences.Get(
+ EDITOR_PREF_LOCAL_CONNECT_INFO,
+ JsonUtility.ToJson(new DeviceInfo(DeviceInfo.LocalIPAddress, string.Empty, string.Empty, DeviceInfo.LocalMachine)));
+ set => EditorPreferences.Set(EDITOR_PREF_LOCAL_CONNECT_INFO, value);
+ }
+
+ ///
+ /// Current setting to use Single Socket Layer connections to the device portal.
+ ///
+ public static bool UseSSL
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_USE_SSL, false);
+ set => EditorPreferences.Set(EDITOR_PREF_USE_SSL, value);
+ }
+
+ ///
+ /// Current setting to verify SSL certificates for connections to the device portal.
+ ///
+ public static bool VerifySSL
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_VERIFY_SSL, true);
+ set => EditorPreferences.Set(EDITOR_PREF_VERIFY_SSL, value);
+ }
+
+ ///
+ /// Current setting to target all the devices registered to the build window.
+ ///
+ public static bool TargetAllConnections
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_PROCESS_ALL, false);
+ set => EditorPreferences.Set(EDITOR_PREF_PROCESS_ALL, value);
+ }
+
+ ///
+ /// If true, the 'Gaze Input' capability will be added to the AppX manifest
+ /// after the Unity build.
+ ///
+ public static bool GazeInputCapabilityEnabled
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_GAZE_INPUT_CAPABILITY_ENABLED, false);
+ set => EditorPreferences.Set(EDITOR_PREF_GAZE_INPUT_CAPABILITY_ENABLED, value);
+ }
+
+ ///
+ /// If true, the appx will be built with multicore support enabled in the
+ /// MSBuild process.
+ ///
+ public static bool MulticoreAppxBuildEnabled
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_MULTICORE_APPX_BUILD_ENABLED, false);
+ set => EditorPreferences.Set(EDITOR_PREF_MULTICORE_APPX_BUILD_ENABLED, value);
+ }
+
+ ///
+ /// Current setting to modify 'Package.appxmanifest' file for sensor access.
+ ///
+ public static bool ResearchModeCapabilityEnabled
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_RESEARCH_MODE_CAPABILITY_ENABLED, false);
+ set => EditorPreferences.Set(EDITOR_PREF_RESEARCH_MODE_CAPABILITY_ENABLED, value);
+ }
+
+ ///
+ /// Current setting to modify 'Assembly-CSharp.csproj' file to allow unsafe code.
+ ///
+ public static bool AllowUnsafeCode
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_ALLOW_UNSAFE_CODE, false);
+ set => EditorPreferences.Set(EDITOR_PREF_ALLOW_UNSAFE_CODE, value);
+ }
+
+ ///
+ /// Current value of the optional path to nuget.exe.
+ ///
+ public static string NugetExecutablePath
+ {
+ get => EditorPreferences.Get(EDITOR_PREF_NUGET_EXECUTABLE_PATH, string.Empty);
+ set => EditorPreferences.Set(EDITOR_PREF_NUGET_EXECUTABLE_PATH, value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs.meta
new file mode 100644
index 00000000000..96b7d1d1aa2
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8bcc29bf358d482d9d21279964cd1ebf
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs
new file mode 100644
index 00000000000..065f56998cb
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using UnityEditor;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ public class UwpBuildInfo : BuildInfo
+ {
+ public UwpBuildInfo(bool isCommandLine = false) : base(isCommandLine)
+ {
+ }
+
+ ///
+ public override BuildTarget BuildTarget => BuildTarget.WSAPlayer;
+
+ ///
+ /// Build the appx bundle after building Unity Player?
+ ///
+ public bool BuildAppx { get; set; } = false;
+
+ ///
+ /// Force rebuilding the appx bundle?
+ ///
+ public bool RebuildAppx { get; set; } = false;
+
+ ///
+ /// VC Platform Toolset used building the appx bundle
+ ///
+ public string PlatformToolset { get; set; }
+
+ ///
+ /// If true, the 'Gaze Input' capability will be added to the AppX
+ /// manifest after the Unity build.
+ ///
+ public bool GazeInputCapabilityEnabled { get; set; } = false;
+
+ ///
+ /// Use multiple cores for building the appx bundle?
+ ///
+ public bool Multicore { get; set; } = false;
+
+ ///
+ /// If true, the 'Research Mode' capability will be added to the AppX
+ /// manifest after the Unity build.
+ ///
+ public bool ResearchModeCapabilityEnabled { get; set; } = false;
+
+ ///
+ /// If true, unsafe code will be allowed in the generated
+ /// Assembly-CSharp project.
+ ///
+ public bool AllowUnsafeCode { get; set; } = false;
+
+ ///
+ /// When present, adds a DeviceCapability for each entry
+ /// in the list to the manifest
+ ///
+ public List DeviceCapabilities { get; set; } = null;
+
+ ///
+ /// Optional path to nuget.exe. Used when performing package restore with nuget.exe (instead of msbuild) is desired.
+ ///
+ public string NugetExecutablePath { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs.meta
new file mode 100644
index 00000000000..7cf130a70f2
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d2271f24e63049e69370386ffca360c7
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs
new file mode 100644
index 00000000000..fc255875ce7
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs
@@ -0,0 +1,133 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.MixedReality.Toolkit.Utilities.Editor;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using UnityEditor;
+using UnityEditor.Build.Reporting;
+using UnityEngine;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ ///
+ /// Class containing various utility methods to build a WSA solution from a Unity project.
+ ///
+ public static class UwpPlayerBuildTools
+ {
+ private static void ParseBuildCommandLine(ref UwpBuildInfo buildInfo)
+ {
+ IBuildInfo iBuildInfo = buildInfo;
+ UnityPlayerBuildTools.ParseBuildCommandLine(ref iBuildInfo);
+
+ string[] arguments = Environment.GetCommandLineArgs();
+
+ for (int i = 0; i < arguments.Length; ++i)
+ {
+ switch (arguments[i])
+ {
+ case "-buildAppx":
+ buildInfo.BuildAppx = true;
+ break;
+ case "-rebuildAppx":
+ buildInfo.RebuildAppx = true;
+ break;
+ case "-targetUwpSdk":
+ // Note: the min sdk target cannot be changed.
+ EditorUserBuildSettings.wsaUWPSDK = arguments[++i];
+ break;
+ case "-nugetPath":
+ buildInfo.NugetExecutablePath = arguments[++i];
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Do a build configured for UWP Applications to the specified path, returns the error from
+ ///
+ /// Should the user be prompted to build the appx as well?
+ /// True, if build was successful.
+ public static async Task BuildPlayer(string buildDirectory, bool showDialog = true, CancellationToken cancellationToken = default)
+ {
+ if (UnityPlayerBuildTools.CheckBuildScenes() == false)
+ {
+ return false;
+ }
+
+ var buildInfo = new UwpBuildInfo
+ {
+ OutputDirectory = buildDirectory,
+ Scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled && !string.IsNullOrEmpty(scene.path)).Select(scene => scene.path),
+ BuildAppx = !showDialog,
+ GazeInputCapabilityEnabled = UwpBuildDeployPreferences.GazeInputCapabilityEnabled,
+
+ // Configure Appx build preferences for post build action
+ RebuildAppx = UwpBuildDeployPreferences.ForceRebuild,
+ Configuration = UwpBuildDeployPreferences.BuildConfig,
+ BuildPlatform = EditorUserBuildSettings.wsaArchitecture,
+ PlatformToolset = UwpBuildDeployPreferences.PlatformToolset,
+ AutoIncrement = BuildDeployPreferences.IncrementBuildVersion,
+ Multicore = UwpBuildDeployPreferences.MulticoreAppxBuildEnabled,
+ ResearchModeCapabilityEnabled = UwpBuildDeployPreferences.ResearchModeCapabilityEnabled,
+ AllowUnsafeCode = UwpBuildDeployPreferences.AllowUnsafeCode,
+
+ // Configure a post build action that will compile the generated solution
+ PostBuildAction = PostBuildAction
+ };
+
+ async void PostBuildAction(IBuildInfo innerBuildInfo, BuildReport buildReport)
+ {
+ if (buildReport.summary.result != BuildResult.Succeeded)
+ {
+ EditorUtility.DisplayDialog($"{PlayerSettings.productName} WindowsStoreApp Build {buildReport.summary.result}!", "See console for details", "OK");
+ }
+ else
+ {
+ var uwpBuildInfo = innerBuildInfo as UwpBuildInfo;
+ Debug.Assert(uwpBuildInfo != null);
+ UwpAppxBuildTools.AddCapabilities(uwpBuildInfo);
+ UwpAppxBuildTools.UpdateAssemblyCSharpProject(uwpBuildInfo);
+
+ if (showDialog &&
+ !EditorUtility.DisplayDialog(PlayerSettings.productName, "Build Complete", "OK", "Build AppX"))
+ {
+ EditorAssemblyReloadManager.LockReloadAssemblies = true;
+ await UwpAppxBuildTools.BuildAppxAsync(uwpBuildInfo, cancellationToken);
+ EditorAssemblyReloadManager.LockReloadAssemblies = false;
+ }
+ }
+ }
+
+ return await BuildPlayer(buildInfo, cancellationToken);
+ }
+
+ ///
+ /// Build the UWP Player.
+ ///
+ public static async Task BuildPlayer(UwpBuildInfo buildInfo, CancellationToken cancellationToken = default)
+ {
+ #region Gather Build Data
+
+ if (buildInfo.IsCommandLine)
+ {
+ ParseBuildCommandLine(ref buildInfo);
+ }
+
+ #endregion Gather Build Data
+
+ BuildReport buildReport = UnityPlayerBuildTools.BuildUnityPlayer(buildInfo);
+
+ bool success = buildReport != null && buildReport.summary.result == BuildResult.Succeeded;
+
+ if (success && buildInfo.BuildAppx)
+ {
+ success &= await UwpAppxBuildTools.BuildAppxAsync(buildInfo, cancellationToken);
+ }
+
+ return success;
+ }
+ }
+}
diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs.meta
new file mode 100644
index 00000000000..e4355a64e18
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b8c458559c834d3dba6ae6320b0ab8e5
+timeCreated: 1663169360
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs b/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs
new file mode 100644
index 00000000000..89a91108a76
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs
@@ -0,0 +1,1697 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.MixedReality.Toolkit.Utilities.Editor;
+using Microsoft.MixedReality.Toolkit.WindowsDevicePortal;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using UnityEditor;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+using FileInfo = System.IO.FileInfo;
+
+namespace Microsoft.MixedReality.Toolkit.Build.Editor
+{
+ ///
+ /// Build window - supports SLN creation, APPX from SLN, Deploy on device, and misc helper utilities associated with the build/deploy/test iteration loop
+ /// Requires the device to be set in developer mode and to have secure connections disabled (in the security tab in the device portal)
+ ///
+ public class BuildDeployWindow : EditorWindow
+ {
+ #region Internal Types
+
+ private enum BuildDeployTab
+ {
+ UnityBuildOptions,
+ AppxBuildOptions,
+ DeployOptions
+ }
+
+#if UNITY_2021_2_OR_NEWER
+ ///
+ /// Matches the deprecated WSASubtarget.
+ ///
+ private enum UWPSubtarget
+ {
+ AnyDevice = 0,
+ PC = 1,
+ Mobile = 2,
+ HoloLens = 3
+ }
+#endif // UNITY_2021_2_OR_NEWER
+
+ #endregion Internal Types
+
+ #region Constants and Readonly Values
+
+ private const string UseRemoteTargetSessionKey = "DeployWindow_UseRemoteTarget";
+
+ private const string HOLOLENS_USB = "HoloLens over USB";
+
+ private const string EMPTY_IP_ADDRESS = "0.0.0.0";
+
+ private const string WifiAdapterType = "IEEE 802";
+
+ private static readonly string[] TAB_NAMES = { "Unity Build Options", "Appx Build Options", "Deploy Options" };
+
+ private static readonly string[] SCRIPTING_BACKEND_NAMES = { "IL2CPP", ".NET" };
+
+ private static readonly int[] SCRIPTING_BACKEND_ENUMS = { (int)ScriptingImplementation.IL2CPP, (int)ScriptingImplementation.WinRTDotNET };
+
+ private static readonly string[] TARGET_DEVICE_OPTIONS = { "Any Device", "PC", "Mobile", "HoloLens" };
+
+ private static readonly string[] ARCHITECTURE_OPTIONS = {
+ "x86",
+ "x64",
+ "ARM",
+ #if UNITY_2019_1_OR_NEWER
+ "ARM64"
+ #endif // UNITY_2019_1_OR_NEWER
+ };
+
+ private static readonly string[] PLATFORM_TOOLSET_VALUES = { string.Empty, "v141", "v142" };
+
+ private static readonly string[] PLATFORM_TOOLSET_NAMES = { "Solution", "v141", "v142" };
+
+ private static readonly string[] LocalRemoteOptions = { "Local", "Remote" };
+
+ private static readonly List Builds = new List(0);
+
+ private static readonly List AppPackageDirectories = new List(0);
+
+ private const string BuildWindowTabKey = "_BuildWindow_Tab";
+
+ private const string WINDOWS_10_KITS_PATH_REGISTRY_PATH = @"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
+
+ private const string WINDOWS_10_KITS_PATH_ALTERNATE_REGISTRY_PATH = @"SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots";
+
+ private const string WINDOWS_10_KITS_PATH_REGISTRY_KEY = "KitsRoot10";
+
+ private const string WINDOWS_10_KITS_PATH_POSTFIX = "Lib";
+
+ private const string WINDOWS_10_KITS_DEFAULT_PATH = @"C:\Program Files (x86)\Windows Kits\10\Lib";
+
+ // StandardAssets/Textures/MRTK_Logo_Black.png
+ private const string LogoLightThemeGuid = "fa0038d8d2df1dd4c99f346c8ec9e746";
+ // StandardAssets/Textures/MRTK_Logo_White.png
+ private const string LogoDarkThemeGuid = "fe5cc215f12ea5e40b5021c4040bce24";
+
+ private static Texture2D LogoLightTheme;
+
+ private static Texture2D LogoDarkTheme;
+
+ #endregion Constants and Readonly Values
+
+ #region Labels
+
+ private readonly GUIContent BuildAllThenInstallLabel = new GUIContent("Build all, then Install", "Builds the Unity Project, the APPX, then installs to the target device.");
+
+ private readonly GUIContent BuildAllLabel = new GUIContent("Build all", "Builds the Unity Project and APPX");
+
+ private readonly GUIContent BuildDirectoryLabel = new GUIContent("Build Directory", "It's recommended to use 'UWP'");
+
+ private readonly GUIContent UseCSharpProjectsLabel = new GUIContent("Generate C# Debug", "Generate C# Project References for debugging.\nOnly available in .NET Scripting runtime.");
+
+ private readonly GUIContent GazeInputCapabilityLabel =
+ new GUIContent("Gaze Input Capability",
+ "If checked, the 'Gaze Input' capability will be added to the AppX manifest after the Unity build.");
+
+ private readonly GUIContent AutoIncrementLabel = new GUIContent("Auto Increment", "Increases Version Build Number");
+
+ private readonly GUIContent VersionNumberLabel = new GUIContent("Version Number", "Major.Minor.Build.Revision\nNote: Revision should always be zero because it's reserved by Windows Store.");
+
+ private readonly GUIContent UseSSLLabel = new GUIContent("Use SSL?", "Use SSL to communicate with Device Portal");
+
+ private readonly GUIContent VerifySSLLabel = new GUIContent("Verify SSL Certificates?", "When using SSL for Device Portal communication, verify the SSL certificate against Root Certificates. For self-signed Device Portal certificates disabling this omits SSL rejection errors.");
+
+ private readonly GUIContent TargetTypeLabel = new GUIContent("Target Type", "Target either local connection or a remote device");
+
+ private readonly GUIContent AddConnectionLabel = new GUIContent("+", "Add a remote connection");
+
+ private readonly GUIContent RemoveConnectionLabel = new GUIContent("-", "Remove a remote connection");
+
+ private readonly GUIContent IPAddressLabel = new GUIContent("IpAddress", "IP Address for this connection to target");
+
+ private readonly GUIContent UsernameLabel = new GUIContent("Username", "Device Portal Username to supply for connections and actions");
+
+ private readonly GUIContent PasswordLabel = new GUIContent("Password", "Device Portal Password to supply for connections and actions");
+
+ private readonly GUIContent ExecuteOnAllDevicesLabel = new GUIContent("Execute action on all devices", "Should the build options perform actions on all the connected devices?");
+
+ private readonly GUIContent AlwaysUninstallLabel = new GUIContent("Always uninstall before install", "Uninstall application before installing");
+
+ private readonly GUIContent ResearchModeCapabilityLabel = new GUIContent("Enable Research Mode", "Enables research mode of HoloLens. This allows access to raw sensor data.");
+
+ private readonly GUIContent AllowUnsafeCodeLabel = new GUIContent("Allow Unsafe Code", "Modify 'Assembly-CSharp.csproj' to allow use of unsafe code. Be careful using this in production.");
+
+ private readonly GUIContent RefreshBuildsLabel = new GUIContent("Refresh Builds", "Re-scan build directory for Appx Packages that can be deployed.");
+
+ private readonly GUIContent InstallAppXLabel = new GUIContent("Install AppX", "Install listed AppX item to either currently selected device or all devices.");
+
+ private readonly GUIContent UninstallAppXLabel = new GUIContent("Uninstall AppX", "Uninstall listed AppX item to either currently selected device or all devices.");
+
+ private readonly GUIContent KillAppLabel = new GUIContent("Kill App", "Kill listed app on either currently selected device or all devices.");
+
+ private readonly GUIContent LaunchAppLabel = new GUIContent("Launch App", "Launch listed app on either currently selected device or all devices.");
+
+ private readonly GUIContent ViewPlayerLogLabel = new GUIContent("View Player Log", "Launch notepad with more recent player log for listed AppX on either currently selected device or from all devices.");
+
+ private readonly GUIContent NugetPathLabel = new GUIContent("Nuget Executable Path", "Only set this when restoring packages with nuget.exe (instead of msbuild) is desired.");
+
+ #endregion Labels
+
+ #region Properties
+
+ private static bool IsValidSdkInstalled { get; set; } = true;
+
+ private static bool ShouldOpenSLNBeEnabled => !string.IsNullOrEmpty(BuildDeployPreferences.BuildDirectory);
+
+ private static bool ShouldBuildSLNBeEnabled => !isBuilding &&
+ !UwpAppxBuildTools.IsBuilding &&
+ !BuildPipeline.isBuildingPlayer &&
+ !string.IsNullOrEmpty(BuildDeployPreferences.BuildDirectory);
+
+ private static bool ShouldBuildAppxBeEnabled => ShouldBuildSLNBeEnabled && !string.IsNullOrEmpty(BuildDeployPreferences.BuildDirectory);
+
+ private static bool DevicePortalConnectionEnabled => (portalConnections.Connections.Count > 1 || IsHoloLensConnectedUsb) &&
+ !string.IsNullOrEmpty(BuildDeployPreferences.BuildDirectory);
+
+ private static bool CanInstall
+ {
+ get
+ {
+ bool canInstall = true;
+ if (
+#if UNITY_2021_2_OR_NEWER
+ currentSubtarget == UWPSubtarget.HoloLens
+#else
+ EditorUserBuildSettings.wsaSubtarget == WSASubtarget.HoloLens
+#endif // UNITY_2021_2_OR_NEWER
+ )
+ {
+ canInstall = DevicePortalConnectionEnabled;
+ }
+
+ return canInstall && Directory.Exists(BuildDeployPreferences.AbsoluteBuildDirectory) && !string.IsNullOrEmpty(PackageName);
+ }
+ }
+
+ private static string PackageName { get; set; }
+
+ private static bool IsHoloLensConnectedUsb
+ {
+ get
+ {
+ bool isConnected = false;
+
+ if (USBDeviceListener.USBDevices != null)
+ {
+ if (USBDeviceListener.USBDevices.Any(device => device.Name.Equals("Microsoft HoloLens")))
+ {
+ isConnected = true;
+ }
+
+ SessionState.SetBool("HoloLensUsbConnected", isConnected);
+ }
+ else
+ {
+ isConnected = SessionState.GetBool("HoloLensUsbConnected", false);
+ }
+
+ return isConnected;
+ }
+ }
+
+ private static DeviceInfo CurrentConnection
+ {
+ get => UseRemoteTarget ? CurrentRemoteConnection : localConnection;
+ }
+
+ private static DeviceInfo CurrentRemoteConnection
+ {
+ get
+ {
+ var connections = portalConnections?.Connections;
+ if (connections != null && CurrentRemoteConnectionIndex >= 0 && CurrentRemoteConnectionIndex < connections.Count)
+ {
+ return connections[CurrentRemoteConnectionIndex];
+ }
+
+ return null;
+ }
+ }
+
+ private static int CurrentRemoteConnectionIndex
+ {
+ get => portalConnections != null ? portalConnections.CurrentConnectionIndex : -1;
+ set
+ {
+ var connections = portalConnections?.Connections;
+ if (connections != null && value >= 0 && value < connections.Count)
+ {
+ portalConnections.CurrentConnectionIndex = value;
+ }
+ }
+ }
+
+ ///
+ /// Tracks whether the current UI preference is to target the local machine or remote machine for deployment.
+ /// Saves state for duration of current Unity session
+ ///
+ private static bool UseRemoteTarget
+ {
+ get => SessionState.GetBool(UseRemoteTargetSessionKey, false);
+ set => SessionState.SetBool(UseRemoteTargetSessionKey, value);
+ }
+
+ #endregion Properties
+
+ #region Fields
+
+ private const float HALF_WIDTH = 256f;
+
+ private string[] targetIps;
+ private readonly List windowsSdkVersions = new List();
+
+ private Vector2 buildSceneListScrollPosition;
+ private Vector2 deployBuildListScrollPosition;
+ private Vector2 appxBuildOptionsScrollPosition;
+
+ private BuildDeployTab currentTab = BuildDeployTab.UnityBuildOptions;
+ private Action[] tabRenders;
+
+ private static bool isBuilding;
+ private static bool isAppRunning;
+
+ private static DevicePortalConnections portalConnections = null;
+ private static CancellationTokenSource appxCancellationTokenSource = null;
+ private static float appxProgressBarTimer = 0.0f;
+ private static DeviceInfo localConnection;
+
+ private static bool lastTestConnectionSuccessful = false;
+ private static DeviceInfo lastTestConnectionTarget;
+ private static DateTime? lastTestConnectionTime = null;
+
+#if UNITY_2021_2_OR_NEWER
+ private static UWPSubtarget currentSubtarget = UWPSubtarget.AnyDevice;
+#endif // UNITY_2021_2_OR_NEWER
+
+ #endregion Fields
+
+ #region Methods
+
+ [MenuItem("Mixed Reality/Toolkit/Utilities/Build Window", false, 0)]
+ public static void OpenWindow()
+ {
+ // Dock it next to the Scene View.
+ var window = GetWindow(typeof(SceneView));
+ window.titleContent = new GUIContent("Build Window", EditorGUIUtility.IconContent("CustomTool").image);
+ window.Show();
+ }
+
+ private void OnEnable()
+ {
+ LogoLightTheme = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(LogoLightThemeGuid));
+ LogoDarkTheme = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(LogoDarkThemeGuid));
+
+ minSize = new Vector2(512, 256);
+
+ tabRenders = new Action[]
+ {
+ () => { RenderUnityBuildView(); },
+ () => { RenderAppxBuildView(); },
+ () => { RenderDeployBuildView(); },
+ };
+
+ LoadWindowsSdkPaths();
+ UpdateBuilds();
+
+ DevicePortal.UseSSL = UwpBuildDeployPreferences.UseSSL;
+
+ localConnection = JsonUtility.FromJson(UwpBuildDeployPreferences.LocalConnectionInfo);
+
+ portalConnections = JsonUtility.FromJson(UwpBuildDeployPreferences.DevicePortalConnections);
+
+ SaveRemotePortalConnections();
+ }
+
+ private void OnGUI()
+ {
+ GUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+ //GUILayout.Label(EditorGUIUtility.isProSkin ? LogoDarkTheme : LogoLightTheme, GUILayout.MaxHeight(96f));
+ GUILayout.FlexibleSpace();
+ GUILayout.EndHorizontal();
+ GUILayout.Space(3f);
+
+ if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WSAPlayer)
+ {
+ RenderStandaloneBuildView();
+ }
+ else
+ {
+ RenderWSABuildView();
+ }
+ }
+
+ private void RenderWSABuildView()
+ {
+ RenderBuildDirectory();
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ using (new EditorGUI.DisabledGroupScope(!ShouldBuildSLNBeEnabled))
+ {
+ if (GUILayout.Button(CanInstall ? BuildAllThenInstallLabel : BuildAllLabel, GUILayout.ExpandWidth(true)))
+ {
+ EditorApplication.delayCall += () => BuildAll(CanInstall);
+ }
+ }
+
+ RenderPlayerSettingsButton();
+ }
+
+ EditorGUILayout.Space();
+
+ BuildDeployTab lastTab = currentTab;
+ currentTab = (BuildDeployTab)GUILayout.Toolbar(SessionState.GetInt(BuildWindowTabKey, (int)currentTab), TAB_NAMES);
+ if (currentTab != lastTab)
+ {
+ SessionState.SetInt(BuildWindowTabKey, (int)currentTab);
+
+ if (currentTab == BuildDeployTab.DeployOptions)
+ {
+ UpdateBuilds();
+ }
+ }
+
+ using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
+ {
+ tabRenders[(int)currentTab].Invoke();
+ }
+ }
+
+ private void RenderStandaloneBuildView()
+ {
+ RenderBuildDirectory();
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ RenderPlayerSettingsButton();
+
+ if (GUILayout.Button("Build Unity Project"))
+ {
+ EditorApplication.delayCall += () => UnityPlayerBuildTools.BuildUnityPlayer(new BuildInfo());
+ }
+
+ if (GUILayout.Button("Open Unity Build Window"))
+ {
+ GetWindow(Type.GetType("UnityEditor.BuildPlayerWindow,UnityEditor"));
+ }
+ }
+ }
+
+ private void RenderUnityBuildView()
+ {
+#if UNITY_2021_2_OR_NEWER
+ currentSubtarget = (UWPSubtarget)EditorGUILayout.Popup("Target Device", (int)currentSubtarget, TARGET_DEVICE_OPTIONS, GUILayout.Width(HALF_WIDTH));
+#else
+ EditorUserBuildSettings.wsaSubtarget = (WSASubtarget)EditorGUILayout.Popup("Target Device", (int)EditorUserBuildSettings.wsaSubtarget, TARGET_DEVICE_OPTIONS, GUILayout.Width(HALF_WIDTH));
+#endif // UNITY_2021_2_OR_NEWER
+
+#if !UNITY_2019_1_OR_NEWER
+ var curScriptingBackend = PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA);
+ if (curScriptingBackend == ScriptingImplementation.WinRTDotNET)
+ {
+ EditorGUILayout.HelpBox(".NET Scripting backend is deprecated in Unity 2018 and is removed in Unity 2019.", MessageType.Warning);
+ }
+
+ var newScriptingBackend = (ScriptingImplementation)EditorGUILayout.IntPopup("Scripting Backend", (int)curScriptingBackend, SCRIPTING_BACKEND_NAMES, SCRIPTING_BACKEND_ENUMS, GUILayout.Width(HALF_WIDTH));
+ if (newScriptingBackend != curScriptingBackend)
+ {
+ bool canUpdate = !Directory.Exists(BuildDeployPreferences.AbsoluteBuildDirectory);
+ if (EditorUtility.DisplayDialog("Attention!",
+ $"Build path contains project built with {curScriptingBackend.ToString()} scripting backend, while project wants to use {newScriptingBackend.ToString()} scripting backend.\n\nSwitching to a new scripting backend requires us to delete all the data currently in your build folder and rebuild the Unity Player!",
+ "Okay", "Cancel"))
+ {
+ Directory.Delete(BuildDeployPreferences.AbsoluteBuildDirectory, true);
+ canUpdate = true;
+ }
+
+ if (canUpdate)
+ {
+ PlayerSettings.SetScriptingBackend(BuildTargetGroup.WSA, newScriptingBackend);
+ }
+ }
+
+ // To prevent potential confusion, only show this when C# projects will be generated
+ if (EditorUserBuildSettings.wsaGenerateReferenceProjects &&
+ PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.WinRTDotNET)
+ {
+ // Allow unsafe code
+ bool curAllowUnsafeCode = UwpBuildDeployPreferences.AllowUnsafeCode;
+ bool newAllowUnsafeCode = EditorGUILayout.ToggleLeft(AllowUnsafeCodeLabel, curAllowUnsafeCode);
+ if (newAllowUnsafeCode != curAllowUnsafeCode)
+ {
+ UwpBuildDeployPreferences.AllowUnsafeCode = newAllowUnsafeCode;
+ }
+ }
+
+ // Generate C# Project References for debugging
+ if (PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.WinRTDotNET)
+ {
+ bool generateReferenceProjects = EditorUserBuildSettings.wsaGenerateReferenceProjects;
+ bool shouldGenerateProjects = EditorGUILayout.ToggleLeft(UseCSharpProjectsLabel, generateReferenceProjects);
+ if (shouldGenerateProjects != generateReferenceProjects)
+ {
+ EditorUserBuildSettings.wsaGenerateReferenceProjects = shouldGenerateProjects;
+ }
+ }
+#endif // !UNITY_2019_1_OR_NEWER
+
+ using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
+ {
+ EditorGUILayout.LabelField("Scenes in Build", EditorStyles.boldLabel);
+
+ using (var scrollView = new EditorGUILayout.ScrollViewScope(buildSceneListScrollPosition, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)))
+ {
+ buildSceneListScrollPosition = scrollView.scrollPosition;
+
+ using (new EditorGUI.DisabledGroupScope(true))
+ {
+ var scenes = EditorBuildSettings.scenes;
+ for (int i = 0; i < scenes.Length; i++)
+ {
+ EditorGUILayout.ToggleLeft($"{i} {scenes[i].path}", scenes[i].enabled);
+ }
+ }
+ }
+ }
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ using (new EditorGUI.DisabledGroupScope(!ShouldOpenSLNBeEnabled))
+ {
+ RenderOpenVisualStudioButton();
+
+ if (GUILayout.Button("Build Unity Project"))
+ {
+ EditorApplication.delayCall += BuildUnityProject;
+ }
+ }
+ }
+
+ EditorGUILayout.Space();
+ }
+
+ private void RenderAppxBuildView()
+ {
+ using (var scrollView = new EditorGUILayout.ScrollViewScope(appxBuildOptionsScrollPosition, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)))
+ {
+ appxBuildOptionsScrollPosition = scrollView.scrollPosition;
+
+ // SDK and MS Build Version (and save setting, if it's changed)
+ // Note that this is the 'Target SDK Version' which is required to physically build the
+ // code on a build machine, not the minimum platform version.
+ string currentSDKVersion = EditorUserBuildSettings.wsaUWPSDK;
+
+ Version chosenSDKVersion = null;
+ for (var i = 0; i < windowsSdkVersions.Count; i++)
+ {
+ // windowsSdkVersions is sorted in ascending order, so we always take
+ // the highest SDK version that is above our minimum.
+ if (windowsSdkVersions[i] >= UwpBuildDeployPreferences.MIN_SDK_VERSION)
+ {
+ chosenSDKVersion = windowsSdkVersions[i];
+ }
+ }
+
+ EditorGUILayout.HelpBox($"Windows SDK Version: {currentSDKVersion}", MessageType.Info);
+
+ // Throw exception if user has no Windows 10 SDK installed
+ if (chosenSDKVersion == null)
+ {
+ if (IsValidSdkInstalled)
+ {
+ Debug.LogError($"Unable to find the required Windows 10 SDK Target!\nPlease be sure to install the {UwpBuildDeployPreferences.MIN_SDK_VERSION} SDK from Visual Studio Installer.");
+ }
+
+ EditorGUILayout.HelpBox($"Unable to find the required Windows 10 SDK Target!\nPlease be sure to install the {UwpBuildDeployPreferences.MIN_SDK_VERSION} SDK from Visual Studio Installer.", MessageType.Error);
+ IsValidSdkInstalled = false;
+ return;
+ }
+
+ IsValidSdkInstalled = true;
+
+ string newSDKVersion = chosenSDKVersion.ToString();
+ if (!newSDKVersion.Equals(currentSDKVersion))
+ {
+ EditorUserBuildSettings.wsaUWPSDK = newSDKVersion;
+ }
+
+ string currentMinPlatformVersion = EditorUserBuildSettings.wsaMinUWPSDK;
+ if (string.IsNullOrWhiteSpace(currentMinPlatformVersion))
+ {
+ // If the min platform version hasn't been specified, set it to the recommended value.
+ EditorUserBuildSettings.wsaMinUWPSDK = UwpBuildDeployPreferences.MIN_PLATFORM_VERSION.ToString();
+ }
+ else if (UwpBuildDeployPreferences.MIN_PLATFORM_VERSION != new Version(currentMinPlatformVersion))
+ {
+ // If the user has manually changed the minimum platform version in the 'Build Settings' window
+ // provide a warning that the generated application may not be deployable to older generation
+ // devices. We generally recommend setting to the lowest value and letting the app model's
+ // capability and versioning checks kick in for applications at runtime.
+ EditorGUILayout.HelpBox(
+ "Minimum platform version is set to a different value from the recommended value: " +
+ $"{UwpBuildDeployPreferences.MIN_PLATFORM_VERSION}, the generated app may not be deployable to older generation devices. " +
+ $"Consider updating the 'Minimum Platform Version' in the Build Settings window to match {UwpBuildDeployPreferences.MIN_PLATFORM_VERSION}",
+ MessageType.Warning);
+ }
+
+ using (var c = new EditorGUI.ChangeCheckScope())
+ {
+ EditorGUILayout.LabelField("Build Options", EditorStyles.boldLabel);
+ var newBuildConfigOption = (WSABuildType)EditorGUILayout.EnumPopup("Build Configuration", UwpBuildDeployPreferences.BuildConfigType, GUILayout.Width(HALF_WIDTH));
+ UwpBuildDeployPreferences.BuildConfig = newBuildConfigOption.ToString().ToLower();
+
+ // Build Platform
+ int currentPlatformIndex = Array.IndexOf(ARCHITECTURE_OPTIONS, EditorUserBuildSettings.wsaArchitecture);
+ int buildPlatformIndex = EditorGUILayout.Popup("Build Platform", currentPlatformIndex, ARCHITECTURE_OPTIONS, GUILayout.Width(HALF_WIDTH));
+
+ // Platform Toolset
+ int currentPlatformToolsetIndex = Array.IndexOf(PLATFORM_TOOLSET_VALUES, UwpBuildDeployPreferences.PlatformToolset);
+ int newPlatformToolsetIndex = EditorGUILayout.Popup("Platform Toolset", currentPlatformToolsetIndex, PLATFORM_TOOLSET_NAMES, GUILayout.Width(HALF_WIDTH));
+
+ // Force rebuild
+ bool forceRebuildAppx = EditorGUILayout.ToggleLeft("Force Rebuild", UwpBuildDeployPreferences.ForceRebuild);
+
+ // Multicore Appx Build
+ bool multicoreAppxBuildEnabled = EditorGUILayout.ToggleLeft("Multicore Build", UwpBuildDeployPreferences.MulticoreAppxBuildEnabled);
+
+ EditorGUILayout.LabelField("Manifest Options", EditorStyles.boldLabel);
+
+ // The 'Gaze Input' capability support was added for HL2 in the Windows SDK 18362, but
+ // existing versions of Unity don't have support for automatically adding the capability to the generated
+ // AppX manifest during the build. This option provides a mechanism for people using the
+ // MRTK build tools to auto-append this capability if desired, instead of having to manually
+ // do this each time on their own.
+ bool gazeInputCapabilityEnabled = EditorGUILayout.ToggleLeft(GazeInputCapabilityLabel, UwpBuildDeployPreferences.GazeInputCapabilityEnabled);
+
+ // Enable Research Mode Capability
+ bool researchModeEnabled = EditorGUILayout.ToggleLeft(ResearchModeCapabilityLabel, UwpBuildDeployPreferences.ResearchModeCapabilityEnabled);
+
+ // Don't draw the preview while building (when appxCancellationTokenSource will be non-null),
+ // since there's a null texture issue when Unity reloads the assets during a build
+ MixedRealityBuildPreferences.DrawAppLauncherModelField(appxCancellationTokenSource == null);
+
+ // Draw the section for nuget executable path
+ EditorGUILayout.LabelField("Nuget Path (Optional)", EditorStyles.boldLabel);
+
+ string nugetExecutablePath = EditorGUILayout.TextField(NugetPathLabel, UwpBuildDeployPreferences.NugetExecutablePath);
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ if (GUILayout.Button("Select Nuget Executable Path"))
+ {
+ nugetExecutablePath = EditorUtility.OpenFilePanel(
+ "Select Nuget Executable Path", "", "exe");
+ }
+ if (GUILayout.Button("Use msbuild for Restore & Clear Path"))
+ {
+ nugetExecutablePath = "";
+ }
+ }
+ if (c.changed)
+ {
+ UwpBuildDeployPreferences.PlatformToolset = PLATFORM_TOOLSET_VALUES[newPlatformToolsetIndex];
+ EditorUserBuildSettings.wsaArchitecture = ARCHITECTURE_OPTIONS[buildPlatformIndex];
+ UwpBuildDeployPreferences.GazeInputCapabilityEnabled = gazeInputCapabilityEnabled;
+ UwpBuildDeployPreferences.ResearchModeCapabilityEnabled = researchModeEnabled;
+ UwpBuildDeployPreferences.ForceRebuild = forceRebuildAppx;
+ UwpBuildDeployPreferences.MulticoreAppxBuildEnabled = multicoreAppxBuildEnabled;
+ UwpBuildDeployPreferences.NugetExecutablePath = nugetExecutablePath;
+ }
+ }
+ }
+
+ EditorGUILayout.LabelField("Versioning Options", EditorStyles.boldLabel);
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ using (var c = new EditorGUI.ChangeCheckScope())
+ {
+ // Auto Increment version
+ bool incrementVersion = EditorGUILayout.ToggleLeft(AutoIncrementLabel, BuildDeployPreferences.IncrementBuildVersion);
+
+ EditorGUILayout.LabelField(VersionNumberLabel, GUILayout.Width(96));
+ Vector3 newVersion = Vector3.zero;
+
+ newVersion.x = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Major);
+ newVersion.y = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Minor);
+ newVersion.z = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Build);
+
+ if (c.changed)
+ {
+ BuildDeployPreferences.IncrementBuildVersion = incrementVersion;
+ PlayerSettings.WSA.packageVersion = new Version((int)newVersion.x, (int)newVersion.y, (int)newVersion.z, 0);
+ }
+ }
+
+ using (new EditorGUI.DisabledGroupScope(true))
+ {
+ EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Revision);
+ }
+ }
+
+ EditorGUILayout.Space();
+
+ if (appxCancellationTokenSource != null)
+ {
+ using (var progressBarRect = new EditorGUILayout.VerticalScope())
+ {
+ appxProgressBarTimer = Mathf.Clamp01(Time.realtimeSinceStartup % 1.0f);
+
+ EditorGUI.ProgressBar(progressBarRect.rect, appxProgressBarTimer, "Building AppX...");
+ GUILayout.Space(16);
+ Repaint();
+ }
+ }
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ GUILayout.FlexibleSpace();
+
+ // Open AppX packages location
+ string appxDirectory = PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.IL2CPP ? $"/AppPackages/{PlayerSettings.productName}" : $"/{PlayerSettings.productName}/AppPackages";
+ string appxBuildPath = Path.GetFullPath($"{BuildDeployPreferences.BuildDirectory}{appxDirectory}");
+
+ using (new EditorGUI.DisabledGroupScope(Builds.Count <= 0 || string.IsNullOrEmpty(appxBuildPath)))
+ {
+ if (GUILayout.Button("Open AppX Packages Location", GUILayout.Width(HALF_WIDTH)))
+ {
+ EditorApplication.delayCall += () => Process.Start("explorer.exe", $"/f /open,{appxBuildPath}");
+ }
+ }
+
+ if (appxCancellationTokenSource == null)
+ {
+ using (new EditorGUI.DisabledGroupScope(!ShouldBuildAppxBeEnabled))
+ {
+ if (GUILayout.Button("Build AppX", GUILayout.Width(HALF_WIDTH)))
+ {
+ // Check if solution exists
+ string slnFilename = Path.Combine(BuildDeployPreferences.BuildDirectory, $"{PlayerSettings.productName}.sln");
+ if (File.Exists(slnFilename))
+ {
+ EditorApplication.delayCall += BuildAppx;
+ }
+ else if (EditorUtility.DisplayDialog("Solution Not Found", "We couldn't find the Visual Studio solution. Would you like to build it?", "Yes, Build Unity", "No"))
+ {
+ EditorApplication.delayCall += () => BuildAll(install: false);
+ }
+ }
+ }
+ }
+ else
+ {
+ if (GUILayout.Button("Cancel Build", GUILayout.Width(HALF_WIDTH)))
+ {
+ appxCancellationTokenSource.Cancel();
+ }
+ }
+ }
+ }
+
+ private void RenderDeployBuildView()
+ {
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ EditorGUILayout.LabelField(TargetTypeLabel, GUILayout.Width(75));
+ UseRemoteTarget = EditorGUILayout.Popup(UseRemoteTarget ? 1 : 0, LocalRemoteOptions, GUILayout.Width(100)) != 0;
+ GUILayout.FlexibleSpace();
+ }
+
+ using (new GUILayout.VerticalScope("box"))
+ {
+ if (UseRemoteTarget)
+ {
+ RenderRemoteConnections();
+
+ RenderSSLButtons();
+ }
+ else
+ {
+ RenderLocalConnection();
+
+ if (IsHoloLensConnectedUsb)
+ {
+ using (new EditorGUI.DisabledGroupScope(!AreCredentialsValid(localConnection)))
+ {
+ if (GUILayout.Button("Discover HoloLens WiFi IP", GUILayout.Width(HALF_WIDTH)))
+ {
+ EditorApplication.delayCall += () =>
+ {
+ DiscoverLocalHololensIP();
+ };
+ }
+ }
+ }
+
+ RenderSSLButtons();
+ }
+
+ EditorGUILayout.Space();
+
+ RenderConnectionButtons();
+ }
+
+ EditorGUILayout.Space();
+
+ RenderBuildsList();
+ }
+
+ private void RenderSSLButtons()
+ {
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ bool useSSL = UwpBuildDeployPreferences.UseSSL;
+ bool newUseSSL = EditorGUILayout.ToggleLeft(UseSSLLabel, useSSL);
+ if (newUseSSL != useSSL)
+ {
+ UwpBuildDeployPreferences.UseSSL = newUseSSL;
+ DevicePortal.UseSSL = newUseSSL;
+ }
+ else if (UwpBuildDeployPreferences.UseSSL != DevicePortal.UseSSL)
+ {
+ DevicePortal.UseSSL = UwpBuildDeployPreferences.UseSSL;
+ }
+
+ bool verifySSL = UwpBuildDeployPreferences.VerifySSL;
+ bool newVerifySSL = EditorGUILayout.ToggleLeft(VerifySSLLabel, verifySSL);
+ if (newVerifySSL != verifySSL)
+ {
+ UwpBuildDeployPreferences.VerifySSL = newVerifySSL;
+ DevicePortal.VerifySSLCertificates = verifySSL;
+ }
+ else if (UwpBuildDeployPreferences.VerifySSL != DevicePortal.VerifySSLCertificates)
+ {
+ DevicePortal.VerifySSLCertificates = UwpBuildDeployPreferences.VerifySSL;
+ }
+
+ GUILayout.FlexibleSpace();
+ }
+ }
+
+ private void RenderConnectionButtons()
+ {
+ DeviceInfo currentConnection = CurrentConnection;
+
+ bool canTestConnection = (!UseRemoteTarget || IsValidIpAddress(currentConnection.IP)) && AreCredentialsValid(currentConnection);
+ using (new EditorGUI.DisabledGroupScope(!canTestConnection))
+ {
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ if (GUILayout.Button("Test Connection", GUILayout.Width(128f)))
+ {
+ lastTestConnectionTime = null;
+
+ EditorApplication.delayCall += async () =>
+ {
+ lastTestConnectionSuccessful = await ConnectToDevice();
+ lastTestConnectionTime = DateTime.UtcNow;
+ lastTestConnectionTarget = currentConnection;
+ };
+ }
+
+ if (lastTestConnectionTime != null)
+ {
+ string successStatus = lastTestConnectionSuccessful ? "Successful" : "Failed";
+ EditorGUILayout.LabelField($"{successStatus} connection to {lastTestConnectionTarget.ToString()}, {lastTestConnectionTime.Value.GetRelativeTime()}");
+ }
+ }
+
+ EditorGUILayout.Space();
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ using (new EditorGUI.DisabledGroupScope(false))
+ {
+ if (GUILayout.Button("Open Device Portal", GUILayout.Width(128f)))
+ {
+ EditorApplication.delayCall += () => OpenDevicePortal();
+ }
+ }
+
+ GUILayout.FlexibleSpace();
+ }
+ }
+ }
+
+ private void RenderLocalConnection()
+ {
+ using (var c = new EditorGUI.ChangeCheckScope())
+ {
+ string target = IsHoloLensConnectedUsb ? HOLOLENS_USB : DeviceInfo.LocalMachine;
+ EditorGUILayout.LabelField(target, GUILayout.Width(HALF_WIDTH));
+
+ EditorGUILayout.LabelField(IPAddressLabel, new GUIContent(localConnection.IP), GUILayout.Width(HALF_WIDTH));
+
+ localConnection.User = EditorGUILayout.TextField(UsernameLabel, localConnection.User, GUILayout.Width(HALF_WIDTH));
+
+ localConnection.Password = EditorGUILayout.PasswordField(PasswordLabel, localConnection.Password, GUILayout.Width(HALF_WIDTH));
+
+ if (c.changed)
+ {
+ SaveLocalConnection();
+ }
+ }
+ }
+
+ private void RenderRemoteConnections()
+ {
+ using (var c = new EditorGUI.ChangeCheckScope())
+ {
+ if (portalConnections.Connections.Count == 0)
+ {
+ AddRemoteConnection();
+ }
+
+ DeviceInfo currentConnection = CurrentRemoteConnection;
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ CurrentRemoteConnectionIndex = EditorGUILayout.Popup(CurrentRemoteConnectionIndex, targetIps, GUILayout.Width(260));
+
+ if (GUILayout.Button(AddConnectionLabel, EditorStyles.miniButtonLeft, GUILayout.Width(20)))
+ {
+ AddRemoteConnection();
+ }
+
+ using (new EditorGUI.DisabledGroupScope(portalConnections.Connections.Count <= 1))
+ {
+ if (GUILayout.Button(RemoveConnectionLabel, EditorStyles.miniButtonRight, GUILayout.Width(20)))
+ {
+ RemoveConnection();
+ }
+ }
+
+ GUILayout.FlexibleSpace();
+ }
+
+ if (IsHoloLensConnectedUsb && IsLocalConnection(currentConnection))
+ {
+ EditorGUILayout.LabelField(HOLOLENS_USB);
+ }
+
+ currentConnection.IP = EditorGUILayout.TextField(IPAddressLabel, currentConnection.IP, GUILayout.Width(HALF_WIDTH));
+
+ currentConnection.User = EditorGUILayout.TextField(UsernameLabel, currentConnection.User, GUILayout.Width(HALF_WIDTH));
+
+ currentConnection.Password = EditorGUILayout.PasswordField(PasswordLabel, currentConnection.Password, GUILayout.Width(HALF_WIDTH));
+
+ if (c.changed)
+ {
+ SaveRemotePortalConnections();
+ }
+ }
+ }
+
+ private void RenderBuildsList()
+ {
+ DeviceInfo currentConnection = CurrentConnection;
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ if (GUILayout.Button(RefreshBuildsLabel))
+ {
+ UpdateBuilds();
+ }
+
+ GUILayout.FlexibleSpace();
+ }
+
+ bool processAll = UwpBuildDeployPreferences.TargetAllConnections;
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ EditorGUI.BeginChangeCheck();
+
+ processAll = EditorGUILayout.ToggleLeft(ExecuteOnAllDevicesLabel, processAll);
+
+ bool fullReinstall = EditorGUILayout.ToggleLeft(AlwaysUninstallLabel, UwpBuildDeployPreferences.FullReinstall);
+
+ GUILayout.FlexibleSpace();
+
+ if (EditorGUI.EndChangeCheck())
+ {
+ UwpBuildDeployPreferences.TargetAllConnections = processAll;
+ UwpBuildDeployPreferences.FullReinstall = fullReinstall;
+ }
+ }
+
+ EditorGUILayout.LabelField(string.Empty, GUI.skin.horizontalSlider);
+
+ if (Builds.Count == 0)
+ {
+ EditorGUILayout.HelpBox("***No builds found in build directory", MessageType.Info);
+ }
+ else
+ {
+ using (new EditorGUILayout.VerticalScope(GUILayout.ExpandHeight(true)))
+ {
+ using (var scrollView = new EditorGUILayout.ScrollViewScope(deployBuildListScrollPosition, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)))
+ {
+ deployBuildListScrollPosition = scrollView.scrollPosition;
+
+ foreach (var fullBuildLocation in Builds)
+ {
+ if (!Directory.Exists(fullBuildLocation))
+ {
+ continue;
+ }
+
+ int lastBackslashIndex = fullBuildLocation.LastIndexOf("\\", StringComparison.Ordinal);
+
+ var directoryDate = Directory.GetLastWriteTime(fullBuildLocation).ToString("yyyy/MM/dd HH:mm:ss");
+ string packageName = fullBuildLocation.Substring(lastBackslashIndex + 1);
+
+ using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox))
+ {
+ using (new EditorGUI.DisabledGroupScope(!CanInstall))
+ {
+ if (GUILayout.Button(InstallAppXLabel, GUILayout.Width(120)))
+ {
+ EditorApplication.delayCall += () =>
+ {
+ ExecuteAction((DeviceInfo connection)
+ => InstallAppOnDeviceAsync(fullBuildLocation, connection));
+ };
+ }
+
+ if (GUILayout.Button(UninstallAppXLabel, GUILayout.Width(120)))
+ {
+ EditorApplication.delayCall += () =>
+ {
+ ExecuteAction((DeviceInfo connection)
+ => UninstallAppOnDeviceAsync(connection));
+ };
+ }
+ }
+
+ bool canLaunchLocal = IsLocalConnection(currentConnection) && IsHoloLensConnectedUsb;
+ bool canLaunchRemote = DevicePortalConnectionEnabled && CanInstall;
+
+ // Launch app...
+ bool launchAppEnabled = canLaunchLocal || canLaunchRemote;
+ using (new EditorGUI.DisabledGroupScope(!launchAppEnabled))
+ {
+ if (isAppRunning)
+ {
+ if (GUILayout.Button(KillAppLabel, GUILayout.Width(96)))
+ {
+ ExecuteAction((DeviceInfo connection)
+ => KillAppOnDeviceAsync(connection));
+
+ isAppRunning = false;
+ }
+ }
+ else
+ {
+ if (GUILayout.Button(LaunchAppLabel, GUILayout.Width(96)))
+ {
+ ExecuteAction((DeviceInfo connection)
+ => LaunchAppOnDeviceAsync(connection));
+
+ isAppRunning = true;
+ }
+ }
+ }
+
+ // Log file
+ string localLogPath = $"%USERPROFILE%\\AppData\\Local\\Packages\\{PlayerSettings.productName}\\TempState\\UnityPlayer.log";
+ bool localLogExists = File.Exists(localLogPath);
+
+ bool viewLogEnabled = localLogExists || canLaunchRemote || canLaunchLocal;
+ using (new EditorGUI.DisabledGroupScope(!viewLogEnabled))
+ {
+ if (GUILayout.Button(ViewPlayerLogLabel, GUILayout.Width(126)))
+ {
+ EditorApplication.delayCall += () =>
+ {
+ ExecuteAction((DeviceInfo connection)
+ => OpenLogFileForDeviceAsync(connection, localLogPath));
+ };
+ }
+ }
+
+ EditorGUILayout.LabelField(new GUIContent($"{packageName} ({directoryDate})"));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ #endregion Methods
+
+ #region Render Helpers
+
+ private void RenderBuildDirectory()
+ {
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ string currentBuildDirectory = BuildDeployPreferences.BuildDirectory;
+
+ EditorGUILayout.LabelField(BuildDirectoryLabel, GUILayout.Width(96));
+
+ using (new EditorGUI.DisabledGroupScope(true))
+ {
+ EditorGUILayout.TextField(currentBuildDirectory, GUILayout.ExpandWidth(true));
+ }
+
+ if (GUILayout.Button(new GUIContent("Select Folder"), EditorStyles.miniButtonLeft, GUILayout.Width(100f)))
+ {
+ var fullBuildPath = Path.GetFullPath(EditorUtility.OpenFolderPanel("Select Build Directory", currentBuildDirectory, string.Empty));
+
+ // Temporary code as original design only allowed relative paths
+ string projectPath = Path.GetFullPath(Path.Combine(Application.dataPath, "../"));
+
+ if (fullBuildPath.StartsWith(projectPath))
+ {
+ BuildDeployPreferences.BuildDirectory = fullBuildPath.Replace(projectPath, string.Empty);
+ }
+ else
+ {
+ Debug.LogError("Build path must be relative to current Unity project");
+ }
+ }
+
+ RenderOpenBuildDirectoryButton();
+ }
+
+ EditorGUILayout.Space();
+ }
+
+ private static void RenderOpenVisualStudioButton()
+ {
+ if (GUILayout.Button("Open in Visual Studio", GUILayout.Width(HALF_WIDTH)))
+ {
+ string slnFilename = Path.Combine(BuildDeployPreferences.BuildDirectory, $"{PlayerSettings.productName}.sln");
+
+ if (File.Exists(slnFilename))
+ {
+ EditorApplication.delayCall += () => Process.Start(new FileInfo(slnFilename).FullName);
+ }
+ else if (EditorUtility.DisplayDialog(
+ "Solution Not Found",
+ "We couldn't find the Project's Solution. Would you like to Build the project now?",
+ "Yes, Build", "No"))
+ {
+ EditorApplication.delayCall += BuildUnityProject;
+ }
+ }
+ }
+
+ private static void RenderPlayerSettingsButton()
+ {
+ if (GUILayout.Button("Open Player Settings"))
+ {
+ Selection.activeObject = Unsupported.GetSerializedAssetInterfaceSingleton("PlayerSettings");
+ }
+ }
+
+ private static void RenderOpenBuildDirectoryButton()
+ {
+ using (new EditorGUI.DisabledGroupScope(!Directory.Exists(BuildDeployPreferences.AbsoluteBuildDirectory)))
+ {
+ if (GUILayout.Button("Open", EditorStyles.miniButtonRight, GUILayout.Width(100f)))
+ {
+ EditorApplication.delayCall += () => Process.Start(BuildDeployPreferences.AbsoluteBuildDirectory);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Utilities
+
+ private void RemoveConnection()
+ {
+ var connections = portalConnections?.Connections;
+ if (connections != null && connections.Count > 0)
+ {
+ portalConnections.Connections.RemoveAt(CurrentRemoteConnectionIndex);
+ CurrentRemoteConnectionIndex--;
+ SaveRemotePortalConnections();
+ }
+ }
+
+ private void AddRemoteConnection()
+ {
+ DeviceInfo currentConnection = CurrentRemoteConnection;
+ AddRemoteConnection(new DeviceInfo(EMPTY_IP_ADDRESS, currentConnection.User, currentConnection.Password));
+ }
+
+ private void AddRemoteConnection(DeviceInfo newDevice)
+ {
+ portalConnections.Connections.Add(newDevice);
+ CurrentRemoteConnectionIndex = portalConnections.Connections.Count - 1;
+ SaveRemotePortalConnections();
+ }
+
+ private async void DiscoverLocalHololensIP()
+ {
+ var machineName = await DevicePortal.GetMachineNameAsync(localConnection);
+ var networkInfo = await DevicePortal.GetIpConfigInfoAsync(localConnection);
+ if (machineName != null && networkInfo != null)
+ {
+ foreach (var adapter in networkInfo.Adapters)
+ {
+ if (adapter.Type.Contains(WifiAdapterType))
+ {
+ foreach (var address in adapter.IpAddresses)
+ {
+ string ipAddress = address.IpAddress;
+ if (IsValidIpAddress(ipAddress)
+ && !portalConnections.Connections.Any(connection => connection.IP == ipAddress))
+ {
+ Debug.Log($"Adding new IP {ipAddress} for local HoloLens {machineName.ComputerName} to remote connection list");
+
+ AddRemoteConnection(new DeviceInfo(ipAddress,
+ localConnection.User,
+ localConnection.Password,
+ machineName.ComputerName));
+
+ return;
+ }
+ }
+ }
+ }
+
+ Debug.Log($"No new or valid WiFi IP Addresses found for local HoloLens {machineName.ComputerName}");
+ }
+ }
+
+ private async Task ConnectToDevice()
+ {
+ DeviceInfo currentConnection = CurrentConnection;
+ var machineName = await DevicePortal.GetMachineNameAsync(currentConnection);
+ if (machineName != null)
+ {
+ currentConnection.MachineName = machineName?.ComputerName;
+ SaveRemotePortalConnections();
+ Debug.Log($"Successfully connected to device {machineName?.ComputerName} with IP {currentConnection.IP}");
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Builds the open Unity project for
+ /// BuildTarget.WSAPlayer.
+ ///
+ public static async void BuildUnityProject()
+ {
+ Debug.Assert(!isBuilding);
+ isBuilding = true;
+
+ appxCancellationTokenSource = new CancellationTokenSource();
+ await UwpPlayerBuildTools.BuildPlayer(BuildDeployPreferences.BuildDirectory, cancellationToken: appxCancellationTokenSource.Token);
+ appxCancellationTokenSource.Dispose();
+ appxCancellationTokenSource = null;
+
+ isBuilding = false;
+ }
+
+ ///
+ /// Builds an AppX for the open Unity project for
+ /// BuildTarget.WSAPlayer.
+ ///
+ public static async void BuildAppx()
+ {
+ Debug.Assert(!isBuilding);
+ isBuilding = true;
+
+ appxCancellationTokenSource = new CancellationTokenSource();
+
+ var buildInfo = new UwpBuildInfo
+ {
+ RebuildAppx = UwpBuildDeployPreferences.ForceRebuild,
+ Configuration = UwpBuildDeployPreferences.BuildConfig,
+ BuildPlatform = EditorUserBuildSettings.wsaArchitecture,
+ PlatformToolset = UwpBuildDeployPreferences.PlatformToolset,
+ OutputDirectory = BuildDeployPreferences.BuildDirectory,
+ AutoIncrement = BuildDeployPreferences.IncrementBuildVersion,
+ Multicore = UwpBuildDeployPreferences.MulticoreAppxBuildEnabled,
+ };
+
+ EditorAssemblyReloadManager.LockReloadAssemblies = true;
+ await UwpAppxBuildTools.BuildAppxAsync(buildInfo, appxCancellationTokenSource.Token);
+ EditorAssemblyReloadManager.LockReloadAssemblies = false;
+ appxCancellationTokenSource.Dispose();
+ appxCancellationTokenSource = null;
+
+ isBuilding = false;
+ }
+
+ ///
+ /// Builds the open Unity project and its AppX for
+ /// BuildTarget.WSAPlayer.
+ ///
+ public static async void BuildAll(bool install = true)
+ {
+ Debug.Assert(!isBuilding);
+ isBuilding = true;
+ EditorAssemblyReloadManager.LockReloadAssemblies = true;
+
+ appxCancellationTokenSource = new CancellationTokenSource();
+
+ // First build SLN
+ if (await UwpPlayerBuildTools.BuildPlayer(BuildDeployPreferences.BuildDirectory, false, appxCancellationTokenSource.Token))
+ {
+ if (install)
+ {
+ string fullBuildLocation = CalcMostRecentBuild();
+
+ await ExecuteActionAsync((DeviceInfo connection)
+ => InstallAppOnDeviceAsync(fullBuildLocation, connection));
+ }
+ }
+
+ appxCancellationTokenSource.Dispose();
+ appxCancellationTokenSource = null;
+ EditorAssemblyReloadManager.LockReloadAssemblies = false;
+ isBuilding = false;
+ }
+
+ private static void UpdateBuilds()
+ {
+ Builds.Clear();
+
+ var curScriptingBackend = PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA);
+ string appxDirectory = curScriptingBackend == ScriptingImplementation.IL2CPP ? Path.Combine("AppPackages", PlayerSettings.productName) : Path.Combine(PlayerSettings.productName, "AppPackages");
+
+ try
+ {
+ AppPackageDirectories.Clear();
+ string[] buildList = Directory.GetDirectories(BuildDeployPreferences.AbsoluteBuildDirectory, "*", SearchOption.AllDirectories);
+ foreach (string appBuild in buildList)
+ {
+ if (appBuild.Contains(appxDirectory) && !appBuild.Contains($"{appxDirectory}\\"))
+ {
+ AppPackageDirectories.AddRange(Directory.GetDirectories(appBuild));
+ }
+ }
+
+ IEnumerable selectedDirectories =
+ from string directory in AppPackageDirectories
+ orderby Directory.GetLastWriteTime(directory) descending
+ select Path.GetFullPath(directory);
+ Builds.AddRange(selectedDirectories);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // unused
+ }
+
+ UpdatePackageName();
+ }
+
+ private static string CalcMostRecentBuild()
+ {
+ UpdateBuilds();
+ DateTime mostRecent = DateTime.MinValue;
+ string mostRecentBuild = string.Empty;
+
+ foreach (var fullBuildLocation in Builds)
+ {
+ DateTime directoryDate = Directory.GetLastWriteTime(fullBuildLocation);
+
+ if (directoryDate > mostRecent)
+ {
+ mostRecentBuild = fullBuildLocation;
+ mostRecent = directoryDate;
+ }
+ }
+
+ return mostRecentBuild;
+ }
+
+ private void SaveLocalConnection()
+ {
+ UwpBuildDeployPreferences.LocalConnectionInfo = JsonUtility.ToJson(localConnection);
+ }
+
+ private void SaveRemotePortalConnections()
+ {
+ targetIps = new string[portalConnections.Connections.Count];
+
+ for (int i = 0; i < targetIps.Length; i++)
+ {
+ var connection = portalConnections.Connections[i];
+
+ if (string.IsNullOrEmpty(connection.MachineName))
+ {
+ targetIps[i] = connection.IP;
+ }
+ else
+ {
+ targetIps[i] = $"{connection.MachineName} - {connection.IP}";
+ }
+ }
+
+ UwpBuildDeployPreferences.DevicePortalConnections = JsonUtility.ToJson(portalConnections);
+ Repaint();
+ }
+
+ private static bool IsLocalConnection(DeviceInfo connection)
+ {
+ return connection.IP.Contains(DeviceInfo.LocalMachine) ||
+ connection.IP.Contains(DeviceInfo.LocalIPAddress);
+ }
+
+ private static bool AreCredentialsValid(DeviceInfo connection)
+ {
+ return !string.IsNullOrEmpty(connection.User) &&
+ !string.IsNullOrEmpty(connection.IP) &&
+ !string.IsNullOrEmpty(connection.Password);
+ }
+
+ private static bool IsValidIpAddress(string ip)
+ {
+ if (string.IsNullOrEmpty(ip))
+ {
+ return false;
+ }
+
+ string ipAddr = ip;
+
+ var portSegments = ip.Split(':');
+ if (portSegments.Length > 2)
+ {
+ return false;
+ }
+ else if (portSegments.Length == 2)
+ {
+ if (!UInt16.TryParse(portSegments[1], out UInt16 result))
+ {
+ return false;
+ }
+
+ ipAddr = portSegments[0];
+ }
+
+ return ipAddr.Split('.').Length == 4 && !ipAddr.Contains(EMPTY_IP_ADDRESS) &&
+ (IPAddress.TryParse(ipAddr, out IPAddress address) || ipAddr.Contains(DeviceInfo.LocalMachine));
+ }
+
+ private static string UpdatePackageName()
+ {
+ if (AppPackageDirectories.Count == 0)
+ {
+ return string.Empty;
+ }
+
+ // Find the manifest
+ string[] manifests = Directory.GetFiles(BuildDeployPreferences.AbsoluteBuildDirectory, "Package.appxmanifest", SearchOption.AllDirectories);
+
+ if (manifests.Length == 0)
+ {
+ Debug.LogError($"Unable to find manifest file for build (in path - {BuildDeployPreferences.AbsoluteBuildDirectory})");
+ return string.Empty;
+ }
+
+ string manifest = manifests[0];
+
+ // Parse it
+ using (var reader = new XmlTextReader(manifest))
+ {
+ while (reader.Read())
+ {
+ switch (reader.NodeType)
+ {
+ case XmlNodeType.Element:
+ if (reader.Name.Equals("identity", StringComparison.OrdinalIgnoreCase))
+ {
+ while (reader.MoveToNextAttribute())
+ {
+ if (reader.Name.Equals("name", StringComparison.OrdinalIgnoreCase))
+ {
+ return PackageName = reader.Value;
+ }
+ }
+ }
+
+ break;
+ }
+ }
+ }
+
+ Debug.LogError($"Unable to find PackageFamilyName in manifest file ({manifest})");
+ return string.Empty;
+ }
+
+ private void LoadWindowsSdkPaths()
+ {
+ string win10KitsPath = WINDOWS_10_KITS_DEFAULT_PATH;
+#if UNITY_EDITOR_WIN
+ // Windows 10 sdk might not be installed on C: drive.
+ // Try to detect the installation path by checking the registry.
+ try
+ {
+ var registryKey = Win32.Registry.LocalMachine.OpenSubKey(WINDOWS_10_KITS_PATH_REGISTRY_PATH);
+ var registryValue = registryKey.GetValue(WINDOWS_10_KITS_PATH_REGISTRY_KEY) as string;
+ win10KitsPath = Path.Combine(registryValue, WINDOWS_10_KITS_PATH_POSTFIX);
+
+ if (!Directory.Exists(win10KitsPath))
+ {
+ registryKey = Win32.Registry.LocalMachine.OpenSubKey(WINDOWS_10_KITS_PATH_ALTERNATE_REGISTRY_PATH);
+ registryValue = registryKey.GetValue(WINDOWS_10_KITS_PATH_REGISTRY_KEY) as string;
+ win10KitsPath = Path.Combine(registryValue, WINDOWS_10_KITS_PATH_POSTFIX);
+
+ if (!Directory.Exists(win10KitsPath))
+ {
+ Debug.LogWarning($"Could not find the Windows 10 SDK installation path via registry. Reverting to default path.");
+ win10KitsPath = WINDOWS_10_KITS_DEFAULT_PATH;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning($"Could not find the Windows 10 SDK installation path via registry. Reverting to default path. {e}");
+ win10KitsPath = WINDOWS_10_KITS_DEFAULT_PATH;
+ }
+#endif
+ var windowsSdkPaths = Directory.GetDirectories(win10KitsPath);
+ for (int i = 0; i < windowsSdkPaths.Length; i++)
+ {
+ windowsSdkVersions.Add(new Version(windowsSdkPaths[i].Substring(windowsSdkPaths[i].LastIndexOf(@"\", StringComparison.Ordinal) + 1)));
+ }
+
+ // There is no well-defined enumeration of Directory.GetDirectories, so the list
+ // is sorted prior to use later in this class.
+ windowsSdkVersions.Sort();
+ }
+
+ #endregion Utilities
+
+ #region Device Portal Commands
+
+ private static void OpenDevicePortal()
+ {
+ DevicePortal.OpenWebPortal(CurrentConnection);
+ }
+
+ private static async void ExecuteAction(Func exec)
+ {
+ await ExecuteActionAsync(exec);
+ }
+
+ private static async Task ExecuteActionAsync(Func exec)
+ {
+ List targetDevices;
+
+ if (UwpBuildDeployPreferences.TargetAllConnections)
+ {
+ targetDevices = new List() { localConnection };
+ targetDevices.AddRange(portalConnections.Connections);
+ }
+ else
+ {
+ targetDevices = new List() { CurrentConnection };
+ }
+
+ await ExecuteActionOnDevicesAsync(exec, targetDevices);
+ }
+
+ private static async Task ExecuteActionOnDevicesAsync(Func exec, List devices)
+ {
+ var installTasks = new List();
+ for (int i = 0; i < devices.Count; i++)
+ {
+ installTasks.Add(exec(devices[i]));
+ }
+
+ await Task.WhenAll(installTasks);
+ }
+
+ private static async Task InstallAppOnDeviceAsync(string buildPath, DeviceInfo targetDevice)
+ {
+ isAppRunning = false;
+ Debug.Log($"Initiating app install on device {targetDevice.ToString()}");
+
+ if (string.IsNullOrEmpty(PackageName))
+ {
+ Debug.LogWarning("No Package Name Found");
+ return;
+ }
+
+ if (UwpBuildDeployPreferences.FullReinstall)
+ {
+ await UninstallAppOnDeviceAsync(targetDevice);
+ }
+
+ if (IsLocalConnection(targetDevice) && (!IsHoloLensConnectedUsb || buildPath.Contains("x64")))
+ {
+ FileInfo[] installerFiles = new DirectoryInfo(buildPath).GetFiles("Install.ps1");
+ if (installerFiles.Length == 1)
+ {
+ var pInfo = new ProcessStartInfo
+ {
+ FileName = "powershell.exe",
+ CreateNoWindow = false,
+ Arguments = $"-executionpolicy bypass -File \"{installerFiles[0].FullName}\""
+ };
+
+ var process = new Process { StartInfo = pInfo };
+
+ process.Start();
+ }
+
+ return;
+ }
+
+ if (buildPath.Contains("x64"))
+ {
+ Debug.Log("Cannot install a x64 app on HoloLens");
+ return;
+ }
+
+ // Get the appx path
+ FileInfo[] files = new DirectoryInfo(buildPath).GetFiles("*.appx");
+ files = files.Length == 0 ? new DirectoryInfo(buildPath).GetFiles("*.appxbundle") : files;
+ files = files.Length == 0 ? new DirectoryInfo(buildPath).GetFiles("*.msix") : files;
+ files = files.Length == 0 ? new DirectoryInfo(buildPath).GetFiles("*.msixbundle") : files;
+
+ if (files.Length == 0)
+ {
+ Debug.LogErrorFormat("No APPX or MSIX found in folder build folder ({0})", buildPath);
+ return;
+ }
+
+ await DevicePortal.InstallAppAsync(files[0].FullName, targetDevice);
+ }
+
+ private static async Task UninstallAppOnDeviceAsync(DeviceInfo currentConnection)
+ {
+ isAppRunning = false;
+
+ if (string.IsNullOrEmpty(PackageName))
+ {
+ Debug.LogWarning("No Package Name Found");
+ return;
+ }
+
+ if (IsLocalConnection(currentConnection) && !IsHoloLensConnectedUsb)
+ {
+ var pInfo = new ProcessStartInfo
+ {
+ FileName = "powershell.exe",
+ CreateNoWindow = true,
+ Arguments = $"-windowstyle hidden -nologo Get-AppxPackage *{PackageName}* | Remove-AppxPackage"
+ };
+
+ var process = new Process { StartInfo = pInfo };
+ process.Start();
+ }
+ else
+ {
+ if (await DevicePortal.IsAppInstalledAsync(PackageName, currentConnection))
+ {
+ await DevicePortal.UninstallAppAsync(PackageName, currentConnection);
+ }
+ }
+ }
+
+ private static async Task LaunchAppOnDeviceAsync(DeviceInfo targetDevice)
+ {
+ if (string.IsNullOrEmpty(PackageName) ||
+ IsLocalConnection(targetDevice) && !IsHoloLensConnectedUsb)
+ {
+ return;
+ }
+
+ if (!await DevicePortal.IsAppRunningAsync(PackageName, targetDevice))
+ {
+ isAppRunning = await DevicePortal.LaunchAppAsync(PackageName, targetDevice);
+ }
+ }
+
+ private static async Task KillAppOnDeviceAsync(DeviceInfo targetDevice)
+ {
+ if (string.IsNullOrEmpty(PackageName) ||
+ IsLocalConnection(targetDevice) && !IsHoloLensConnectedUsb)
+ {
+ return;
+ }
+
+ if (await DevicePortal.IsAppRunningAsync(PackageName, targetDevice))
+ {
+ isAppRunning = !await DevicePortal.StopAppAsync(PackageName, targetDevice);
+ }
+ }
+
+ private static async Task OpenLogFileForDeviceAsync(DeviceInfo targetDevice, string localLogPath)
+ {
+ if (string.IsNullOrEmpty(PackageName))
+ {
+ return;
+ }
+
+ if (IsLocalConnection(targetDevice) && File.Exists(localLogPath))
+ {
+ Process.Start(localLogPath);
+ return;
+ }
+
+ if (!IsLocalConnection(targetDevice) || IsHoloLensConnectedUsb)
+ {
+ string logFilePath = await DevicePortal.DownloadLogFileAsync(PackageName, targetDevice);
+
+ if (!string.IsNullOrEmpty(logFilePath))
+ {
+ try
+ {
+ Process.Start(logFilePath);
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"Failed to open {logFilePath}!\n{e.Message}");
+ }
+ }
+
+ return;
+ }
+
+ Debug.Log("No Log Found");
+ }
+
+ #endregion Device Portal Commands
+ }
+}
diff --git a/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs.meta b/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs.meta
new file mode 100644
index 00000000000..dc446902716
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0e458e04038177440bb68f19a0d598ae
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs b/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs
new file mode 100644
index 00000000000..4422b67db99
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.MixedReality.Toolkit.Editor.Inspectors")]
+namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
+{
+ ///
+ /// List of the stages of the project configurator
+ ///
+ internal enum ConfigurationStage
+ {
+ Init = 0,
+ SelectXRSDKPlugin = 100,
+ InstallOpenXR = 101,
+ InstallMSOpenXR = 102,
+ InstallBuiltinPlugin = 150,
+ ProjectConfiguration = 200,
+ ImportTMP = 300,
+ ShowExamples = 400,
+ Done = 500
+ };
+}
diff --git a/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs.meta b/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs.meta
new file mode 100644
index 00000000000..43bc056897d
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 606ad949ea9d452586f5f7fde697eaa6
+timeCreated: 1663171394
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs b/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs
new file mode 100644
index 00000000000..ac73555270b
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using UnityEditor;
+using UnityEngine;
+
+namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
+{
+ public static class EditorAssemblyReloadManager
+ {
+ private static bool locked = false;
+
+ ///
+ /// Locks the Editor's ability to reload assemblies.
+ ///
+ ///
+ /// This is useful for ensuring async tasks complete in the editor without having to worry if any script
+ /// changes that happen during the running task will cancel it when the editor re-compiles the assemblies.
+ ///
+ public static bool LockReloadAssemblies
+ {
+ set
+ {
+ locked = value;
+
+ if (locked)
+ {
+ EditorApplication.LockReloadAssemblies();
+
+ if ((EditorWindow.focusedWindow != null) &&
+ !Application.isBatchMode)
+ {
+ EditorWindow.focusedWindow.ShowNotification(new GUIContent("Assembly reloading temporarily paused."));
+ }
+ }
+ else
+ {
+ EditorApplication.UnlockReloadAssemblies();
+ EditorApplication.delayCall += () => AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
+
+ if ((EditorWindow.focusedWindow != null) &&
+ !Application.isBatchMode)
+ {
+ EditorWindow.focusedWindow.ShowNotification(new GUIContent("Assembly reloading resumed."));
+ }
+ }
+ }
+ get => locked;
+ }
+ }
+}
diff --git a/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs.meta b/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs.meta
new file mode 100644
index 00000000000..9e6e694f945
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 74c541b64cf647c7bac1587122d0ff28
+timeCreated: 1663170204
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/Extensions.meta b/com.microsoft.mrtk.buildwindow/Extensions.meta
new file mode 100644
index 00000000000..4cd9a77a292
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/Extensions.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 15379ca1bb754f42a05446dccbfec8f8
+timeCreated: 1663169587
\ No newline at end of file
diff --git a/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs b/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs
new file mode 100644
index 00000000000..b3281d27a16
--- /dev/null
+++ b/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs
@@ -0,0 +1,403 @@
+// MIT License
+
+// Copyright(c) 2016 Modest Tree Media Inc
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.ExceptionServices;
+using System.Text;
+using System.Threading;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace Microsoft.MixedReality.Toolkit.Utilities
+{
+ ///
+ /// We could just add a generic GetAwaiter to YieldInstruction and CustomYieldInstruction
+ /// but instead we add specific methods to each derived class to allow for return values
+ /// that make the most sense for the specific instruction type.
+ ///
+ public static class AwaiterExtensions
+ {
+ public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSeconds instruction)
+ {
+ return GetAwaiterReturnVoid(instruction);
+ }
+
+ public static SimpleCoroutineAwaiter GetAwaiter(this WaitForUpdate instruction)
+ {
+ return GetAwaiterReturnVoid(instruction);
+ }
+
+ public static SimpleCoroutineAwaiter GetAwaiter(this WaitForEndOfFrame instruction)
+ {
+ return GetAwaiterReturnVoid(instruction);
+ }
+
+ public static SimpleCoroutineAwaiter GetAwaiter(this WaitForFixedUpdate instruction)
+ {
+ return GetAwaiterReturnVoid(instruction);
+ }
+
+ public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSecondsRealtime instruction)
+ {
+ return GetAwaiterReturnVoid(instruction);
+ }
+
+ public static SimpleCoroutineAwaiter GetAwaiter(this WaitUntil instruction)
+ {
+ return GetAwaiterReturnVoid(instruction);
+ }
+
+ public static SimpleCoroutineAwaiter GetAwaiter(this WaitWhile instruction)
+ {
+ return GetAwaiterReturnVoid(instruction);
+ }
+
+ public static SimpleCoroutineAwaiter GetAwaiter(this AsyncOperation instruction)
+ {
+ return GetAwaiterReturnSelf(instruction);
+ }
+
+ public static SimpleCoroutineAwaiter