ProjectUtils.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. using System;
  2. using GodotTools.Core;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Xml;
  8. using System.Xml.Linq;
  9. using JetBrains.Annotations;
  10. using Microsoft.Build.Construction;
  11. using Microsoft.Build.Globbing;
  12. namespace GodotTools.ProjectEditor
  13. {
  14. public sealed class MSBuildProject
  15. {
  16. internal ProjectRootElement Root { get; set; }
  17. public bool HasUnsavedChanges { get; set; }
  18. public void Save() => Root.Save();
  19. public MSBuildProject(ProjectRootElement root)
  20. {
  21. Root = root;
  22. }
  23. }
  24. public static class ProjectUtils
  25. {
  26. public static MSBuildProject Open(string path)
  27. {
  28. var root = ProjectRootElement.Open(path);
  29. return root != null ? new MSBuildProject(root) : null;
  30. }
  31. [PublicAPI]
  32. public static void AddItemToProjectChecked(string projectPath, string itemType, string include)
  33. {
  34. var dir = Directory.GetParent(projectPath).FullName;
  35. var root = ProjectRootElement.Open(projectPath);
  36. Debug.Assert(root != null);
  37. if (root.AreDefaultCompileItemsEnabled())
  38. {
  39. // No need to add. It's already included automatically by the MSBuild Sdk.
  40. // This assumes the source file is inside the project directory and not manually excluded in the csproj
  41. return;
  42. }
  43. var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\");
  44. if (root.AddItemChecked(itemType, normalizedInclude))
  45. root.Save();
  46. }
  47. public static void RenameItemInProjectChecked(string projectPath, string itemType, string oldInclude, string newInclude)
  48. {
  49. var dir = Directory.GetParent(projectPath).FullName;
  50. var root = ProjectRootElement.Open(projectPath);
  51. Debug.Assert(root != null);
  52. if (root.AreDefaultCompileItemsEnabled())
  53. {
  54. // No need to add. It's already included automatically by the MSBuild Sdk.
  55. // This assumes the source file is inside the project directory and not manually excluded in the csproj
  56. return;
  57. }
  58. var normalizedOldInclude = oldInclude.NormalizePath();
  59. var normalizedNewInclude = newInclude.NormalizePath();
  60. var item = root.FindItemOrNullAbs(itemType, normalizedOldInclude);
  61. if (item == null)
  62. return;
  63. item.Include = normalizedNewInclude.RelativeToPath(dir).Replace("/", "\\");
  64. root.Save();
  65. }
  66. public static void RemoveItemFromProjectChecked(string projectPath, string itemType, string include)
  67. {
  68. var root = ProjectRootElement.Open(projectPath);
  69. Debug.Assert(root != null);
  70. if (root.AreDefaultCompileItemsEnabled())
  71. {
  72. // No need to add. It's already included automatically by the MSBuild Sdk.
  73. // This assumes the source file is inside the project directory and not manually excluded in the csproj
  74. return;
  75. }
  76. var normalizedInclude = include.NormalizePath();
  77. if (root.RemoveItemChecked(itemType, normalizedInclude))
  78. root.Save();
  79. }
  80. public static void RenameItemsToNewFolderInProjectChecked(string projectPath, string itemType, string oldFolder, string newFolder)
  81. {
  82. var dir = Directory.GetParent(projectPath).FullName;
  83. var root = ProjectRootElement.Open(projectPath);
  84. Debug.Assert(root != null);
  85. if (root.AreDefaultCompileItemsEnabled())
  86. {
  87. // No need to add. It's already included automatically by the MSBuild Sdk.
  88. // This assumes the source file is inside the project directory and not manually excluded in the csproj
  89. return;
  90. }
  91. bool dirty = false;
  92. var oldFolderNormalized = oldFolder.NormalizePath();
  93. var newFolderNormalized = newFolder.NormalizePath();
  94. string absOldFolderNormalized = Path.GetFullPath(oldFolderNormalized).NormalizePath();
  95. string absNewFolderNormalized = Path.GetFullPath(newFolderNormalized).NormalizePath();
  96. foreach (var item in root.FindAllItemsInFolder(itemType, oldFolderNormalized))
  97. {
  98. string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath();
  99. string absNewIncludeNormalized = absNewFolderNormalized + absPathNormalized.Substring(absOldFolderNormalized.Length);
  100. item.Include = absNewIncludeNormalized.RelativeToPath(dir).Replace("/", "\\");
  101. dirty = true;
  102. }
  103. if (dirty)
  104. root.Save();
  105. }
  106. public static void RemoveItemsInFolderFromProjectChecked(string projectPath, string itemType, string folder)
  107. {
  108. var root = ProjectRootElement.Open(projectPath);
  109. Debug.Assert(root != null);
  110. if (root.AreDefaultCompileItemsEnabled())
  111. {
  112. // No need to add. It's already included automatically by the MSBuild Sdk.
  113. // This assumes the source file is inside the project directory and not manually excluded in the csproj
  114. return;
  115. }
  116. var folderNormalized = folder.NormalizePath();
  117. var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList();
  118. if (itemsToRemove.Count > 0)
  119. {
  120. foreach (var item in itemsToRemove)
  121. item.Parent.RemoveChild(item);
  122. root.Save();
  123. }
  124. }
  125. private static string[] GetAllFilesRecursive(string rootDirectory, string mask)
  126. {
  127. string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories);
  128. // We want relative paths
  129. for (int i = 0; i < files.Length; i++)
  130. {
  131. files[i] = files[i].RelativeToPath(rootDirectory);
  132. }
  133. return files;
  134. }
  135. public static string[] GetIncludeFiles(string projectPath, string itemType)
  136. {
  137. var result = new List<string>();
  138. var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
  139. var root = ProjectRootElement.Open(projectPath);
  140. Debug.Assert(root != null);
  141. if (root.AreDefaultCompileItemsEnabled())
  142. {
  143. var excluded = new List<string>();
  144. result = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs").ToList();
  145. foreach (var item in root.Items)
  146. {
  147. if (string.IsNullOrEmpty(item.Condition))
  148. continue;
  149. if (item.ItemType != itemType)
  150. continue;
  151. string normalizedExclude = item.Exclude.NormalizePath();
  152. var glob = MSBuildGlob.Parse(normalizedExclude);
  153. excluded.AddRange(result.Where(includedFile => glob.IsMatch(includedFile)));
  154. }
  155. result.RemoveAll(f => excluded.Contains(f));
  156. }
  157. foreach (var itemGroup in root.ItemGroups)
  158. {
  159. if (itemGroup.Condition.Length != 0)
  160. continue;
  161. foreach (var item in itemGroup.Items)
  162. {
  163. if (item.ItemType != itemType)
  164. continue;
  165. string normalizedInclude = item.Include.NormalizePath();
  166. var glob = MSBuildGlob.Parse(normalizedInclude);
  167. foreach (var existingFile in existingFiles)
  168. {
  169. if (glob.IsMatch(existingFile))
  170. {
  171. result.Add(existingFile);
  172. }
  173. }
  174. }
  175. }
  176. return result.ToArray();
  177. }
  178. public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName)
  179. {
  180. var root = project.Root;
  181. if (!string.IsNullOrEmpty(root.Sdk))
  182. return;
  183. root.Sdk = ProjectGenerator.GodotSdkAttrValue;
  184. root.ToolsVersion = null;
  185. root.DefaultTargets = null;
  186. root.AddProperty("TargetFramework", "net472");
  187. // Remove obsolete properties, items and elements. We're going to be conservative
  188. // here to minimize the chances of introducing breaking changes. As such we will
  189. // only remove elements that could potentially cause issues with the Godot.NET.Sdk.
  190. void RemoveElements(IEnumerable<ProjectElement> elements)
  191. {
  192. foreach (var element in elements)
  193. element.Parent.RemoveChild(element);
  194. }
  195. // Default Configuration
  196. RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
  197. .Where(p => p.Name == "Configuration" && p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Debug"));
  198. // Default Platform
  199. RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
  200. .Where(p => p.Name == "Platform" && p.Condition.Trim() == "'$(Platform)' == ''" && p.Value == "AnyCPU"));
  201. // Simple properties
  202. var yabaiProperties = new[]
  203. {
  204. "OutputPath",
  205. "BaseIntermediateOutputPath",
  206. "IntermediateOutputPath",
  207. "TargetFrameworkVersion",
  208. "ProjectTypeGuids",
  209. "ApiConfiguration"
  210. };
  211. RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
  212. .Where(p => yabaiProperties.Contains(p.Name)));
  213. // Configuration dependent properties
  214. var yabaiPropertiesForConfigs = new[]
  215. {
  216. "DebugSymbols",
  217. "DebugType",
  218. "Optimize",
  219. "DefineConstants",
  220. "ErrorReport",
  221. "WarningLevel",
  222. "ConsolePause"
  223. };
  224. foreach (var config in new[] {"ExportDebug", "ExportRelease", "Debug"})
  225. {
  226. var group = root.PropertyGroups
  227. .First(g => g.Condition.Trim() == $"'$(Configuration)|$(Platform)' == '{config}|AnyCPU'");
  228. RemoveElements(group.Properties.Where(p => yabaiPropertiesForConfigs.Contains(p.Name)));
  229. if (group.Count == 0)
  230. {
  231. // No more children, safe to delete the group
  232. group.Parent.RemoveChild(group);
  233. }
  234. }
  235. // Godot API References
  236. var apiAssemblies = new[] {ApiAssemblyNames.Core, ApiAssemblyNames.Editor};
  237. RemoveElements(root.ItemGroups.SelectMany(g => g.Items)
  238. .Where(i => i.ItemType == "Reference" && apiAssemblies.Contains(i.Include)));
  239. // Microsoft.NETFramework.ReferenceAssemblies PackageReference
  240. RemoveElements(root.ItemGroups.SelectMany(g => g.Items).Where(i =>
  241. i.ItemType == "PackageReference" &&
  242. i.Include.Equals("Microsoft.NETFramework.ReferenceAssemblies", StringComparison.OrdinalIgnoreCase)));
  243. // Imports
  244. var yabaiImports = new[]
  245. {
  246. "$(MSBuildBinPath)/Microsoft.CSharp.targets",
  247. "$(MSBuildBinPath)Microsoft.CSharp.targets"
  248. };
  249. RemoveElements(root.Imports.Where(import => yabaiImports.Contains(
  250. import.Project.Replace("\\", "/").Replace("//", "/"))));
  251. // 'EnableDefaultCompileItems' and 'GenerateAssemblyInfo' are kept enabled by default
  252. // on new projects, but when migrating old projects we disable them to avoid errors.
  253. root.AddProperty("EnableDefaultCompileItems", "false");
  254. root.AddProperty("GenerateAssemblyInfo", "false");
  255. // Older AssemblyInfo.cs cause the following error:
  256. // 'Properties/AssemblyInfo.cs(19,28): error CS8357:
  257. // The specified version string contains wildcards, which are not compatible with determinism.
  258. // Either remove wildcards from the version string, or disable determinism for this compilation.'
  259. // We disable deterministic builds to prevent this. The user can then fix this manually when desired
  260. // by fixing 'AssemblyVersion("1.0.*")' to not use wildcards.
  261. root.AddProperty("Deterministic", "false");
  262. project.HasUnsavedChanges = true;
  263. var xDoc = XDocument.Parse(root.RawXml);
  264. if (xDoc.Root == null)
  265. return; // Too bad, we will have to keep the xmlns/namespace and xml declaration
  266. XElement GetElement(XDocument doc, string name, string value, string parentName)
  267. {
  268. foreach (var node in doc.DescendantNodes())
  269. {
  270. if (!(node is XElement element))
  271. continue;
  272. if (element.Name.LocalName.Equals(name) && element.Value == value &&
  273. element.Parent != null && element.Parent.Name.LocalName.Equals(parentName))
  274. {
  275. return element;
  276. }
  277. }
  278. return null;
  279. }
  280. // Add comment about Microsoft.NET.Sdk properties disabled during migration
  281. GetElement(xDoc, name: "EnableDefaultCompileItems", value: "false", parentName: "PropertyGroup")
  282. .AddBeforeSelf(new XComment("The following properties were overriden during migration to prevent errors.\n" +
  283. " Enabling them may require other manual changes to the project and its files."));
  284. void RemoveNamespace(XElement element)
  285. {
  286. element.Attributes().Where(x => x.IsNamespaceDeclaration).Remove();
  287. element.Name = element.Name.LocalName;
  288. foreach (var node in element.DescendantNodes())
  289. {
  290. if (node is XElement xElement)
  291. {
  292. // Need to do the same for all children recursively as it adds it to them for some reason...
  293. RemoveNamespace(xElement);
  294. }
  295. }
  296. }
  297. // Remove xmlns/namespace
  298. RemoveNamespace(xDoc.Root);
  299. // Remove xml declaration
  300. xDoc.Nodes().FirstOrDefault(node => node.NodeType == XmlNodeType.XmlDeclaration)?.Remove();
  301. string projectFullPath = root.FullPath;
  302. root = ProjectRootElement.Create(xDoc.CreateReader());
  303. root.FullPath = projectFullPath;
  304. project.Root = root;
  305. }
  306. public static void EnsureGodotSdkIsUpToDate(MSBuildProject project)
  307. {
  308. var root = project.Root;
  309. string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue;
  310. if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase))
  311. return;
  312. root.Sdk = godotSdkAttrValue;
  313. project.HasUnsavedChanges = true;
  314. }
  315. }
  316. }