Last active
July 3, 2020 07:47
-
-
Save tintoy/76b48591e2a34429c1f5c02556b8b4d2 to your computer and use it in GitHub Desktop.
Sketch out an alternative to Roslyn's MSBuildWorkspace (which isn't available on .NET Core / .NET Standard yet).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.Text; | |
using Serilog; | |
using Serilog.Events; | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Threading; | |
using MSB = Microsoft.Build.Evaluation; | |
using MSBC = Microsoft.Build.Construction; | |
using MSBE = Microsoft.Build.Execution; | |
using MSBF = Microsoft.Build.Framework; | |
namespace RoslynWS | |
{ | |
static class Program | |
{ | |
static readonly FileInfo SolutionFile = new FileInfo(@"D:\Development\test-projects\RoslynWS\RoslynWS.sln"); | |
static void Main() | |
{ | |
SynchronizationContext.SetSynchronizationContext( | |
new SynchronizationContext() | |
); | |
ConfigureLogging(); | |
try | |
{ | |
Log.Information("First run..."); | |
DateTime then = DateTime.Now; | |
Load(); | |
Log.Information("First run complete ({RunTimeMS}ms).", (DateTime.Now - then).TotalMilliseconds); | |
Log.Information("Second run..."); | |
then = DateTime.Now; | |
Load(); | |
Log.Information("Second run complete ({RunTimeMS}ms).", (DateTime.Now - then).TotalMilliseconds); | |
} | |
catch (Exception unexpectedError) | |
{ | |
Log.Error(unexpectedError, "Unexpected error."); | |
} | |
} | |
static void Load() | |
{ | |
Log.Verbose("Creating workspace..."); | |
AdhocWorkspace ws = new AdhocWorkspace(); | |
Log.Verbose("Initialising solution..."); | |
Solution solution = ws.AddSolution(SolutionInfo.Create( | |
SolutionId.CreateNewId(), | |
VersionStamp.Create(), | |
filePath: SolutionFile.FullName | |
)); | |
Log.Verbose("Loading solution..."); | |
MSBC.SolutionFile solutionFile = MSBC.SolutionFile.Parse(SolutionFile.FullName); | |
Log.Verbose("Initialising project collection..."); | |
MSB.ProjectCollection projectCollection = MSBuildHelper.CreateProjectCollection( | |
solutionDirectory: SolutionFile.Directory.FullName | |
); | |
Log.Verbose("Loading projects..."); | |
foreach (var solutionProject in solutionFile.ProjectsInOrder) | |
{ | |
var projectId = ProjectId.CreateNewId(); | |
var msbuildProject = projectCollection.LoadProject(solutionProject.AbsolutePath); | |
List<DocumentInfo> projectDocuments = new List<DocumentInfo>(); | |
foreach (MSB.ProjectItem item in msbuildProject.GetItems("Compile")) | |
{ | |
string itemPath = item.GetMetadataValue("FullPath"); | |
projectDocuments.Add( | |
DocumentInfo.Create( | |
DocumentId.CreateNewId(projectId), | |
name: Path.GetFileName(itemPath), | |
filePath: itemPath, | |
loader: TextLoader.From( | |
TextAndVersion.Create( | |
SourceText.From(File.ReadAllText(itemPath)), | |
VersionStamp.Create() | |
) | |
) | |
) | |
); | |
} | |
Log.Information("Resolving assembly references for project {ProjectName}...", | |
Path.GetFileName(msbuildProject.FullPath) | |
); | |
List<MetadataReference> references = new List<MetadataReference>(); | |
foreach (string assemblyPath in ResolveReferences(msbuildProject)) | |
{ | |
references.Add( | |
MetadataReference.CreateFromFile(assemblyPath, MetadataReferenceProperties.Assembly) | |
); | |
} | |
solution = solution.AddProject(ProjectInfo.Create( | |
projectId, | |
VersionStamp.Create(), | |
name: Path.GetFileNameWithoutExtension(msbuildProject.FullPath), | |
assemblyName: Path.GetFileNameWithoutExtension(msbuildProject.FullPath), | |
language: "C#", | |
filePath: msbuildProject.FullPath, | |
outputFilePath: msbuildProject.GetPropertyValue("TargetPath"), | |
documents: projectDocuments, | |
metadataReferences: references | |
)); | |
var project = solution.GetProject(projectId); | |
solution = solution.WithProjectCompilationOptions(projectId, project.CompilationOptions.WithSpecificDiagnosticOptions( | |
new Dictionary<string, ReportDiagnostic> | |
{ | |
["CS1701"] = ReportDiagnostic.Suppress | |
} | |
)); | |
ws.TryApplyChanges(solution); | |
} | |
Log.Verbose("Compiling first project..."); | |
var prj = ws.CurrentSolution.Projects.First(); | |
Compilation compilation = prj.GetCompilationAsync().Result; | |
if (compilation != null) | |
{ | |
var diagnostics = compilation.GetDiagnostics(); | |
Log.Information("Compiled first project; {DiagnosticCount} diagnostics.", diagnostics.Length); | |
foreach (var diagnostic in diagnostics) | |
{ | |
if (diagnostic.IsSuppressed || diagnostic.Severity == DiagnosticSeverity.Hidden) | |
continue; | |
switch (diagnostic.Severity) | |
{ | |
case DiagnosticSeverity.Info: | |
{ | |
Log.Information("Diagnostic: {Diagnostic}", diagnostic); | |
break; | |
} | |
case DiagnosticSeverity.Warning: | |
{ | |
Log.Warning("Diagnostic: {Diagnostic}", diagnostic); | |
break; | |
} | |
case DiagnosticSeverity.Error: | |
{ | |
Log.Error("Diagnostic: {Diagnostic}", diagnostic); | |
break; | |
} | |
} | |
} | |
INamedTypeSymbol programSymbol = compilation.Assembly.GetTypeByMetadataName("RoslynWS.Program"); | |
if (programSymbol != null) | |
Log.Information("Found Program symbol."); | |
else | |
Log.Warning("Program symbol not found."); | |
} | |
else | |
Log.Warning("Failed to compile first project."); | |
} | |
static IEnumerable<string> ResolveReferences(MSB.Project msbuildProject) | |
{ | |
MSBE.ProjectInstance snapshot = msbuildProject.CreateProjectInstance(); | |
IDictionary<string, MSBE.TargetResult> outputs; | |
if (!snapshot.Build(new string[] { "ResolveAssemblyReferences" }, null, out outputs)) | |
{ | |
Log.Warning("Failed to build resolver targets."); | |
yield break; | |
} | |
foreach (string targetName in outputs.Keys) | |
{ | |
MSBE.TargetResult targetResult = outputs[targetName]; | |
MSBF.ITaskItem[] items = targetResult.Items.ToArray(); | |
foreach (MSBF.ITaskItem item in items) | |
yield return item.GetMetadata("FullPath"); | |
} | |
} | |
static void ConfigureLogging() | |
{ | |
Log.Logger = new LoggerConfiguration() | |
.MinimumLevel.Verbose() | |
.Enrich.FromLogContext() | |
.WriteTo.Debug( | |
restrictedToMinimumLevel: LogEventLevel.Verbose | |
) | |
.WriteTo.LiterateConsole( | |
restrictedToMinimumLevel: LogEventLevel.Verbose, | |
outputTemplate: "[{Level:l}] {Message:l}{NewLine}{Exception}" | |
) | |
.CreateLogger(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>netcoreapp2.0</TargetFramework> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="Microsoft.Build" Version="15.3.409" /> | |
<PackageReference Include="Microsoft.Build.Runtime" Version="15.3.409" /> | |
<PackageReference Include="Microsoft.CodeAnalysis" Version="2.3.2" /> | |
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="2.3.2" /> | |
<PackageReference Include="Serilog" Version="2.5.0" /> | |
<PackageReference Include="Serilog.Sinks.Debug" Version="1.0.0" /> | |
<PackageReference Include="Serilog.Sinks.Literate" Version="3.0.0" /> | |
<PackageReference Include="System.Runtime" Version="4.3.0" /> | |
</ItemGroup> | |
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Concurrent; | |
using System.Diagnostics; | |
namespace RoslynWS | |
{ | |
/// <summary> | |
/// Information about the .NET Core runtime. | |
/// </summary> | |
public class DotNetRuntimeInfo | |
{ | |
/// <summary> | |
/// A cache of .NET runtime information by target directory. | |
/// </summary> | |
static readonly ConcurrentDictionary<string, DotNetRuntimeInfo> _cache = new ConcurrentDictionary<string, DotNetRuntimeInfo>(); | |
/// <summary> | |
/// The .NET Core version. | |
/// </summary> | |
public string Version { get; set; } | |
/// <summary> | |
/// The .NET Core base directory. | |
/// </summary> | |
public string BaseDirectory { get; set; } | |
/// <summary> | |
/// The current runtime identifier (RID). | |
/// </summary> | |
public string RID { get; set; } | |
/// <summary> | |
/// Get information about the current .NET Core runtime. | |
/// </summary> | |
/// <param name="baseDirectory"> | |
/// An optional base directory where dotnet.exe should be run (this may affect the version it reports due to global.json). | |
/// </param> | |
/// <returns> | |
/// A <see cref="DotNetRuntimeInfo"/> containing the runtime information. | |
/// </returns> | |
public static DotNetRuntimeInfo GetCurrent(string baseDirectory = null) | |
{ | |
return _cache.GetOrAdd(baseDirectory, _ => | |
{ | |
DotNetRuntimeInfo runtimeInfo = new DotNetRuntimeInfo(); | |
Process dotnetInfoProcess = Process.Start(new ProcessStartInfo | |
{ | |
FileName = "dotnet", | |
WorkingDirectory = baseDirectory, | |
Arguments = "--info", | |
UseShellExecute = false, | |
RedirectStandardOutput = true | |
}); | |
using (dotnetInfoProcess) | |
{ | |
dotnetInfoProcess.WaitForExit(); | |
string currentSection = null; | |
string currentLine; | |
while ((currentLine = dotnetInfoProcess.StandardOutput.ReadLine()) != null) | |
{ | |
if (String.IsNullOrWhiteSpace(currentLine)) | |
continue; | |
if (!currentLine.StartsWith(" ")) | |
{ | |
currentSection = currentLine; | |
continue; | |
} | |
string[] property = currentLine.Split(new char[] { ':' }, count: 2); | |
if (property.Length != 2) | |
continue; | |
property[0] = property[0].Trim(); | |
property[1] = property[1].Trim(); | |
switch (currentSection) | |
{ | |
case "Product Information:": | |
{ | |
switch (property[0]) | |
{ | |
case "Version": | |
{ | |
runtimeInfo.Version = property[1]; | |
break; | |
} | |
} | |
break; | |
} | |
case "Runtime Environment:": | |
{ | |
switch (property[0]) | |
{ | |
case "Base Path": | |
{ | |
runtimeInfo.BaseDirectory = property[1]; | |
break; | |
} | |
case "RID": | |
{ | |
runtimeInfo.RID = property[1]; | |
break; | |
} | |
} | |
break; | |
} | |
} | |
} | |
} | |
return runtimeInfo; | |
}); | |
} | |
/// <summary> | |
/// Clear the cache of .NET runtime information. | |
/// </summary> | |
public static void ClearCache() | |
{ | |
_cache.Clear(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
{ | |
/// <summary> | |
/// Helper methods for working with MSBuild projects. | |
/// </summary> | |
public static class MSBuildHelper | |
{ | |
/// <summary> | |
/// The names of well-known item metadata. | |
/// </summary> | |
public static readonly ImmutableSortedSet<string> WellknownMetadataNames = | |
ImmutableSortedSet.Create( | |
"FullPath", | |
"RootDir", | |
"Filename", | |
"Extension", | |
"RelativeDir", | |
"Directory", | |
"RecursiveDir", | |
"Identity", | |
"ModifiedTime", | |
"CreatedTime", | |
"AccessedTime" | |
); | |
/// <summary> | |
/// Create an MSBuild project collection. | |
/// </summary> | |
/// <param name="solutionDirectory"> | |
/// The base (i.e. solution) directory. | |
/// </param> | |
/// <returns> | |
/// The project collection. | |
/// </returns> | |
public static ProjectCollection CreateProjectCollection(string solutionDirectory) | |
{ | |
return CreateProjectCollection(solutionDirectory, | |
DotNetRuntimeInfo.GetCurrent(solutionDirectory) | |
); | |
} | |
/// <summary> | |
/// Create an MSBuild project collection. | |
/// </summary> | |
/// <param name="solutionDirectory"> | |
/// The base (i.e. solution) directory. | |
/// </param> | |
/// <param name="runtimeInfo"> | |
/// Information about the current .NET Core runtime. | |
/// </param> | |
/// <returns> | |
/// The project collection. | |
/// </returns> | |
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<string, string> 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; | |
} | |
/// <summary> | |
/// Create global properties for MSBuild. | |
/// </summary> | |
/// <param name="runtimeInfo"> | |
/// Information about the current .NET Core runtime. | |
/// </param> | |
/// <param name="solutionDirectory"> | |
/// The base (i.e. solution) directory. | |
/// </param> | |
/// <returns> | |
/// A dictionary containing the global properties. | |
/// </returns> | |
public static Dictionary<string, string> 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<string, string> | |
{ | |
[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") | |
}; | |
} | |
/// <summary> | |
/// Ensure that environment variables are populated using the specified MSBuild global properties. | |
/// </summary> | |
/// <param name="globalMSBuildProperties"> | |
/// The MSBuild global properties | |
/// </param> | |
public static void EnsureMSBuildEnvironment(Dictionary<string, string> 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] | |
); | |
} | |
/// <summary> | |
/// Does the specified property name represent a private property? | |
/// </summary> | |
/// <param name="propertyName"> | |
/// The property name. | |
/// </param> | |
/// <returns> | |
/// <c>true</c>, if the property name starts with an underscore; otherwise, <c>false</c>. | |
/// </returns> | |
public static bool IsPrivateProperty(string propertyName) => propertyName?.StartsWith("_") ?? false; | |
/// <summary> | |
/// Does the specified metadata name represent a private property? | |
/// </summary> | |
/// <param name="metadataName"> | |
/// The metadata name. | |
/// </param> | |
/// <returns> | |
/// <c>true</c>, if the metadata name starts with an underscore; otherwise, <c>false</c>. | |
/// </returns> | |
public static bool IsPrivateMetadata(string metadataName) => metadataName?.StartsWith("_") ?? false; | |
/// <summary> | |
/// Does the specified item type represent a private property? | |
/// </summary> | |
/// <param name="itemType"> | |
/// The item type. | |
/// </param> | |
/// <returns> | |
/// <c>true</c>, if the item type starts with an underscore; otherwise, <c>false</c>. | |
/// </returns> | |
public static bool IsPrivateItemType(string itemType) => itemType?.StartsWith("_") ?? false; | |
/// <summary> | |
/// Determine whether the specified metadata name represents well-known (built-in) item metadata. | |
/// </summary> | |
/// <param name="metadataName"> | |
/// The metadata name. | |
/// </param> | |
/// <returns> | |
/// <c>true</c>, if <paramref name="metadataName"/> represents well-known item metadata; otherwise, <c>false</c>. | |
/// </returns> | |
public static bool IsWellKnownItemMetadata(string metadataName) => WellknownMetadataNames.Contains(metadataName); | |
/// <summary> | |
/// Create a copy of the project for caching. | |
/// </summary> | |
/// <param name="project"> | |
/// The MSBuild project. | |
/// </param> | |
/// <returns> | |
/// The project copy (independent of original, but sharing the same <see cref="ProjectCollection"/>). | |
/// </returns> | |
/// <remarks> | |
/// You can only create a single cached copy for a given project. | |
/// </remarks> | |
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; | |
} | |
/// <summary> | |
/// The names of well-known MSBuild properties. | |
/// </summary> | |
public static class WellKnownPropertyNames | |
{ | |
/// <summary> | |
/// The "MSBuildExtensionsPath" property. | |
/// </summary> | |
public static readonly string MSBuildExtensionsPath = "MSBuildExtensionsPath"; | |
/// <summary> | |
/// The "MSBuildSDKsPath" property. | |
/// </summary> | |
public static readonly string MSBuildSDKsPath = "MSBuildSDKsPath"; | |
/// <summary> | |
/// The "SolutionDir" property. | |
/// </summary> | |
public static readonly string SolutionDir = "SolutionDir"; | |
/// <summary> | |
/// The "_ResolveReferenceDependencies" property. | |
/// </summary> | |
public static readonly string ResolveReferenceDependencies = "_ResolveReferenceDependencies"; | |
/// <summary> | |
/// The "DesignTimeBuild" property. | |
/// </summary> | |
public static readonly string DesignTimeBuild = "DesignTimeBuild"; | |
/// <summary> | |
/// The "BuildProjectReferences" property. | |
/// </summary> | |
public static readonly string BuildProjectReferences = "BuildProjectReferences"; | |
/// <summary> | |
/// The "RoslynTargetsPath" property. | |
/// </summary> | |
public static readonly string RoslynTargetsPath = "RoslynTargetsPath"; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment