// Copyright (c) Microsoft Corporation. // Copyright (c) Rafael Rivera. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Xml; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; namespace HoloToolkit.Unity { /// <summary> /// 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) /// </summary> public class BuildDeployWindow : EditorWindow { private const float UpdateBuildsPeriod = 1.0f; private const string SdkVersion = #if UNITY_2017_2_OR_NEWER "10.0.16299.0"; #else "10.0.15063.0"; #endif private readonly string[] tabNames = { "Unity Build Options", "Appx Build Options", "Deploy Options" }; private readonly string[] scriptingBackendNames = { "IL2CPP", ".NET" }; private readonly int[] scriptingBackendEnum = { (int)ScriptingImplementation.IL2CPP, (int)ScriptingImplementation.WinRTDotNET }; private readonly string[] deviceNames = { "Any Device", "PC", "Mobile", "HoloLens" }; private readonly List<string> builds = new List<string>(0); private static readonly List<string> appPackageDirectories = new List<string>(0); #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("Unity C# Projects", "Generate C# Project References for debugging"); 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 pairHoloLensUsbLabel = new GUIContent("Pair HoloLens", "Pairs the USB connected HoloLens with the Build Window so you can deploy via USB"); private readonly GUIContent useSSLLabel = new GUIContent("Use SSL?", "Use SLL to communicate with Device Portal"); 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", "Note: Local Machine will install on any HoloLens connected to USB as well."); private readonly GUIContent doAllLabel = new GUIContent(" Do actions on all devices", "Should the build options perform actions on all the connected devices?"); private readonly GUIContent uninstallLabel = new GUIContent("Uninstall First", "Uninstall application before installing"); #endregion private enum BuildDeployTab { UnityBuildOptions, AppxBuildOptions, DeployOptions } private enum BuildPlatformEnum { x86 = 1, x64 = 2 } private enum BuildConfigEnum { Debug = 0, Release = 1, Master = 2 } #region Properties private static bool ShouldOpenSLNBeEnabled { get { return !string.IsNullOrEmpty(BuildDeployPrefs.BuildDirectory); } } private static bool ShouldBuildSLNBeEnabled { get { return !string.IsNullOrEmpty(BuildDeployPrefs.BuildDirectory); } } private static bool ShouldBuildAppxBeEnabled { get { return ShouldBuildSLNBeEnabled && !string.IsNullOrEmpty(BuildDeployPrefs.BuildDirectory) && !string.IsNullOrEmpty(BuildDeployPrefs.MsBuildVersion) && !string.IsNullOrEmpty(BuildDeployPrefs.BuildConfig); } } private static bool DevicePortalConnectionEnabled { get { return (portalConnections.Connections.Count > 1 || IsHoloLensConnectedUsb) && !string.IsNullOrEmpty(BuildDeployPrefs.BuildDirectory); } } private static bool CanInstall { get { return Directory.Exists(BuildDeployPrefs.AbsoluteBuildDirectory) && !string.IsNullOrEmpty(familyPackageName); } } private static string familyPackageName; 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; } } #endregion // Properties #region Fields private int halfWidth; private int quarterWidth; private float timeLastUpdatedBuilds; private string[] targetIps; private string[] windowsSdkPaths; private Vector2 scrollPosition; private BuildDeployTab currentTab = BuildDeployTab.UnityBuildOptions; private static bool isAppRunning; private static int currentConnectionInfoIndex; private static DevicePortalConnections portalConnections; #endregion // Fields #region Methods [MenuItem("Mixed Reality Toolkit/Build Window", false, 0)] public static void OpenWindow() { // Dock it next to the Scene View. var window = GetWindow<BuildDeployWindow>(typeof(SceneView)); window.titleContent = new GUIContent("Build Window"); window.Show(); } private void OnEnable() { Setup(); } private void Setup() { titleContent = new GUIContent("Build Window"); minSize = new Vector2(512, 256); 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); } UpdateBuilds(); portalConnections = JsonUtility.FromJson<DevicePortalConnections>(BuildDeployPrefs.DevicePortalConnections); UpdatePortalConnections(); } private void OnGUI() { quarterWidth = Screen.width / 4; halfWidth = Screen.width / 2; #region Quick Options if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WSAPlayer) { EditorGUILayout.HelpBox("Build window only available for UWP build target.", MessageType.Warning); GUILayout.BeginVertical(); GUILayout.Space(5); EditorGUILayout.BeginHorizontal(); // Build directory (and save setting, if it's changed) string curBuildDirectory = BuildDeployPrefs.BuildDirectory; EditorGUILayout.LabelField(buildDirectoryLabel, GUILayout.Width(96)); string newBuildDirectory = EditorGUILayout.TextField( curBuildDirectory, GUILayout.Width(64), GUILayout.ExpandWidth(true)); if (newBuildDirectory != curBuildDirectory) { BuildDeployPrefs.BuildDirectory = newBuildDirectory; } GUI.enabled = Directory.Exists(BuildDeployPrefs.AbsoluteBuildDirectory); if (GUILayout.Button("Open Build Directory", GUILayout.Width(quarterWidth))) { EditorApplication.delayCall += () => Process.Start(BuildDeployPrefs.AbsoluteBuildDirectory); } GUI.enabled = true; if (GUILayout.Button("Open Player Settings", GUILayout.Width(quarterWidth))) { EditorApplication.ExecuteMenuItem("Edit/Project Settings/Player"); } EditorGUILayout.EndHorizontal(); GUILayout.EndVertical(); return; } GUILayout.BeginVertical(); GUILayout.Space(5); GUILayout.Label("Quick Options"); EditorGUILayout.BeginHorizontal(); EditorUserBuildSettings.wsaSubtarget = (WSASubtarget)EditorGUILayout.Popup((int)EditorUserBuildSettings.wsaSubtarget, deviceNames); GUI.enabled = ShouldBuildSLNBeEnabled; bool canInstall = CanInstall; // Build & Run button... if (GUILayout.Button(CanInstall ? buildAllThenInstallLabel : buildAllLabel, GUILayout.Width(halfWidth - 20))) { EditorApplication.delayCall += () => { BuildAll(canInstall); }; } GUI.enabled = true; if (GUILayout.Button("Open Player Settings", GUILayout.Width(quarterWidth))) { EditorApplication.ExecuteMenuItem("Edit/Project Settings/Player"); } // If Xbox Controller support is enabled and we're targeting the HoloLens device, // Enable the HID capability. if (EditorUserBuildSettings.wsaSubtarget == WSASubtarget.HoloLens) { PlayerSettings.WSA.SetCapability( PlayerSettings.WSACapability.HumanInterfaceDevice, EditorPrefsUtility.GetEditorPref("Enable Xbox Controller Support", false)); BuildDeployPrefs.BuildPlatform = BuildPlatformEnum.x86.ToString(); } else { PlayerSettings.WSA.SetCapability(PlayerSettings.WSACapability.HumanInterfaceDevice, false); } EditorGUILayout.EndHorizontal(); GUILayout.EndVertical(); GUILayout.Space(10); #endregion currentTab = (BuildDeployTab)GUILayout.Toolbar(SessionState.GetInt("_MRTK_BuildWindow_Tab", (int)currentTab), tabNames); SessionState.SetInt("_MRTK_BuildWindow_Tab", (int)currentTab); GUILayout.Space(10); switch (currentTab) { case BuildDeployTab.UnityBuildOptions: UnityBuildGUI(); break; case BuildDeployTab.AppxBuildOptions: AppxBuildGUI(); break; case BuildDeployTab.DeployOptions: DeployGUI(); break; default: throw new ArgumentOutOfRangeException(); } } private void Update() { if (Time.realtimeSinceStartup - timeLastUpdatedBuilds > UpdateBuildsPeriod) { UpdateBuilds(); } } private void UnityBuildGUI() { GUILayout.BeginVertical(); EditorGUILayout.BeginHorizontal(); // Build directory (and save setting, if it's changed) string curBuildDirectory = BuildDeployPrefs.BuildDirectory; EditorGUILayout.LabelField(buildDirectoryLabel, GUILayout.Width(96)); string newBuildDirectory = EditorGUILayout.TextField( curBuildDirectory, GUILayout.Width(64), GUILayout.ExpandWidth(true)); if (newBuildDirectory != curBuildDirectory) { BuildDeployPrefs.BuildDirectory = newBuildDirectory; } GUI.enabled = Directory.Exists(BuildDeployPrefs.AbsoluteBuildDirectory); if (GUILayout.Button("Open Build Directory", GUILayout.Width(halfWidth))) { EditorApplication.delayCall += () => Process.Start(BuildDeployPrefs.AbsoluteBuildDirectory); } GUI.enabled = true; EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUI.enabled = ShouldOpenSLNBeEnabled; if (GUILayout.Button("Open in Visual Studio", GUILayout.Width(halfWidth))) { // Open SLN string slnFilename = Path.Combine(BuildDeployPrefs.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 += () => BuildDeployTools.BuildSLN(BuildDeployPrefs.BuildDirectory); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); // Generate C# Project References for debugging GUILayout.FlexibleSpace(); var previousLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 105; bool generateReferenceProjects = EditorUserBuildSettings.wsaGenerateReferenceProjects; bool shouldGenerateProjects = EditorGUILayout.Toggle(useCSharpProjectsLabel, generateReferenceProjects); if (shouldGenerateProjects != generateReferenceProjects) { EditorUserBuildSettings.wsaGenerateReferenceProjects = shouldGenerateProjects; } EditorGUIUtility.labelWidth = previousLabelWidth; // Build Unity Player GUI.enabled = ShouldBuildSLNBeEnabled; if (GUILayout.Button("Build Unity Project", GUILayout.Width(halfWidth))) { EditorApplication.delayCall += () => BuildDeployTools.BuildSLN(BuildDeployPrefs.BuildDirectory); } GUI.enabled = true; EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } private void AppxBuildGUI() { GUILayout.BeginVertical(); GUILayout.BeginHorizontal(); // SDK and MS Build Version(and save setting, if it's changed) string currentSDKVersion = EditorUserBuildSettings.wsaUWPSDK; int currentSDKVersionIndex = -1; for (var i = 0; i < windowsSdkPaths.Length; i++) { if (string.IsNullOrEmpty(currentSDKVersion)) { currentSDKVersionIndex = windowsSdkPaths.Length - 1; } else { if (windowsSdkPaths[i].Equals(SdkVersion)) { currentSDKVersionIndex = i; } } } EditorGUILayout.LabelField("Required SDK Version: " + SdkVersion); // Throw exception if user has no Windows 10 SDK installed if (currentSDKVersionIndex < 0) { Debug.LogErrorFormat("Unable to find the required Windows 10 SDK Target!\n" + "Please be sure to install the {0} SDK from Visual Studio Installer.", SdkVersion); GUILayout.EndHorizontal(); EditorGUILayout.HelpBox(string.Format("Unable to find the required Windows 10 SDK Target!\n" + "Please be sure to install the {0} SDK from Visual Studio Installer.", SdkVersion), MessageType.Error); GUILayout.BeginHorizontal(); } var curScriptingBackend = PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA); var newScriptingBackend = (ScriptingImplementation)EditorGUILayout.IntPopup( "Scripting Backend", (int)curScriptingBackend, scriptingBackendNames, scriptingBackendEnum, GUILayout.Width(halfWidth)); if (newScriptingBackend != curScriptingBackend) { bool canUpdate = !Directory.Exists(BuildDeployPrefs.AbsoluteBuildDirectory); if (!canUpdate && EditorUtility.DisplayDialog("Attention!", string.Format("Build path contains project built with {0} scripting backend, while current project is using {1} scripting backend.\n\n" + "Switching to a new scripting backend requires us to delete all the data currently in your build folder and rebuild the Unity Player!", newScriptingBackend.ToString(), curScriptingBackend.ToString()), "Okay", "Cancel")) { Directory.Delete(BuildDeployPrefs.AbsoluteBuildDirectory, true); } if (canUpdate) { PlayerSettings.SetScriptingBackend(BuildTargetGroup.WSA, newScriptingBackend); } } string newSDKVersion = windowsSdkPaths[currentSDKVersionIndex]; if (!newSDKVersion.Equals(currentSDKVersion)) { EditorUserBuildSettings.wsaUWPSDK = newSDKVersion; } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); // Build config (and save setting, if it's changed) string curBuildConfigString = BuildDeployPrefs.BuildConfig; BuildConfigEnum buildConfigOption; if (curBuildConfigString.ToLower().Equals("master")) { buildConfigOption = BuildConfigEnum.Master; } else if (curBuildConfigString.ToLower().Equals("release")) { buildConfigOption = BuildConfigEnum.Release; } else { buildConfigOption = BuildConfigEnum.Debug; } buildConfigOption = (BuildConfigEnum)EditorGUILayout.EnumPopup("Build Configuration", buildConfigOption, GUILayout.Width(halfWidth)); string buildConfigString = buildConfigOption.ToString(); if (buildConfigString != curBuildConfigString) { BuildDeployPrefs.BuildConfig = buildConfigString; } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); // Build Platform (and save setting, if it's changed) string curBuildPlatformString = BuildDeployPrefs.BuildPlatform; var buildPlatformOption = BuildPlatformEnum.x86; if (curBuildPlatformString.ToLower().Equals("x86")) { buildPlatformOption = BuildPlatformEnum.x86; } else if (curBuildPlatformString.ToLower().Equals("x64")) { buildPlatformOption = BuildPlatformEnum.x64; } buildPlatformOption = (BuildPlatformEnum)EditorGUILayout.EnumPopup("Build Platform", buildPlatformOption, GUILayout.Width(halfWidth)); string newBuildPlatformString; switch (buildPlatformOption) { case BuildPlatformEnum.x86: case BuildPlatformEnum.x64: newBuildPlatformString = buildPlatformOption.ToString(); break; default: throw new ArgumentOutOfRangeException(); } if (newBuildPlatformString != curBuildPlatformString) { BuildDeployPrefs.BuildPlatform = newBuildPlatformString; } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); var previousLabelWidth = EditorGUIUtility.labelWidth; // Auto Increment version EditorGUIUtility.labelWidth = 96; bool curIncrementVersion = BuildDeployPrefs.IncrementBuildVersion; bool newIncrementVersion = EditorGUILayout.Toggle(autoIncrementLabel, curIncrementVersion); // Restore previous label width EditorGUIUtility.labelWidth = previousLabelWidth; if (newIncrementVersion != curIncrementVersion) { BuildDeployPrefs.IncrementBuildVersion = newIncrementVersion; } EditorGUILayout.LabelField(versionNumberLabel, GUILayout.Width(96)); Vector3 newVersion = Vector3.zero; EditorGUI.BeginChangeCheck(); newVersion.x = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Major, GUILayout.Width(quarterWidth / 2 - 3)); newVersion.y = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Minor, GUILayout.Width(quarterWidth / 2 - 3)); newVersion.z = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Build, GUILayout.Width(quarterWidth / 2 - 3)); if (EditorGUI.EndChangeCheck()) { PlayerSettings.WSA.packageVersion = new Version((int)newVersion.x, (int)newVersion.y, (int)newVersion.z, 0); } GUI.enabled = false; EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Revision, GUILayout.Width(quarterWidth / 2 - 3)); GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); // Force rebuild previousLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 50; bool curForceRebuildAppx = BuildDeployPrefs.ForceRebuild; bool newForceRebuildAppx = EditorGUILayout.Toggle("Rebuild", curForceRebuildAppx); if (newForceRebuildAppx != curForceRebuildAppx) { BuildDeployPrefs.ForceRebuild = newForceRebuildAppx; } // Restore previous label width EditorGUIUtility.labelWidth = previousLabelWidth; // Build APPX GUI.enabled = ShouldBuildAppxBeEnabled; if (GUILayout.Button("Build APPX", GUILayout.Width(halfWidth))) { // Check if SLN exists string slnFilename = Path.Combine(BuildDeployPrefs.BuildDirectory, PlayerSettings.productName + ".sln"); if (File.Exists(slnFilename)) { // Build APPX EditorApplication.delayCall += () => { BuildDeployTools.BuildAppxFromSLN( PlayerSettings.productName, BuildDeployTools.DefaultMSBuildVersion, BuildDeployPrefs.ForceRebuild, BuildDeployPrefs.BuildConfig, BuildDeployPrefs.BuildPlatform, BuildDeployPrefs.BuildDirectory, BuildDeployPrefs.IncrementBuildVersion); }; } else if (EditorUtility.DisplayDialog("Solution Not Found", "We couldn't find the solution. Would you like to Build it?", "Yes, Build", "No")) { // Build SLN then APPX EditorApplication.delayCall += () => BuildAll(install: false); } GUI.enabled = true; } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); // Open AppX packages location string appxBuildPath = Path.GetFullPath(BuildDeployPrefs.BuildDirectory + "/" + PlayerSettings.productName + "/AppPackages"); GUI.enabled = builds.Count > 0 && !string.IsNullOrEmpty(appxBuildPath); if (GUILayout.Button("Open APPX Packages Location", GUILayout.Width(halfWidth))) { EditorApplication.delayCall += () => Process.Start("explorer.exe", "/f /open," + appxBuildPath); } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.EndVertical(); } private void DeployGUI() { Debug.Assert(portalConnections.Connections.Count != 0); Debug.Assert(currentConnectionInfoIndex >= 0); GUILayout.BeginVertical(); EditorGUI.BeginChangeCheck(); GUILayout.BeginHorizontal(); // Launch app... GUI.enabled = IsHoloLensConnectedUsb; if (GUILayout.Button(pairHoloLensUsbLabel, GUILayout.Width(quarterWidth))) { EditorApplication.delayCall += () => { var newConnection = default(ConnectInfo); foreach (var targetDevice in portalConnections.Connections) { if (!IsLocalConnection(targetDevice)) { continue; } var machineName = BuildDeployPortal.GetMachineName(targetDevice); var networkInfo = BuildDeployPortal.GetNetworkInfo(targetDevice); if (networkInfo != null) { var newIps = new List<string>(); foreach (var adapter in networkInfo.Adapters) { newIps.AddRange(from address in adapter.IpAddresses where !address.IpAddress.Contains("0.0.0.0") select address.IpAddress); } if (newIps.Count == 0) { Debug.LogWarning("This HoloLens is not connected to any networks and cannot be paired."); } foreach (var ip in newIps) { if (portalConnections.Connections.Any(connection => connection.IP == ip)) { Debug.LogFormat("Already paired"); continue; } newConnection.IP = ip; newConnection.User = targetDevice.User; newConnection.Password = targetDevice.Password; if (machineName != null) { newConnection.MachineName = machineName.ComputerName; } } } } if (IsValidIpAddress(newConnection.IP)) { portalConnections.Connections.Add(newConnection); for (var i = 0; i < portalConnections.Connections.Count; i++) { if (portalConnections.Connections[i].IP == newConnection.IP) { currentConnectionInfoIndex = i; SessionState.SetInt("_MRTK_BuildWindow_CurrentDeviceIndex", currentConnectionInfoIndex); break; } } UpdatePortalConnections(); } }; } GUI.enabled = true; GUILayout.FlexibleSpace(); var previousLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 64; bool useSSL = EditorGUILayout.Toggle(useSSLLabel, BuildDeployPrefs.UseSSL); EditorGUIUtility.labelWidth = previousLabelWidth; currentConnectionInfoIndex = EditorGUILayout.Popup( SessionState.GetInt("_MRTK_BuildWindow_CurrentDeviceIndex", 0), targetIps, GUILayout.Width(halfWidth - 48)); var currentConnection = portalConnections.Connections[currentConnectionInfoIndex]; bool currentConnectionIsLocal = IsLocalConnection(currentConnection); if (currentConnectionIsLocal) { currentConnection.MachineName = "Local Machine"; } GUI.enabled = IsValidIpAddress(currentConnection.IP); if (GUILayout.Button(addConnectionLabel, GUILayout.Width(20))) { portalConnections.Connections.Add(new ConnectInfo("0.0.0.0", currentConnection.User, currentConnection.Password)); currentConnectionInfoIndex++; currentConnection = portalConnections.Connections[currentConnectionInfoIndex]; UpdatePortalConnections(); } GUI.enabled = portalConnections.Connections.Count > 1 && currentConnectionInfoIndex != 0; if (GUILayout.Button(removeConnectionLabel, GUILayout.Width(20))) { portalConnections.Connections.RemoveAt(currentConnectionInfoIndex); currentConnectionInfoIndex--; currentConnection = portalConnections.Connections[currentConnectionInfoIndex]; UpdatePortalConnections(); } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.Space(5); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label(currentConnection.MachineName, GUILayout.Width(halfWidth)); GUILayout.EndHorizontal(); previousLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 64; GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUI.enabled = !currentConnectionIsLocal; currentConnection.IP = EditorGUILayout.TextField( ipAddressLabel, currentConnection.IP, GUILayout.Width(halfWidth)); GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); currentConnection.User = EditorGUILayout.TextField("Username", currentConnection.User, GUILayout.Width(halfWidth)); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); currentConnection.Password = EditorGUILayout.PasswordField("Password", currentConnection.Password, GUILayout.Width(halfWidth)); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); EditorGUIUtility.labelWidth = 152; bool processAll = EditorGUILayout.Toggle( doAllLabel, BuildDeployPrefs.TargetAllConnections, GUILayout.Width(176)); EditorGUIUtility.labelWidth = 86; bool fullReinstall = EditorGUILayout.Toggle( uninstallLabel, BuildDeployPrefs.FullReinstall, GUILayout.ExpandWidth(false)); EditorGUIUtility.labelWidth = previousLabelWidth; if (EditorGUI.EndChangeCheck()) { SessionState.SetInt("_MRTK_BuildWindow_CurrentDeviceIndex", currentConnectionInfoIndex); BuildDeployPrefs.TargetAllConnections = processAll; BuildDeployPrefs.FullReinstall = fullReinstall; BuildDeployPrefs.UseSSL = useSSL; // Format our local connection if (currentConnection.IP.Contains("127.0.0.1")) { currentConnection.IP = "Local Machine"; } portalConnections.Connections[currentConnectionInfoIndex] = currentConnection; UpdatePortalConnections(); Repaint(); } GUILayout.FlexibleSpace(); // Connect if (!IsLocalConnection(currentConnection)) { GUI.enabled = IsValidIpAddress(currentConnection.IP) && IsCredentialsValid(currentConnection); if (GUILayout.Button("Connect", GUILayout.Width(quarterWidth))) { EditorApplication.delayCall += () => { var machineName = BuildDeployPortal.GetMachineName(currentConnection); if (machineName != null) { currentConnection.MachineName = machineName.ComputerName; } portalConnections.Connections[currentConnectionInfoIndex] = currentConnection; UpdatePortalConnections(); Repaint(); }; } GUI.enabled = true; } GUI.enabled = DevicePortalConnectionEnabled && CanInstall; // Open web portal if (GUILayout.Button("Open Device Portal", GUILayout.Width(quarterWidth))) { EditorApplication.delayCall += () => OpenDevicePortal(portalConnections); } GUI.enabled = true; GUILayout.EndHorizontal(); // Build list if (builds.Count == 0) { GUILayout.Label("*** No builds found in build directory", EditorStyles.boldLabel); } else { EditorGUILayout.Separator(); GUILayout.BeginVertical(GUILayout.ExpandHeight(true)); scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); foreach (var fullBuildLocation in builds) { int lastBackslashIndex = fullBuildLocation.LastIndexOf("\\", StringComparison.Ordinal); var directoryDate = Directory.GetLastWriteTime(fullBuildLocation).ToString("yyyy/MM/dd HH:mm:ss"); string packageName = fullBuildLocation.Substring(lastBackslashIndex + 1); GUILayout.Space(2); EditorGUILayout.BeginHorizontal(); GUILayout.Space(12); GUI.enabled = CanInstall; if (GUILayout.Button("Install", GUILayout.Width(96))) { EditorApplication.delayCall += () => { if (processAll) { InstallAppOnDevicesList(fullBuildLocation, portalConnections); } else { InstallOnTargetDevice(fullBuildLocation, currentConnection); } }; } GUI.enabled = true; // Uninstall... GUI.enabled = CanInstall; if (GUILayout.Button("Uninstall", GUILayout.Width(96))) { EditorApplication.delayCall += () => { if (processAll) { UninstallAppOnDevicesList(portalConnections); } else { UninstallAppOnTargetDevice(familyPackageName, currentConnection); } }; } GUI.enabled = true; bool canLaunchLocal = currentConnectionInfoIndex == 0 && IsHoloLensConnectedUsb; bool canLaunchRemote = DevicePortalConnectionEnabled && CanInstall && currentConnectionInfoIndex != 0; // Launch app... GUI.enabled = canLaunchLocal || canLaunchRemote; if (GUILayout.Button(new GUIContent(isAppRunning ? "Kill App" : "Launch App", "These are remote commands only"), GUILayout.Width(96))) { EditorApplication.delayCall += () => { if (isAppRunning) { if (processAll) { KillAppOnDeviceList(portalConnections); isAppRunning = false; } else { isAppRunning = !KillAppOnTargetDevice(currentConnection); } } else { if (processAll) { LaunchAppOnDeviceList(portalConnections); isAppRunning = true; } else { isAppRunning = LaunchAppOnTargetDevice(currentConnection); } } }; } GUI.enabled = true; // Log file string localLogPath = string.Format("%USERPROFILE%\\AppData\\Local\\Packages\\{0}\\TempState\\UnityPlayer.log", PlayerSettings.productName); bool localLogExists = File.Exists(localLogPath); GUI.enabled = localLogExists || canLaunchRemote || canLaunchLocal; if (GUILayout.Button("View Log", GUILayout.Width(96))) { EditorApplication.delayCall += () => { if (processAll) { OpenLogFilesOnDeviceList(portalConnections, localLogPath); } else { OpenLogFileForTargetDevice(currentConnection, localLogPath); } }; } GUI.enabled = true; GUILayout.Space(8); GUILayout.Label(new GUIContent(packageName + " (" + directoryDate + ")")); EditorGUILayout.EndHorizontal(); } GUILayout.EndScrollView(); GUILayout.EndVertical(); } GUILayout.EndVertical(); } #endregion // Methods #region Utilities private void BuildAll(bool install = true) { // First build SLN if (!BuildDeployTools.BuildSLN(BuildDeployPrefs.BuildDirectory, false)) { return; } // Next, APPX if (!BuildDeployTools.BuildAppxFromSLN( PlayerSettings.productName, BuildDeployPrefs.MsBuildVersion, BuildDeployPrefs.ForceRebuild, BuildDeployPrefs.BuildConfig, BuildDeployPrefs.BuildPlatform, BuildDeployPrefs.BuildDirectory, BuildDeployPrefs.IncrementBuildVersion, showDialog: !install)) { return; } // Next, Install if (install) { string fullBuildLocation = CalcMostRecentBuild(); if (BuildDeployPrefs.TargetAllConnections) { InstallAppOnDevicesList(fullBuildLocation, portalConnections); } else { InstallOnTargetDevice(fullBuildLocation, portalConnections.Connections[currentConnectionInfoIndex]); } } } private void UpdateBuilds() { builds.Clear(); try { appPackageDirectories.Clear(); string[] buildList = Directory.GetDirectories(BuildDeployPrefs.AbsoluteBuildDirectory, "*", SearchOption.AllDirectories); foreach (string appBuild in buildList) { if (appBuild.Contains("AppPackages") && !appBuild.Contains("AppPackages\\")) { appPackageDirectories.AddRange(Directory.GetDirectories(appBuild)); } } IEnumerable<string> selectedDirectories = from string directory in appPackageDirectories orderby Directory.GetLastWriteTime(directory) descending select Path.GetFullPath(directory); builds.AddRange(selectedDirectories); } catch (DirectoryNotFoundException) { // unused } familyPackageName = CalcPackageFamilyName(); timeLastUpdatedBuilds = Time.realtimeSinceStartup; } private 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 UpdatePortalConnections() { targetIps = new string[portalConnections.Connections.Count]; if (currentConnectionInfoIndex > portalConnections.Connections.Count - 1) { currentConnectionInfoIndex = portalConnections.Connections.Count - 1; } targetIps[0] = "Local Machine"; for (int i = 1; i < targetIps.Length; i++) { targetIps[i] = portalConnections.Connections[i].MachineName; } BuildDeployPrefs.DevicePortalConnections = JsonUtility.ToJson(portalConnections); Repaint(); } private static bool IsLocalConnection(ConnectInfo connection) { return connection.IP.Contains("Local Machine") || connection.IP.Contains("127.0.0.1"); } private static bool IsCredentialsValid(ConnectInfo connection) { return !string.IsNullOrEmpty(connection.User) && !string.IsNullOrEmpty(connection.IP); } private static bool IsValidIpAddress(string ip) { if (string.IsNullOrEmpty(ip)) { return false; } if (ip.Contains("Local Machine")) { return true; } if (ip.Contains("0.0.0.0")) { return false; } var subAddresses = ip.Split('.'); return subAddresses.Length > 3; } private static string CalcPackageFamilyName() { if (appPackageDirectories.Count == 0) { return string.Empty; } // Find the manifest string[] manifests = Directory.GetFiles(BuildDeployPrefs.AbsoluteBuildDirectory, "Package.appxmanifest", SearchOption.AllDirectories); if (manifests.Length == 0) { Debug.LogError("Unable to find manifest file for build (in path - " + BuildDeployPrefs.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 reader.Value; } } } break; } } } Debug.LogError("Unable to find PackageFamilyName in manifest file (" + manifest + ")"); return string.Empty; } #endregion #region Device Portal Commands private static void OpenDevicePortal(DevicePortalConnections targetDevices) { MachineName usbMachine = null; if (IsHoloLensConnectedUsb) { usbMachine = BuildDeployPortal.GetMachineName(targetDevices.Connections.FirstOrDefault(targetDevice => targetDevice.IP.Contains("Local Machine"))); } for (int i = 0; i < targetDevices.Connections.Count; i++) { bool isLocalMachine = IsLocalConnection(targetDevices.Connections[i]); if (isLocalMachine && !IsHoloLensConnectedUsb) { continue; } if (IsHoloLensConnectedUsb) { if (isLocalMachine || usbMachine != null && usbMachine.ComputerName != targetDevices.Connections[i].MachineName) { BuildDeployPortal.OpenWebPortal(targetDevices.Connections[i]); } } else { if (!isLocalMachine) { BuildDeployPortal.OpenWebPortal(targetDevices.Connections[i]); } } } } private static void InstallOnTargetDevice(string buildPath, ConnectInfo targetDevice) { isAppRunning = false; string packageFamilyName = CalcPackageFamilyName(); if (string.IsNullOrEmpty(packageFamilyName)) { return; } if (IsLocalConnection(targetDevice) && !IsHoloLensConnectedUsb || buildPath.Contains("x64")) { FileInfo[] installerFiles = new DirectoryInfo(buildPath).GetFiles("*.ps1"); if (installerFiles.Length == 1) { var pInfo = new ProcessStartInfo { FileName = "powershell.exe", CreateNoWindow = false, Arguments = string.Format("-executionpolicy bypass -File \"{0}\"", installerFiles[0].FullName) }; var process = new Process { StartInfo = pInfo }; process.Start(); } return; } if (buildPath.Contains("x64")) { return; } // Get the appx path FileInfo[] files = new DirectoryInfo(buildPath).GetFiles("*.appx"); files = files.Length == 0 ? new DirectoryInfo(buildPath).GetFiles("*.appxbundle") : files; if (files.Length == 0) { Debug.LogErrorFormat("No APPX found in folder build folder ({0})", buildPath); return; } BuildDeployPortal.IsAppInstalled(packageFamilyName, targetDevice); // Kick off the install BuildDeployPortal.InstallApp(files[0].FullName, targetDevice); } private static void InstallAppOnDevicesList(string buildPath, DevicePortalConnections targetList) { string packageFamilyName = CalcPackageFamilyName(); if (string.IsNullOrEmpty(packageFamilyName)) { return; } if (BuildDeployPrefs.FullReinstall) { UninstallAppOnDevicesList(targetList); } try { for (int i = 0; i < targetList.Connections.Count; i++) { EditorUtility.DisplayProgressBar("Installing on devices", string.Format("Installing on {0}", targetList.Connections[i].MachineName), i / (float)targetList.Connections.Count); InstallOnTargetDevice(buildPath, targetList.Connections[i]); } } catch (Exception e) { Debug.LogError(e.Message); } EditorUtility.ClearProgressBar(); } private static void UninstallAppOnTargetDevice(string packageFamilyName, ConnectInfo currentConnection, bool showDialog = true) { isAppRunning = false; if (IsLocalConnection(currentConnection) && !IsHoloLensConnectedUsb) { var pInfo = new ProcessStartInfo { FileName = "powershell.exe", CreateNoWindow = true, Arguments = string.Format("-windowstyle hidden -nologo Get-AppxPackage *{0}* | Remove-AppxPackage", packageFamilyName) }; var process = new Process { StartInfo = pInfo }; process.Start(); } else { if (BuildDeployPortal.IsAppInstalled(packageFamilyName, currentConnection)) { BuildDeployPortal.UninstallApp(packageFamilyName, currentConnection, showDialog); } } } private static void UninstallAppOnDevicesList(DevicePortalConnections targetList) { string packageFamilyName = CalcPackageFamilyName(); if (string.IsNullOrEmpty(packageFamilyName)) { return; } try { for (int i = 0; i < targetList.Connections.Count; i++) { EditorUtility.DisplayProgressBar("Uninstalling on devices", string.Format("Uninstalling ({0})", targetList.Connections[i].IP), i / (float)targetList.Connections.Count); UninstallAppOnTargetDevice(packageFamilyName, targetList.Connections[i], false); } } catch (Exception e) { Debug.LogError(e.ToString()); } EditorUtility.ClearProgressBar(); } private static bool LaunchAppOnTargetDevice(ConnectInfo targetDevice, bool showDialog = true) { string packageFamilyName = CalcPackageFamilyName(); if (string.IsNullOrEmpty(packageFamilyName)) { return false; } if (IsLocalConnection(targetDevice) && !IsHoloLensConnectedUsb) { return false; } if (showDialog) { EditorUtility.DisplayProgressBar("Launching Application", string.Format("Launching {0} on {1}", packageFamilyName, targetDevice.MachineName), 0.25f); } bool success = !BuildDeployPortal.IsAppRunning(PlayerSettings.productName, targetDevice) && BuildDeployPortal.LaunchApp(packageFamilyName, targetDevice, false); if (showDialog) { EditorUtility.ClearProgressBar(); } return success; } private static void LaunchAppOnDeviceList(DevicePortalConnections targetDevices) { for (int i = 0; i < targetDevices.Connections.Count; i++) { EditorUtility.DisplayProgressBar("Launching App on devices", string.Format("Launching on {0}", targetDevices.Connections[i].IP), i / (float)targetDevices.Connections.Count); LaunchAppOnTargetDevice(targetDevices.Connections[i], false); } } private static bool KillAppOnTargetDevice(ConnectInfo targetDevice, bool showDialog = true) { string packageFamilyName = CalcPackageFamilyName(); if (string.IsNullOrEmpty(packageFamilyName)) { return false; } if (IsLocalConnection(targetDevice) && !IsHoloLensConnectedUsb) { return false; } if (showDialog) { EditorUtility.DisplayProgressBar("Stopping Application", string.Format("Stopping {0} on {1}", packageFamilyName, targetDevice.MachineName), 0.5f); } bool success = BuildDeployPortal.IsAppRunning(PlayerSettings.productName, targetDevice) && BuildDeployPortal.KillApp(packageFamilyName, targetDevice, false); if (showDialog) { EditorUtility.ClearProgressBar(); } return success; } private static void KillAppOnDeviceList(DevicePortalConnections targetDevices) { for (int i = 0; i < targetDevices.Connections.Count; i++) { EditorUtility.DisplayProgressBar("Stopping Application on devices", string.Format("Stopping on {0}", targetDevices.Connections[i].MachineName), i / (float)targetDevices.Connections.Count); KillAppOnTargetDevice(targetDevices.Connections[i], false); } } private static void OpenLogFileForTargetDevice(ConnectInfo targetDevice, string localLogPath) { string packageFamilyName = CalcPackageFamilyName(); if (string.IsNullOrEmpty(packageFamilyName)) { return; } if (IsLocalConnection(targetDevice) && File.Exists(localLogPath)) { Process.Start(localLogPath); return; } if (!IsLocalConnection(targetDevice) || IsHoloLensConnectedUsb) { BuildDeployPortal.DeviceLogFile_View(packageFamilyName, targetDevice); return; } Debug.Log("No Log Found"); } private static void OpenLogFilesOnDeviceList(DevicePortalConnections targetDevices, string localLogPath) { for (int i = 0; i < targetDevices.Connections.Count; i++) { OpenLogFileForTargetDevice(targetDevices.Connections[i], localLogPath); } } #endregion } }