using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; using Microsoft.Build.Exceptions; using System; using System.Collections.Generic; using System.IO; using System.Collections.Immutable; namespace RoslynWS { /// /// Helper methods for working with MSBuild projects. /// public static class MSBuildHelper { /// /// The names of well-known item metadata. /// public static readonly ImmutableSortedSet WellknownMetadataNames = ImmutableSortedSet.Create( "FullPath", "RootDir", "Filename", "Extension", "RelativeDir", "Directory", "RecursiveDir", "Identity", "ModifiedTime", "CreatedTime", "AccessedTime" ); /// /// Create an MSBuild project collection. /// /// /// The base (i.e. solution) directory. /// /// /// The project collection. /// public static ProjectCollection CreateProjectCollection(string solutionDirectory) { return CreateProjectCollection(solutionDirectory, DotNetRuntimeInfo.GetCurrent(solutionDirectory) ); } /// /// Create an MSBuild project collection. /// /// /// The base (i.e. solution) directory. /// /// /// Information about the current .NET Core runtime. /// /// /// The project collection. /// public static ProjectCollection CreateProjectCollection(string solutionDirectory, DotNetRuntimeInfo runtimeInfo) { if (String.IsNullOrWhiteSpace(solutionDirectory)) throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'baseDir'.", nameof(solutionDirectory)); if (runtimeInfo == null) throw new ArgumentNullException(nameof(runtimeInfo)); if (String.IsNullOrWhiteSpace(runtimeInfo.BaseDirectory)) throw new InvalidOperationException("Cannot determine base directory for .NET Core."); Dictionary globalProperties = CreateGlobalMSBuildProperties(runtimeInfo, solutionDirectory); EnsureMSBuildEnvironment(globalProperties); ProjectCollection projectCollection = new ProjectCollection(globalProperties) { IsBuildEnabled = false }; // Override toolset paths (for some reason these point to the main directory where the dotnet executable lives). Toolset toolset = projectCollection.GetToolset("15.0"); toolset = new Toolset( toolsVersion: "15.0", toolsPath: globalProperties["MSBuildExtensionsPath"], projectCollection: projectCollection, msbuildOverrideTasksPath: "" ); projectCollection.AddToolset(toolset); return projectCollection; } /// /// Create global properties for MSBuild. /// /// /// Information about the current .NET Core runtime. /// /// /// The base (i.e. solution) directory. /// /// /// A dictionary containing the global properties. /// public static Dictionary CreateGlobalMSBuildProperties(DotNetRuntimeInfo runtimeInfo, string solutionDirectory) { if (runtimeInfo == null) throw new ArgumentNullException(nameof(runtimeInfo)); if (String.IsNullOrWhiteSpace(solutionDirectory)) throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'solutionDirectory'.", nameof(solutionDirectory)); if (solutionDirectory.Length > 0 && solutionDirectory[solutionDirectory.Length - 1] != Path.DirectorySeparatorChar) solutionDirectory += Path.DirectorySeparatorChar; return new Dictionary { [WellKnownPropertyNames.DesignTimeBuild] = "true", [WellKnownPropertyNames.BuildProjectReferences] = "false", [WellKnownPropertyNames.ResolveReferenceDependencies] = "true", [WellKnownPropertyNames.SolutionDir] = solutionDirectory, [WellKnownPropertyNames.MSBuildExtensionsPath] = runtimeInfo.BaseDirectory, [WellKnownPropertyNames.MSBuildSDKsPath] = Path.Combine(runtimeInfo.BaseDirectory, "Sdks"), [WellKnownPropertyNames.RoslynTargetsPath] = Path.Combine(runtimeInfo.BaseDirectory, "Roslyn") }; } /// /// Ensure that environment variables are populated using the specified MSBuild global properties. /// /// /// The MSBuild global properties /// public static void EnsureMSBuildEnvironment(Dictionary globalMSBuildProperties) { if (globalMSBuildProperties == null) throw new ArgumentNullException(nameof(globalMSBuildProperties)); // Kinda sucks that the simplest way to get MSBuild to resolve SDKs correctly is using environment variables, but there you go. Environment.SetEnvironmentVariable( WellKnownPropertyNames.MSBuildExtensionsPath, globalMSBuildProperties[WellKnownPropertyNames.MSBuildExtensionsPath] ); Environment.SetEnvironmentVariable( WellKnownPropertyNames.MSBuildSDKsPath, globalMSBuildProperties[WellKnownPropertyNames.MSBuildSDKsPath] ); } /// /// Does the specified property name represent a private property? /// /// /// The property name. /// /// /// true, if the property name starts with an underscore; otherwise, false. /// public static bool IsPrivateProperty(string propertyName) => propertyName?.StartsWith("_") ?? false; /// /// Does the specified metadata name represent a private property? /// /// /// The metadata name. /// /// /// true, if the metadata name starts with an underscore; otherwise, false. /// public static bool IsPrivateMetadata(string metadataName) => metadataName?.StartsWith("_") ?? false; /// /// Does the specified item type represent a private property? /// /// /// The item type. /// /// /// true, if the item type starts with an underscore; otherwise, false. /// public static bool IsPrivateItemType(string itemType) => itemType?.StartsWith("_") ?? false; /// /// Determine whether the specified metadata name represents well-known (built-in) item metadata. /// /// /// The metadata name. /// /// /// true, if represents well-known item metadata; otherwise, false. /// public static bool IsWellKnownItemMetadata(string metadataName) => WellknownMetadataNames.Contains(metadataName); /// /// Create a copy of the project for caching. /// /// /// The MSBuild project. /// /// /// The project copy (independent of original, but sharing the same ). /// /// /// You can only create a single cached copy for a given project. /// public static Project CloneAsCachedProject(this Project project) { if (project == null) throw new ArgumentNullException(nameof(project)); ProjectRootElement clonedXml = project.Xml.DeepClone(); Project clonedProject = new Project(clonedXml, project.GlobalProperties, project.ToolsVersion, project.ProjectCollection); clonedProject.FullPath = Path.ChangeExtension(project.FullPath, ".cached" + Path.GetExtension(project.FullPath) ); return clonedProject; } /// /// The names of well-known MSBuild properties. /// public static class WellKnownPropertyNames { /// /// The "MSBuildExtensionsPath" property. /// public static readonly string MSBuildExtensionsPath = "MSBuildExtensionsPath"; /// /// The "MSBuildSDKsPath" property. /// public static readonly string MSBuildSDKsPath = "MSBuildSDKsPath"; /// /// The "SolutionDir" property. /// public static readonly string SolutionDir = "SolutionDir"; /// /// The "_ResolveReferenceDependencies" property. /// public static readonly string ResolveReferenceDependencies = "_ResolveReferenceDependencies"; /// /// The "DesignTimeBuild" property. /// public static readonly string DesignTimeBuild = "DesignTimeBuild"; /// /// The "BuildProjectReferences" property. /// public static readonly string BuildProjectReferences = "BuildProjectReferences"; /// /// The "RoslynTargetsPath" property. /// public static readonly string RoslynTargetsPath = "RoslynTargetsPath"; } } }