This repository has been archived by the owner on Jan 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 169
/
ProjectLoader.cs
253 lines (224 loc) · 10.8 KB
/
ProjectLoader.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.Quantum.QsLanguageServer
{
/// <summary>
/// Note that this class is *not* threadsafe,
/// and design time builds will fail if a (design time) build is already in progress.
/// </summary>
internal class ProjectLoader
{
public Action<string, MessageType> Log { get; }
public ProjectLoader(Action<string, MessageType>? log = null) =>
this.Log = log ?? ((_, __) => { });
// possibly configurable properties
/// <summary>
/// Returns a dictionary with global properties used to load projects at runtime.
/// BuildProjectReferences is set to false such that references are not built upon ResolveAssemblyReferencesDesignTime.
/// </summary>
internal static Dictionary<string, string> GlobalProperties(string? targetFramework = null)
{
var props = new Dictionary<string, string>
{
["BuildProjectReferences"] = "false",
["EnableFrameworkPathOverride"] = "false", // otherwise msbuild fails on .net 461 projects
};
if (targetFramework != null)
{
// necessary for multi-framework projects.
props["TargetFramework"] = targetFramework;
}
return props;
}
private static readonly Regex TargetFrameworkMoniker =
new(@"(netstandard[1-9]\.[0-9])|(netcoreapp[1-9]\.[0-9])|(net[1-9][0-9][0-9]?)|(net[1-9]\.[0-9])");
private readonly ImmutableArray<string> supportedQsFrameworks =
ImmutableArray.Create("netstandard2.", "netcoreapp3.1", "net6.");
/// <summary>
/// Returns true if the given framework is officially supported for Q# projects.
/// </summary>
public bool IsSupportedQsFramework(string framework) =>
framework != null && this.supportedQsFrameworks.Any(framework.ToLowerInvariant().StartsWith);
// general purpose routines
/// <summary>
/// Returns a 1-way hash of the project file name so it can be sent with telemetry.
/// Returns an empty string if any exception is thrown in the process.
/// </summary>
internal static string GetProjectNameHash(string projectFile)
{
try
{
using SHA256 hashAlgorithm = SHA256.Create();
string fileName = Path.GetFileName(projectFile);
byte[] data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(fileName));
var sBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
return sBuilder.ToString();
}
catch
{
return string.Empty;
}
}
/// <summary>
/// Returns all targeted frameworks of the given project.
/// IMPORTANT: currently only supports .net core-style projects.
/// </summary>
private static string[] TargetedFrameworks(Project project)
{
// this routine does not work in full generality, but it will do for now for our purposes
var evaluatedProps = project.Properties.Where(p => p.Name?.ToLowerInvariant()?.StartsWith("targetframework") ?? false);
return evaluatedProps
.SelectMany(p => TargetFrameworkMoniker.Matches(p.EvaluatedValue.ToLowerInvariant())
.Where(m => m.Success).Select(m => m.Value))
.ToArray();
}
/// <summary>
/// Returns a dictionary with the properties used for design time builds of the project corresponding to the given project file.
/// Chooses a target framework for the build properties according to the given comparison.
/// Chooses the first target framework is no comparison is given.
/// Logs a suitable error is no target framework can be determined.
/// </summary>
/// <exception cref="ArgumentException"><paramref name="projectFile"/> is null or does not exist.</exception>
internal IDictionary<string, string> DesignTimeBuildProperties(
string projectFile,
Comparison<string>? preferredFramework = null)
{
if (!File.Exists(projectFile))
{
throw new ArgumentException("given project file is null or does not exist", nameof(projectFile));
}
string? FrameworkAndMetadata(Project project)
{
var frameworks = TargetedFrameworks(project).ToList();
if (preferredFramework != null)
{
frameworks.Sort(preferredFramework);
}
return frameworks.FirstOrDefault();
}
var framework = LoadAndApply(projectFile, GlobalProperties(), FrameworkAndMetadata);
return GlobalProperties(framework).ToImmutableDictionary();
}
/// <summary>
/// Loads the project corresponding to the given project file with the given properties,
/// applies the given query to it, and unloads it. Returns the result of the query.
/// NOTE: unloads the GlobalProjectCollection to force a cache clearing.
/// </summary>
/// <exception cref="ArgumentException"><paramref name="projectFile"/> is null or does not exist.</exception>
private static T LoadAndApply<T>(string projectFile, IDictionary<string, string> properties, Func<Project, T> query)
{
if (!File.Exists(projectFile))
{
throw new ArgumentException("given project file is null or does not exist", nameof(projectFile));
}
Project? project = null;
try
{
// Unloading the project unloads the project but *doesn't* clear the cache to be resilient to inconsistent states.
// Hence we actually need to unload all projects, which does make sure the cache is cleared and changes on disk are reflected.
// See e.g. https://github.com/Microsoft/msbuild/issues/795
ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); // needed due to the caching behavior of MS build
project = new Project(projectFile, properties, ToolLocationHelper.CurrentToolsVersion);
return query(project);
}
finally
{
if (project != null)
{
ProjectCollection.GlobalProjectCollection?.UnloadProject(project);
}
}
}
// routines for loading and processing information from Q# projects specifically
/// <summary>
/// Loads the project for the given project file, restores all packages,
/// and builds the target ResolveAssemblyReferencesDesignTime, logging suitable errors in the process.
/// If the built project instance is recognized as a valid Q# project by the server, returns the built project instance.
/// Returns null if this is not the case, or if the given project file is null or does not exist.
/// </summary>
private ProjectInstance? QsProjectInstance(string projectFile)
{
if (!File.Exists(projectFile))
{
return null;
}
var loggers = new ILogger[] { new Utils.MSBuildLogger(this.Log) };
int PreferSupportedFrameworks(string x, string y) => (this.IsSupportedQsFramework(y) ? 1 : 0) - (this.IsSupportedQsFramework(x) ? 1 : 0);
var properties = this.DesignTimeBuildProperties(projectFile, PreferSupportedFrameworks);
// restore project (requires reloading the project after for the restore to take effect)
var succeed = LoadAndApply(projectFile, properties, project =>
project.CreateProjectInstance().Build("Restore", loggers));
if (!succeed)
{
this.Log($"Failed to restore project '{projectFile}'.", MessageType.Error);
}
// build the project instance and returns it if it is indeed a Q# project
return LoadAndApply(projectFile, properties, project =>
{
var instance = project.CreateProjectInstance();
var target = instance.Targets.ContainsKey("ResolveTargetPackage")
? "ResolveTargetPackage"
: "ResolveAssemblyReferencesDesignTime";
succeed = instance.Build(target, loggers);
if (!succeed)
{
this.Log($"Failed to resolve assembly references for project '{projectFile}'.", MessageType.Error);
}
return instance.Targets.ContainsKey("QSharpCompile") ? instance : null;
});
}
/// <summary>
/// Returns the project instance for the given project file with all assembly references resolved,
/// if the given project is recognized as a valid Q# project by the server, and null otherwise.
/// Returns null without logging anything if the given project file does not end in .csproj.
/// Logs suitable messages using the given log function if the project file cannot be found, or if the design time build fails.
/// Logs whether or not the project is recognized as Q# project.
/// </summary>
public ProjectInstance? TryGetQsProjectInstance(string projectFile)
{
if (!projectFile.ToLowerInvariant().EndsWith(".csproj"))
{
return null;
}
ProjectInstance? instance = null;
try
{
instance = this.QsProjectInstance(projectFile);
}
catch (Exception ex)
{
this.Log($"Error on loading project '{projectFile}': {ex.Message}.", MessageType.Error);
}
if (!File.Exists(projectFile))
{
this.Log($"Could not find project file '{projectFile}'.", MessageType.Warning);
}
else if (instance == null)
{
this.Log($"Ignoring non-Q# project '{projectFile}'.", MessageType.Log);
}
else
{
this.Log($"Discovered Q# project '{projectFile}'.", MessageType.Log);
}
return instance;
}
}
}