AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
BuildDeployTools.cs
Go to the documentation of this file.
1 // Copyright (c) Microsoft Corporation. All rights reserved.
2 // Licensed under the MIT License. See LICENSE in the project root for license information.
3 
4 using System;
5 using System.Diagnostics;
6 using System.IO;
7 using System.Linq;
8 using System.Xml.Linq;
9 using Microsoft.Win32;
10 using UnityEditor;
11 using UnityEngine;
12 using Debug = UnityEngine.Debug;
13 
14 namespace HoloToolkit.Unity
15 {
19  public class BuildDeployTools
20  {
21  public const string DefaultMSBuildVersion = "15.0";
22 
23  public static bool CanBuild()
24  {
25  if (PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.IL2CPP && IsIl2CppAvailable())
26  {
27  return true;
28  }
29 
30  return PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.WinRTDotNET && IsDotNetAvailable();
31  }
32 
33  public static bool IsDotNetAvailable()
34  {
35  return Directory.Exists(EditorApplication.applicationContentsPath + "\\PlaybackEngines\\MetroSupport\\Managed\\UAP");
36  }
37 
38  public static bool IsIl2CppAvailable()
39  {
40  return Directory.Exists(EditorApplication.applicationContentsPath + "\\PlaybackEngines\\MetroSupport\\Managed\\il2cpp");
41  }
42 
47  public static bool CheckBuildScenes()
48  {
49  if (EditorBuildSettings.scenes.Length == 0)
50  {
51  return EditorUtility.DisplayDialog("Attention!",
52  "No scenes are present in the build settings!\n\n Do you want to cancel and add one?",
53  "Continue Anyway", "Cancel Build");
54  }
55 
56  return true;
57  }
58 
62  public static bool BuildSLN()
63  {
64  return BuildSLN(BuildDeployPrefs.BuildDirectory, false);
65  }
66 
67  public static bool BuildSLN(string buildDirectory, bool showDialog = true)
68  {
69  // Use BuildSLNUtilities to create the SLN
70  bool buildSuccess = false;
71 
72  if (CheckBuildScenes() == false)
73  {
74  return false;
75  }
76 
77  var buildInfo = new BuildInfo
78  {
79  // These properties should all match what the Standalone.proj file specifies
80  OutputDirectory = buildDirectory,
81  Scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(scene => scene.path),
82  BuildTarget = BuildTarget.WSAPlayer,
83  WSASdk = WSASDK.UWP,
84  WSAUWPBuildType = EditorUserBuildSettings.wsaUWPBuildType,
85  WSAUwpSdk = EditorUserBuildSettings.wsaUWPSDK,
86 
87  // Configure a post build action that will compile the generated solution
88 #if UNITY_2018_1_OR_NEWER
89  PostBuildAction = (innerBuildInfo, buildReport) =>
90  {
91  if (buildReport.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
92  {
93  EditorUtility.DisplayDialog(string.Format("{0} WindowsStoreApp Build {1}!", PlayerSettings.productName, buildReport.summary.result), "See console for details", "OK");
94  }
95 #else
96  PostBuildAction = (innerBuildInfo, buildError) =>
97  {
98  if (!string.IsNullOrEmpty(buildError))
99  {
100  EditorUtility.DisplayDialog(string.Format("{0} WindowsStoreApp Build Failed!", PlayerSettings.productName), buildError, "OK");
101  }
102 #endif
103  else
104  {
105  if (showDialog)
106  {
107  if (!EditorUtility.DisplayDialog(PlayerSettings.productName, "Build Complete", "OK", "Build AppX"))
108  {
109  BuildAppxFromSLN(
110  PlayerSettings.productName,
117  }
118  }
119 
120  buildSuccess = true;
121  }
122  }
123  };
124 
126 
127  BuildSLNUtilities.PerformBuild(buildInfo);
128 
129  return buildSuccess;
130  }
131 
132  public static string CalcMSBuildPath(string msBuildVersion)
133  {
134  if (msBuildVersion.Equals("14.0"))
135  {
136  using (RegistryKey key =
137  Registry.LocalMachine.OpenSubKey(
138  string.Format(@"Software\Microsoft\MSBuild\ToolsVersions\{0}", msBuildVersion)))
139  {
140  if (key != null)
141  {
142  var msBuildBinFolder = (string)key.GetValue("MSBuildToolsPath");
143  return Path.Combine(msBuildBinFolder, "msbuild.exe");
144  }
145  }
146  }
147 
148  // If we got this far then we don't have VS 2015 installed and need to use msBuild 15
149  msBuildVersion = "15.0";
150 
151  // For MSBuild 15+ we should to use vswhere to give us the correct instance
152  string output = @"/C vswhere -version " + msBuildVersion + " -products * -requires Microsoft.Component.MSBuild -property installationPath";
153 
154  // get the right program files path based on whether the PC is x86 or x64
155  string programFiles = @"C:\Program Files (x86)\Microsoft Visual Studio\Installer";
156 
157  var vswherePInfo = new ProcessStartInfo
158  {
159  FileName = "cmd.exe",
160  CreateNoWindow = true,
161  UseShellExecute = false,
162  RedirectStandardOutput = true,
163  RedirectStandardError = false,
164  Arguments = output,
165  WorkingDirectory = programFiles
166  };
167 
168  using (var vswhereP = new Process())
169  {
170  vswhereP.StartInfo = vswherePInfo;
171  vswhereP.Start();
172  output = vswhereP.StandardOutput.ReadToEnd();
173  vswhereP.WaitForExit();
174  }
175 
176  string[] paths = output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
177 
178  if (paths.Length > 0)
179  {
180  // if there are multiple 2017 installs,
181  // prefer enterprise, then pro, then community
182  string bestPath = paths.OrderBy(p => p.ToLower().Contains("enterprise"))
183  .ThenBy(p => p.ToLower().Contains("professional"))
184  .ThenBy(p => p.ToLower().Contains("community")).First();
185 
186  return bestPath + @"\MSBuild\" + msBuildVersion + @"\Bin\MSBuild.exe";
187  }
188 
189  Debug.LogError("Unable to find a valid path to Visual Studio Instance!");
190  return string.Empty;
191  }
192 
193  public static bool RestoreNugetPackages(string nugetPath, string storePath)
194  {
195  Debug.Assert(File.Exists(nugetPath));
196  Debug.Assert(Directory.Exists(storePath));
197 
198  var nugetPInfo = new ProcessStartInfo
199  {
200  FileName = nugetPath,
201  CreateNoWindow = true,
202  UseShellExecute = false,
203  Arguments = "restore \"" + storePath + "/project.json\""
204  };
205 
206  using (var nugetP = new Process())
207  {
208  nugetP.StartInfo = nugetPInfo;
209  nugetP.Start();
210  nugetP.WaitForExit();
211  nugetP.Close();
212  nugetP.Dispose();
213  }
214 
215  return File.Exists(storePath + "\\project.lock.json");
216  }
217 
218  public static bool BuildAppxFromSLN(string productName, string msBuildVersion, bool forceRebuildAppx, string buildConfig, string buildPlatform, string buildDirectory, bool incrementVersion, bool showDialog = true)
219  {
220  EditorUtility.DisplayProgressBar("Build AppX", "Building AppX Package...", 0);
221  string slnFilename = Path.Combine(buildDirectory, PlayerSettings.productName + ".sln");
222 
223  if (!File.Exists(slnFilename))
224  {
225  Debug.LogError("Unable to find Solution to build from!");
226  EditorUtility.ClearProgressBar();
227  return false;
228  }
229 
230  // Get and validate the msBuild path...
231  var msBuildPath = CalcMSBuildPath(msBuildVersion);
232 
233  if (!File.Exists(msBuildPath))
234  {
235  Debug.LogErrorFormat("MSBuild.exe is missing or invalid:\n{0}.", msBuildPath);
236  EditorUtility.ClearProgressBar();
237  return false;
238  }
239 
240  // Get the path to the NuGet tool
241  string unity = Path.GetDirectoryName(EditorApplication.applicationPath);
242  System.Diagnostics.Debug.Assert(unity != null, "unity != null");
243  string storePath = Path.GetFullPath(Path.Combine(Path.Combine(Application.dataPath, ".."), buildDirectory));
244  string solutionProjectPath = Path.GetFullPath(Path.Combine(storePath, productName + @".sln"));
245 
246  // Bug in Unity editor that doesn't copy project.json and project.lock.json files correctly if solutionProjectPath is not in a folder named UWP.
247  if (!File.Exists(storePath + "\\project.json"))
248  {
249  File.Copy(unity + @"\Data\PlaybackEngines\MetroSupport\Tools\project.json", storePath + "\\project.json");
250  }
251 
252  string assemblyCSharp = string.Format("{0}/GeneratedProjects/UWP/Assembly-CSharp", storePath);
253  string assemblyCSharpFirstPass = string.Format("{0}/GeneratedProjects/UWP/Assembly-CSharp-firstpass", storePath);
254  bool restoreFirstPass = Directory.Exists(assemblyCSharpFirstPass);
255  string nugetPath = Path.Combine(unity, @"Data\PlaybackEngines\MetroSupport\Tools\NuGet.exe");
256 
257  // Before building, need to run a nuget restore to generate a json.lock file. Failing to do this breaks the build in VS RTM
258  if (PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.WinRTDotNET &&
259  (!RestoreNugetPackages(nugetPath, storePath) ||
260  !RestoreNugetPackages(nugetPath, storePath + "\\" + productName) ||
261  EditorUserBuildSettings.wsaGenerateReferenceProjects && !RestoreNugetPackages(nugetPath, assemblyCSharp) ||
262  EditorUserBuildSettings.wsaGenerateReferenceProjects && restoreFirstPass && !RestoreNugetPackages(nugetPath, assemblyCSharpFirstPass)))
263  {
264  Debug.LogError("Failed to restore nuget packages");
265  EditorUtility.ClearProgressBar();
266  return false;
267  }
268 
269  EditorUtility.DisplayProgressBar("Build AppX", "Building AppX Package...", 25);
270 
271  // Ensure that the generated .appx version increments by modifying Package.appxmanifest
272  if (!SetPackageVersion(incrementVersion))
273  {
274  Debug.LogError("Failed to increment package version!");
275  EditorUtility.ClearProgressBar();
276  return false;
277  }
278 
279  // Now do the actual build
280  var pInfo = new ProcessStartInfo
281  {
282  FileName = msBuildPath,
283  CreateNoWindow = true,
284  UseShellExecute = false,
285  RedirectStandardOutput = true,
286  RedirectStandardError = true,
287  Arguments = string.Format("\"{0}\" /t:{1} /p:Configuration={2} /p:Platform={3} /verbosity:m",
288  solutionProjectPath,
289  forceRebuildAppx ? "Rebuild" : "Build",
290  buildConfig,
291  buildPlatform)
292  };
293 
294  var process = new Process
295  {
296  StartInfo = pInfo,
297  EnableRaisingEvents = true
298  };
299 
300  try
301  {
302  process.ErrorDataReceived += (sender, args) =>
303  {
304  if (!string.IsNullOrEmpty(args.Data))
305  {
306  Debug.LogError(args.Data);
307  }
308  };
309 
310  process.OutputDataReceived += (sender, args) =>
311  {
312  if (!string.IsNullOrEmpty(args.Data))
313  {
314  Debug.Log(args.Data);
315  }
316  };
317 
318  if (!process.Start())
319  {
320  Debug.LogError("Failed to start process!");
321  EditorUtility.ClearProgressBar();
322  process.Close();
323  process.Dispose();
324  return false;
325  }
326 
327  process.BeginOutputReadLine();
328  process.BeginErrorReadLine();
329  process.WaitForExit();
330 
331  EditorUtility.ClearProgressBar();
332 
333  if (process.ExitCode == 0 && showDialog &&
334  !EditorUtility.DisplayDialog("Build AppX", "AppX Build Successful!", "OK", "Open AppX Folder"))
335  {
336  Process.Start("explorer.exe", string.Format("/f /open,{0}/{1}/AppPackages", Path.GetFullPath(BuildDeployPrefs.BuildDirectory), PlayerSettings.productName));
337  }
338 
339  if (process.ExitCode != 0)
340  {
341  Debug.LogError(string.Format("MSBuild error (code = {0})", process.ExitCode));
342  EditorUtility.ClearProgressBar();
343  EditorUtility.DisplayDialog(PlayerSettings.productName + " build Failed!", "Failed to build appx from solution. Error code: " + process.ExitCode, "OK");
344 
345  process.Close();
346  process.Dispose();
347  return false;
348  }
349 
350  process.Close();
351  process.Dispose();
352  }
353  catch (Exception e)
354  {
355  process.Close();
356  process.Dispose();
357  Debug.LogError("Cmd Process EXCEPTION: " + e);
358  EditorUtility.ClearProgressBar();
359  return false;
360  }
361 
362  return true;
363  }
364 
365  private static bool SetPackageVersion(bool increment)
366  {
367  // Find the manifest, assume the one we want is the first one
368  string[] manifests = Directory.GetFiles(BuildDeployPrefs.AbsoluteBuildDirectory, "Package.appxmanifest", SearchOption.AllDirectories);
369 
370  if (manifests.Length == 0)
371  {
372  Debug.LogError(string.Format("Unable to find Package.appxmanifest file for build (in path - {0})", BuildDeployPrefs.AbsoluteBuildDirectory));
373  return false;
374  }
375 
376  string manifest = manifests[0];
377  var rootNode = XElement.Load(manifest);
378  var identityNode = rootNode.Element(rootNode.GetDefaultNamespace() + "Identity");
379 
380  if (identityNode == null)
381  {
382  Debug.LogError(string.Format("Package.appxmanifest for build (in path - {0}) is missing an <Identity /> node", BuildDeployPrefs.AbsoluteBuildDirectory));
383  return false;
384  }
385 
386  // We use XName.Get instead of string -> XName implicit conversion because
387  // when we pass in the string "Version", the program doesn't find the attribute.
388  // Best guess as to why this happens is that implicit string conversion doesn't set the namespace to empty
389  var versionAttr = identityNode.Attribute(XName.Get("Version"));
390 
391  if (versionAttr == null)
392  {
393  Debug.LogError(string.Format("Package.appxmanifest for build (in path - {0}) is missing a version attribute in the <Identity /> node.", BuildDeployPrefs.AbsoluteBuildDirectory));
394  return false;
395  }
396 
397  // Assume package version always has a '.' between each number.
398  // According to https://msdn.microsoft.com/en-us/library/windows/apps/br211441.aspx
399  // Package versions are always of the form Major.Minor.Build.Revision.
400  // Note: Revision number reserved for Windows Store, and a value other than 0 will fail WACK.
401  var version = PlayerSettings.WSA.packageVersion;
402  var newVersion = new Version(version.Major, version.Minor, increment ? version.Build + 1 : version.Build, version.Revision);
403 
404  PlayerSettings.WSA.packageVersion = newVersion;
405  versionAttr.Value = newVersion.ToString();
406  rootNode.Save(manifest);
407  return true;
408  }
409  }
410 }
static void PerformBuild(BuildInfo buildInfo)
static bool BuildAppxFromSLN(string productName, string msBuildVersion, bool forceRebuildAppx, string buildConfig, string buildPlatform, string buildDirectory, bool incrementVersion, bool showDialog=true)
static bool CheckBuildScenes()
Displays a dialog if no scenes are present in the build and returns true if build can proceed...
static bool BuildSLN()
Do a build configured for Mixed Reality Applications, returns the error from BuildPipeline.BuildPlayer
Class containing various utility methods to build a WSA solution from a Unity project.
static void RaiseOverrideBuildDefaults(ref BuildInfo toConfigure)
Call this method to give other code an opportunity to override BuildInfo defaults.
static bool BuildSLN(string buildDirectory, bool showDialog=true)
static string CalcMSBuildPath(string msBuildVersion)
static bool RestoreNugetPackages(string nugetPath, string storePath)
Contains utility functions for building for the device