123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- using System;
- using GodotTools.Core;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.IO;
- using System.Linq;
- using System.Xml;
- using System.Xml.Linq;
- using JetBrains.Annotations;
- using Microsoft.Build.Construction;
- using Microsoft.Build.Globbing;
- namespace GodotTools.ProjectEditor
- {
- public sealed class MSBuildProject
- {
- internal ProjectRootElement Root { get; set; }
- public bool HasUnsavedChanges { get; set; }
- public void Save() => Root.Save();
- public MSBuildProject(ProjectRootElement root)
- {
- Root = root;
- }
- }
- public static class ProjectUtils
- {
- public static MSBuildProject Open(string path)
- {
- var root = ProjectRootElement.Open(path);
- return root != null ? new MSBuildProject(root) : null;
- }
- [PublicAPI]
- public static void AddItemToProjectChecked(string projectPath, string itemType, string include)
- {
- var dir = Directory.GetParent(projectPath).FullName;
- var root = ProjectRootElement.Open(projectPath);
- Debug.Assert(root != null);
- if (root.AreDefaultCompileItemsEnabled())
- {
- // No need to add. It's already included automatically by the MSBuild Sdk.
- // This assumes the source file is inside the project directory and not manually excluded in the csproj
- return;
- }
- var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\");
- if (root.AddItemChecked(itemType, normalizedInclude))
- root.Save();
- }
- public static void RenameItemInProjectChecked(string projectPath, string itemType, string oldInclude, string newInclude)
- {
- var dir = Directory.GetParent(projectPath).FullName;
- var root = ProjectRootElement.Open(projectPath);
- Debug.Assert(root != null);
- if (root.AreDefaultCompileItemsEnabled())
- {
- // No need to add. It's already included automatically by the MSBuild Sdk.
- // This assumes the source file is inside the project directory and not manually excluded in the csproj
- return;
- }
- var normalizedOldInclude = oldInclude.NormalizePath();
- var normalizedNewInclude = newInclude.NormalizePath();
- var item = root.FindItemOrNullAbs(itemType, normalizedOldInclude);
- if (item == null)
- return;
- item.Include = normalizedNewInclude.RelativeToPath(dir).Replace("/", "\\");
- root.Save();
- }
- public static void RemoveItemFromProjectChecked(string projectPath, string itemType, string include)
- {
- var root = ProjectRootElement.Open(projectPath);
- Debug.Assert(root != null);
- if (root.AreDefaultCompileItemsEnabled())
- {
- // No need to add. It's already included automatically by the MSBuild Sdk.
- // This assumes the source file is inside the project directory and not manually excluded in the csproj
- return;
- }
- var normalizedInclude = include.NormalizePath();
- if (root.RemoveItemChecked(itemType, normalizedInclude))
- root.Save();
- }
- public static void RenameItemsToNewFolderInProjectChecked(string projectPath, string itemType, string oldFolder, string newFolder)
- {
- var dir = Directory.GetParent(projectPath).FullName;
- var root = ProjectRootElement.Open(projectPath);
- Debug.Assert(root != null);
- if (root.AreDefaultCompileItemsEnabled())
- {
- // No need to add. It's already included automatically by the MSBuild Sdk.
- // This assumes the source file is inside the project directory and not manually excluded in the csproj
- return;
- }
- bool dirty = false;
- var oldFolderNormalized = oldFolder.NormalizePath();
- var newFolderNormalized = newFolder.NormalizePath();
- string absOldFolderNormalized = Path.GetFullPath(oldFolderNormalized).NormalizePath();
- string absNewFolderNormalized = Path.GetFullPath(newFolderNormalized).NormalizePath();
- foreach (var item in root.FindAllItemsInFolder(itemType, oldFolderNormalized))
- {
- string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath();
- string absNewIncludeNormalized = absNewFolderNormalized + absPathNormalized.Substring(absOldFolderNormalized.Length);
- item.Include = absNewIncludeNormalized.RelativeToPath(dir).Replace("/", "\\");
- dirty = true;
- }
- if (dirty)
- root.Save();
- }
- public static void RemoveItemsInFolderFromProjectChecked(string projectPath, string itemType, string folder)
- {
- var root = ProjectRootElement.Open(projectPath);
- Debug.Assert(root != null);
- if (root.AreDefaultCompileItemsEnabled())
- {
- // No need to add. It's already included automatically by the MSBuild Sdk.
- // This assumes the source file is inside the project directory and not manually excluded in the csproj
- return;
- }
- var folderNormalized = folder.NormalizePath();
- var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList();
- if (itemsToRemove.Count > 0)
- {
- foreach (var item in itemsToRemove)
- item.Parent.RemoveChild(item);
- root.Save();
- }
- }
- private static string[] GetAllFilesRecursive(string rootDirectory, string mask)
- {
- string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories);
- // We want relative paths
- for (int i = 0; i < files.Length; i++)
- {
- files[i] = files[i].RelativeToPath(rootDirectory);
- }
- return files;
- }
- public static string[] GetIncludeFiles(string projectPath, string itemType)
- {
- var result = new List<string>();
- var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
- var root = ProjectRootElement.Open(projectPath);
- Debug.Assert(root != null);
- if (root.AreDefaultCompileItemsEnabled())
- {
- var excluded = new List<string>();
- result = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs").ToList();
- foreach (var item in root.Items)
- {
- if (string.IsNullOrEmpty(item.Condition))
- continue;
- if (item.ItemType != itemType)
- continue;
- string normalizedExclude = item.Exclude.NormalizePath();
- var glob = MSBuildGlob.Parse(normalizedExclude);
- excluded.AddRange(result.Where(includedFile => glob.IsMatch(includedFile)));
- }
- result.RemoveAll(f => excluded.Contains(f));
- }
- foreach (var itemGroup in root.ItemGroups)
- {
- if (itemGroup.Condition.Length != 0)
- continue;
- foreach (var item in itemGroup.Items)
- {
- if (item.ItemType != itemType)
- continue;
- string normalizedInclude = item.Include.NormalizePath();
- var glob = MSBuildGlob.Parse(normalizedInclude);
- foreach (var existingFile in existingFiles)
- {
- if (glob.IsMatch(existingFile))
- {
- result.Add(existingFile);
- }
- }
- }
- }
- return result.ToArray();
- }
- public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName)
- {
- var root = project.Root;
- if (!string.IsNullOrEmpty(root.Sdk))
- return;
- root.Sdk = ProjectGenerator.GodotSdkAttrValue;
- root.ToolsVersion = null;
- root.DefaultTargets = null;
- root.AddProperty("TargetFramework", "net472");
- // Remove obsolete properties, items and elements. We're going to be conservative
- // here to minimize the chances of introducing breaking changes. As such we will
- // only remove elements that could potentially cause issues with the Godot.NET.Sdk.
- void RemoveElements(IEnumerable<ProjectElement> elements)
- {
- foreach (var element in elements)
- element.Parent.RemoveChild(element);
- }
- // Default Configuration
- RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
- .Where(p => p.Name == "Configuration" && p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Debug"));
- // Default Platform
- RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
- .Where(p => p.Name == "Platform" && p.Condition.Trim() == "'$(Platform)' == ''" && p.Value == "AnyCPU"));
- // Simple properties
- var yabaiProperties = new[]
- {
- "OutputPath",
- "BaseIntermediateOutputPath",
- "IntermediateOutputPath",
- "TargetFrameworkVersion",
- "ProjectTypeGuids",
- "ApiConfiguration"
- };
- RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
- .Where(p => yabaiProperties.Contains(p.Name)));
- // Configuration dependent properties
- var yabaiPropertiesForConfigs = new[]
- {
- "DebugSymbols",
- "DebugType",
- "Optimize",
- "DefineConstants",
- "ErrorReport",
- "WarningLevel",
- "ConsolePause"
- };
- foreach (var config in new[] {"ExportDebug", "ExportRelease", "Debug"})
- {
- var group = root.PropertyGroups
- .First(g => g.Condition.Trim() == $"'$(Configuration)|$(Platform)' == '{config}|AnyCPU'");
- RemoveElements(group.Properties.Where(p => yabaiPropertiesForConfigs.Contains(p.Name)));
- if (group.Count == 0)
- {
- // No more children, safe to delete the group
- group.Parent.RemoveChild(group);
- }
- }
- // Godot API References
- var apiAssemblies = new[] {ApiAssemblyNames.Core, ApiAssemblyNames.Editor};
- RemoveElements(root.ItemGroups.SelectMany(g => g.Items)
- .Where(i => i.ItemType == "Reference" && apiAssemblies.Contains(i.Include)));
- // Microsoft.NETFramework.ReferenceAssemblies PackageReference
- RemoveElements(root.ItemGroups.SelectMany(g => g.Items).Where(i =>
- i.ItemType == "PackageReference" &&
- i.Include.Equals("Microsoft.NETFramework.ReferenceAssemblies", StringComparison.OrdinalIgnoreCase)));
- // Imports
- var yabaiImports = new[]
- {
- "$(MSBuildBinPath)/Microsoft.CSharp.targets",
- "$(MSBuildBinPath)Microsoft.CSharp.targets"
- };
- RemoveElements(root.Imports.Where(import => yabaiImports.Contains(
- import.Project.Replace("\\", "/").Replace("//", "/"))));
- // 'EnableDefaultCompileItems' and 'GenerateAssemblyInfo' are kept enabled by default
- // on new projects, but when migrating old projects we disable them to avoid errors.
- root.AddProperty("EnableDefaultCompileItems", "false");
- root.AddProperty("GenerateAssemblyInfo", "false");
- // Older AssemblyInfo.cs cause the following error:
- // 'Properties/AssemblyInfo.cs(19,28): error CS8357:
- // The specified version string contains wildcards, which are not compatible with determinism.
- // Either remove wildcards from the version string, or disable determinism for this compilation.'
- // We disable deterministic builds to prevent this. The user can then fix this manually when desired
- // by fixing 'AssemblyVersion("1.0.*")' to not use wildcards.
- root.AddProperty("Deterministic", "false");
- project.HasUnsavedChanges = true;
- var xDoc = XDocument.Parse(root.RawXml);
- if (xDoc.Root == null)
- return; // Too bad, we will have to keep the xmlns/namespace and xml declaration
- XElement GetElement(XDocument doc, string name, string value, string parentName)
- {
- foreach (var node in doc.DescendantNodes())
- {
- if (!(node is XElement element))
- continue;
- if (element.Name.LocalName.Equals(name) && element.Value == value &&
- element.Parent != null && element.Parent.Name.LocalName.Equals(parentName))
- {
- return element;
- }
- }
- return null;
- }
- // Add comment about Microsoft.NET.Sdk properties disabled during migration
- GetElement(xDoc, name: "EnableDefaultCompileItems", value: "false", parentName: "PropertyGroup")
- .AddBeforeSelf(new XComment("The following properties were overriden during migration to prevent errors.\n" +
- " Enabling them may require other manual changes to the project and its files."));
- void RemoveNamespace(XElement element)
- {
- element.Attributes().Where(x => x.IsNamespaceDeclaration).Remove();
- element.Name = element.Name.LocalName;
- foreach (var node in element.DescendantNodes())
- {
- if (node is XElement xElement)
- {
- // Need to do the same for all children recursively as it adds it to them for some reason...
- RemoveNamespace(xElement);
- }
- }
- }
- // Remove xmlns/namespace
- RemoveNamespace(xDoc.Root);
- // Remove xml declaration
- xDoc.Nodes().FirstOrDefault(node => node.NodeType == XmlNodeType.XmlDeclaration)?.Remove();
- string projectFullPath = root.FullPath;
- root = ProjectRootElement.Create(xDoc.CreateReader());
- root.FullPath = projectFullPath;
- project.Root = root;
- }
- public static void EnsureGodotSdkIsUpToDate(MSBuildProject project)
- {
- var root = project.Root;
- string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue;
- if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase))
- return;
- root.Sdk = godotSdkAttrValue;
- project.HasUnsavedChanges = true;
- }
- }
- }
|