719 lines
30 KiB
C#
719 lines
30 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// Query the build process to see if we're already building.
|
|
/// </summary>
|
|
public static bool IsBuilding { get; private set; } = false;
|
|
|
|
/// <summary>
|
|
/// The list of filename extensions that are valid VCProjects.
|
|
/// </summary>
|
|
private static readonly string[] VcProjExtensions = { "vcsproj", "vcxproj" };
|
|
|
|
/// <summary>
|
|
/// Build the UWP appx bundle for this project. Requires that <see cref="UwpPlayerBuildTools.BuildPlayer(string,bool,CancellationToken)"/> has already be run or a user has
|
|
/// previously built the Unity Player with the WSA Player as the Build Target.
|
|
/// </summary>
|
|
/// <returns>True, if the appx build was successful.</returns>
|
|
public static async Task<bool> 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<int> 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<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given the project name and build path, resolves the valid VcProject file (i.e. .vcsproj, vcxproj)
|
|
/// </summary>
|
|
/// <returns>A valid path if the project file exists, null otherwise</returns>
|
|
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 <Identity /> node");
|
|
return false;
|
|
}
|
|
|
|
var dependencies = rootNode.Element(rootNode.GetDefaultNamespace() + "Dependencies");
|
|
|
|
if (dependencies == null)
|
|
{
|
|
Debug.LogError($"Package.appxmanifest for build (in path - {manifestFilePath}) is missing <Dependencies /> 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 <Identity /> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the AppX manifest path in the project output directory.
|
|
/// </summary>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates 'Assembly-CSharp.csproj' file according to the values set in buildInfo.
|
|
/// </summary>
|
|
/// <param name="buildInfo">An IBuildInfo containing a valid OutputDirectory</param>
|
|
/// <remarks>Only used with the .NET backend in Unity 2018 or older, with Unity C# Projects enabled.</remarks>
|
|
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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the 'Assembly-CSharp.csproj' files path in the project output directory.
|
|
/// </summary>
|
|
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");
|
|
|
|
int latestIndex = -1;
|
|
int latestVersion = -1;
|
|
|
|
for (int i = 0; i < windowsSdkPaths.Length; i++)
|
|
{
|
|
windowsSdkPaths[i] = windowsSdkPaths[i].Substring(windowsSdkPaths[i].LastIndexOf(@"\", StringComparison.Ordinal) + 1);
|
|
string[] versionSplit = windowsSdkPaths[i].Split('.');
|
|
if (versionSplit.Length >= 3
|
|
&& int.TryParse(versionSplit[2], out int currentVersion)
|
|
&& currentVersion > latestVersion)
|
|
{
|
|
latestVersion = currentVersion;
|
|
latestIndex = i;
|
|
}
|
|
}
|
|
|
|
EditorUserBuildSettings.wsaUWPSDK = windowsSdkPaths[latestIndex];
|
|
Debug.Log($"Using SDK version {EditorUserBuildSettings.wsaUWPSDK}");
|
|
}
|
|
|
|
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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Will return an empty string if logDirectory is not set.
|
|
/// </remarks>
|
|
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";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds capabilities according to the values in the buildInfo to the manifest file.
|
|
/// </summary>
|
|
/// <param name="buildInfo">An IBuildInfo containing a valid OutputDirectory and all capabilities</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a capability to the given rootNode, which must be the read AppX manifest from
|
|
/// the build output.
|
|
/// </summary>
|
|
/// <param name="rootNode">An XElement containing the AppX manifest from
|
|
/// the build output</param>
|
|
/// <param name="capability">The added capabilities tag as XName</param>
|
|
/// <param name="value">Value of the Name-XAttribute of the added capability</param>
|
|
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)));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the 'Gaze Input' capability to the manifest.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>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.</para>
|
|
/// </remarks>
|
|
public static void AddGazeInputCapability(XElement rootNode)
|
|
{
|
|
AddCapability(rootNode, rootNode.GetDefaultNamespace() + "DeviceCapability", "gazeInput");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the given capabilities to the manifest.
|
|
/// </summary>
|
|
public static void AddCapabilities(XElement rootNode, List<string> capabilities)
|
|
{
|
|
foreach (string capability in capabilities)
|
|
{
|
|
AddCapability(rootNode, rootNode.GetDefaultNamespace() + "DeviceCapability", capability);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the 'Research Mode' capability to the manifest.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>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.</para>
|
|
/// </remarks>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enables unsafe code in the generated Assembly-CSharp project.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>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.</para>
|
|
/// <para>For further information take a look at https://docs.microsoft.com/windows/mixed-reality/research-mode. </para>
|
|
/// <para>Note that this function is only public to poke a hole for testing - do not
|
|
/// take a dependency on this function.</para>
|
|
/// </remarks>
|
|
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";
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This struct controls the behavior of the arguments that are used
|
|
/// when finding msbuild.exe.
|
|
/// </summary>
|
|
private struct VSWhereFindOption
|
|
{
|
|
public VSWhereFindOption(string args, string suffix)
|
|
{
|
|
arguments = args;
|
|
pathSuffix = suffix;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to populate the Arguments of ProcessStartInfo when invoking
|
|
/// vswhere.
|
|
/// </summary>
|
|
public string arguments;
|
|
|
|
/// <summary>
|
|
/// This string is added as a suffix to the result of the vswhere path
|
|
/// search.
|
|
/// </summary>
|
|
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"),
|
|
};
|
|
}
|
|
}
|