ContentBuilder.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. #region File Description
  2. //-----------------------------------------------------------------------------
  3. // ContentBuilder.cs
  4. //
  5. // Microsoft XNA Community Game Platform
  6. // Copyright (C) Microsoft Corporation. All rights reserved.
  7. //-----------------------------------------------------------------------------
  8. #endregion
  9. #region Using Statements
  10. using System;
  11. using System.IO;
  12. using System.Diagnostics;
  13. using System.Collections.Generic;
  14. using Microsoft.Build.Construction;
  15. using Microsoft.Build.Evaluation;
  16. using Microsoft.Build.Execution;
  17. using Microsoft.Build.Framework;
  18. #endregion
  19. namespace WinFormsContentLoading
  20. {
  21. /// <summary>
  22. /// This class wraps the MSBuild functionality needed to build XNA Framework
  23. /// content dynamically at runtime. It creates a temporary MSBuild project
  24. /// in memory, and adds whatever content files you choose to this project.
  25. /// It then builds the project, which will create compiled .xnb content files
  26. /// in a temporary directory. After the build finishes, you can use a regular
  27. /// ContentManager to load these temporary .xnb files in the usual way.
  28. /// </summary>
  29. class ContentBuilder : IDisposable
  30. {
  31. #region Fields
  32. // What importers or processors should we load?
  33. const string xnaVersion = ", Version=4.0.0.0, PublicKeyToken=842cf8be1de50553";
  34. static string[] pipelineAssemblies =
  35. {
  36. "Microsoft.Xna.Framework.Content.Pipeline.FBXImporter" + xnaVersion,
  37. "Microsoft.Xna.Framework.Content.Pipeline.XImporter" + xnaVersion,
  38. "Microsoft.Xna.Framework.Content.Pipeline.TextureImporter" + xnaVersion,
  39. "Microsoft.Xna.Framework.Content.Pipeline.EffectImporter" + xnaVersion,
  40. // If you want to use custom importers or processors from
  41. // a Content Pipeline Extension Library, add them here.
  42. //
  43. // If your extension DLL is installed in the GAC, you should refer to it by assembly
  44. // name, eg. "MyPipelineExtension, Version=1.0.0.0, PublicKeyToken=1234567812345678".
  45. //
  46. // If the extension DLL is not in the GAC, you should refer to it by
  47. // file path, eg. "c:/MyProject/bin/MyPipelineExtension.dll".
  48. };
  49. // MSBuild objects used to dynamically build content.
  50. Project buildProject;
  51. ProjectRootElement projectRootElement;
  52. BuildParameters buildParameters;
  53. List<ProjectItem> projectItems = new List<ProjectItem>();
  54. ErrorLogger errorLogger;
  55. // Temporary directories used by the content build.
  56. string buildDirectory;
  57. string processDirectory;
  58. string baseDirectory;
  59. // Generate unique directory names if there is more than one ContentBuilder.
  60. static int directorySalt;
  61. // Have we been disposed?
  62. bool isDisposed;
  63. #endregion
  64. #region Properties
  65. /// <summary>
  66. /// Gets the output directory, which will contain the generated .xnb files.
  67. /// </summary>
  68. public string OutputDirectory
  69. {
  70. get { return Path.Combine(buildDirectory, "bin/Content"); }
  71. }
  72. #endregion
  73. #region Initialization
  74. /// <summary>
  75. /// Creates a new content builder.
  76. /// </summary>
  77. public ContentBuilder()
  78. {
  79. CreateTempDirectory();
  80. CreateBuildProject();
  81. }
  82. /// <summary>
  83. /// Finalizes the content builder.
  84. /// </summary>
  85. ~ContentBuilder()
  86. {
  87. Dispose(false);
  88. }
  89. /// <summary>
  90. /// Disposes the content builder when it is no longer required.
  91. /// </summary>
  92. public void Dispose()
  93. {
  94. Dispose(true);
  95. GC.SuppressFinalize(this);
  96. }
  97. /// <summary>
  98. /// Implements the standard .NET IDisposable pattern.
  99. /// </summary>
  100. protected virtual void Dispose(bool disposing)
  101. {
  102. if (!isDisposed)
  103. {
  104. isDisposed = true;
  105. DeleteTempDirectory();
  106. }
  107. }
  108. #endregion
  109. #region MSBuild
  110. /// <summary>
  111. /// Creates a temporary MSBuild content project in memory.
  112. /// </summary>
  113. void CreateBuildProject()
  114. {
  115. string projectPath = Path.Combine(buildDirectory, "content.contentproj");
  116. string outputPath = Path.Combine(buildDirectory, "bin");
  117. // Create the build project.
  118. projectRootElement = ProjectRootElement.Create(projectPath);
  119. // Include the standard targets file that defines how to build XNA Framework content.
  120. projectRootElement.AddImport("$(MSBuildExtensionsPath)\\Microsoft\\XNA Game Studio\\" +
  121. "v4.0\\Microsoft.Xna.GameStudio.ContentPipeline.targets");
  122. buildProject = new Project(projectRootElement);
  123. buildProject.SetProperty("XnaPlatform", "Windows");
  124. buildProject.SetProperty("XnaProfile", "Reach");
  125. buildProject.SetProperty("XnaFrameworkVersion", "v4.0");
  126. buildProject.SetProperty("Configuration", "Release");
  127. buildProject.SetProperty("OutputPath", outputPath);
  128. // Register any custom importers or processors.
  129. foreach (string pipelineAssembly in pipelineAssemblies)
  130. {
  131. buildProject.AddItem("Reference", pipelineAssembly);
  132. }
  133. // Hook up our custom error logger.
  134. errorLogger = new ErrorLogger();
  135. buildParameters = new BuildParameters(ProjectCollection.GlobalProjectCollection);
  136. buildParameters.Loggers = new ILogger[] { errorLogger };
  137. }
  138. /// <summary>
  139. /// Adds a new content file to the MSBuild project. The importer and
  140. /// processor are optional: if you leave the importer null, it will
  141. /// be autodetected based on the file extension, and if you leave the
  142. /// processor null, data will be passed through without any processing.
  143. /// </summary>
  144. public void Add(string filename, string name, string importer, string processor)
  145. {
  146. ProjectItem item = buildProject.AddItem("Compile", filename)[0];
  147. item.SetMetadataValue("Link", Path.GetFileName(filename));
  148. item.SetMetadataValue("Name", name);
  149. if (!string.IsNullOrEmpty(importer))
  150. item.SetMetadataValue("Importer", importer);
  151. if (!string.IsNullOrEmpty(processor))
  152. item.SetMetadataValue("Processor", processor);
  153. projectItems.Add(item);
  154. }
  155. /// <summary>
  156. /// Removes all content files from the MSBuild project.
  157. /// </summary>
  158. public void Clear()
  159. {
  160. buildProject.RemoveItems(projectItems);
  161. projectItems.Clear();
  162. }
  163. /// <summary>
  164. /// Builds all the content files which have been added to the project,
  165. /// dynamically creating .xnb files in the OutputDirectory.
  166. /// Returns an error message if the build fails.
  167. /// </summary>
  168. public string Build()
  169. {
  170. // Clear any previous errors.
  171. errorLogger.Errors.Clear();
  172. // Create and submit a new asynchronous build request.
  173. BuildManager.DefaultBuildManager.BeginBuild(buildParameters);
  174. BuildRequestData request = new BuildRequestData(buildProject.CreateProjectInstance(), new string[0]);
  175. BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(request);
  176. submission.ExecuteAsync(null, null);
  177. // Wait for the build to finish.
  178. submission.WaitHandle.WaitOne();
  179. BuildManager.DefaultBuildManager.EndBuild();
  180. // If the build failed, return an error string.
  181. if (submission.BuildResult.OverallResult == BuildResultCode.Failure)
  182. {
  183. return string.Join("\n", errorLogger.Errors.ToArray());
  184. }
  185. return null;
  186. }
  187. #endregion
  188. #region Temp Directories
  189. /// <summary>
  190. /// Creates a temporary directory in which to build content.
  191. /// </summary>
  192. void CreateTempDirectory()
  193. {
  194. // Start with a standard base name:
  195. //
  196. // %temp%\WinFormsContentLoading.ContentBuilder
  197. baseDirectory = Path.Combine(Path.GetTempPath(), GetType().FullName);
  198. // Include our process ID, in case there is more than
  199. // one copy of the program running at the same time:
  200. //
  201. // %temp%\WinFormsContentLoading.ContentBuilder\<ProcessId>
  202. int processId = Process.GetCurrentProcess().Id;
  203. processDirectory = Path.Combine(baseDirectory, processId.ToString());
  204. // Include a salt value, in case the program
  205. // creates more than one ContentBuilder instance:
  206. //
  207. // %temp%\WinFormsContentLoading.ContentBuilder\<ProcessId>\<Salt>
  208. directorySalt++;
  209. buildDirectory = Path.Combine(processDirectory, directorySalt.ToString());
  210. // Create our temporary directory.
  211. Directory.CreateDirectory(buildDirectory);
  212. PurgeStaleTempDirectories();
  213. }
  214. /// <summary>
  215. /// Deletes our temporary directory when we are finished with it.
  216. /// </summary>
  217. void DeleteTempDirectory()
  218. {
  219. Directory.Delete(buildDirectory, true);
  220. // If there are no other instances of ContentBuilder still using their
  221. // own temp directories, we can delete the process directory as well.
  222. if (Directory.GetDirectories(processDirectory).Length == 0)
  223. {
  224. Directory.Delete(processDirectory);
  225. // If there are no other copies of the program still using their
  226. // own temp directories, we can delete the base directory as well.
  227. if (Directory.GetDirectories(baseDirectory).Length == 0)
  228. {
  229. Directory.Delete(baseDirectory);
  230. }
  231. }
  232. }
  233. /// <summary>
  234. /// Ideally, we want to delete our temp directory when we are finished using
  235. /// it. The DeleteTempDirectory method (called by whichever happens first out
  236. /// of Dispose or our finalizer) does exactly that. Trouble is, sometimes
  237. /// these cleanup methods may never execute. For instance if the program
  238. /// crashes, or is halted using the debugger, we never get a chance to do
  239. /// our deleting. The next time we start up, this method checks for any temp
  240. /// directories that were left over by previous runs which failed to shut
  241. /// down cleanly. This makes sure these orphaned directories will not just
  242. /// be left lying around forever.
  243. /// </summary>
  244. void PurgeStaleTempDirectories()
  245. {
  246. // Check all subdirectories of our base location.
  247. foreach (string directory in Directory.GetDirectories(baseDirectory))
  248. {
  249. // The subdirectory name is the ID of the process which created it.
  250. int processId;
  251. if (int.TryParse(Path.GetFileName(directory), out processId))
  252. {
  253. try
  254. {
  255. // Is the creator process still running?
  256. Process.GetProcessById(processId);
  257. }
  258. catch (ArgumentException)
  259. {
  260. // If the process is gone, we can delete its temp directory.
  261. Directory.Delete(directory, true);
  262. }
  263. }
  264. }
  265. }
  266. #endregion
  267. }
  268. }