forked from microsoft/qsharp-compiler
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ProjectLoader.cs
243 lines (215 loc) · 12.9 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
// Copyright (c) Microsoft Corporation. All rights reserved.
// 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 readonly Action<string, MessageType> Log;
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) { props["TargetFramework"] = targetFramework; } // necessary for multi-framework projects.
return props;
}
private static readonly Regex TargetFrameworkMoniker =
new Regex(@"(netstandard[1-9]\.[0-9])|(netcoreapp[1-9]\.[0-9])|(net[1-9][0-9][0-9]?)");
private readonly ImmutableArray<string> SupportedQsFrameworks =
ImmutableArray.Create("netstandard2.", "netcoreapp2.");
/// <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)
: false;
/// <summary>
/// contains a list of Properties from the project that we want to track e.g. for telemetry.
/// </summary>
private static readonly IEnumerable<string> PropertiesToTrack =
new string[] { "QsharpLangVersion" };
/// <summary>
/// Returns true if the package with the given name should be tracked.
/// </summary>
private static bool GeneratePackageInfo(string packageName) =>
packageName.StartsWith("microsoft.quantum.", StringComparison.InvariantCultureIgnoreCase);
// general purpose routines
/// <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.
/// Returns a dictionary with additional project information (e.g. for telemetry) as out parameter.
/// Throws an ArgumentException if the given project file is null or does not exist.
/// </summary>
internal IDictionary<string, string> DesignTimeBuildProperties(string projectFile,
out Dictionary<string, string> metadata, Comparison<string> preferredFramework = null)
{
if (!File.Exists(projectFile)) throw new ArgumentException("given project file is null or does not exist", nameof(projectFile));
(string, Dictionary<string, string>) FrameworkAndMetadata(Project project)
{
string GetVersion(ProjectItem item) => item.DirectMetadata
.FirstOrDefault(data => data.Name.Equals("Version", StringComparison.InvariantCultureIgnoreCase))?.EvaluatedValue;
var packageRefs = project.Items.Where(item =>
item.ItemType == "PackageReference" && GeneratePackageInfo(item.EvaluatedInclude))
.Select(item => (item.EvaluatedInclude, GetVersion(item)));
var trackedProperties = project.Properties.Where(p =>
p?.Name != null && PropertiesToTrack.Contains(p.Name, StringComparer.InvariantCultureIgnoreCase))
.Select(p => (p.Name?.ToLowerInvariant(), p.EvaluatedValue));
var projInfo = new Dictionary<string, string>();
foreach (var (package, version) in packageRefs)
{ projInfo[$"pkgref.{package}"] = version; }
foreach (var (name, value) in trackedProperties)
{ projInfo[name] = value; }
projInfo["projectNameHash"] = GetProjectNameHash(projectFile);
var frameworks = TargetedFrameworks(project).ToList();
if (preferredFramework != null) frameworks.Sort(preferredFramework);
return (frameworks.FirstOrDefault(), projInfo);
}
var info = LoadAndApply(projectFile, GlobalProperties(), FrameworkAndMetadata);
metadata = info.Item2;
return GlobalProperties(info.Item1).ToImmutableDictionary();
}
/// <summary>
/// Returns a 1-way hash of the project file name so it can be sent with telemetry.
/// if any exception is thrown, it just logs the error message and returns an empty string.
/// </summary>
internal 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 (Exception e)
{
this.Log($"Error creating hash for project name '{projectFile}': {e.Message}", MessageType.Warning);
return string.Empty;
}
}
/// <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.
/// Throws an ArgumentNullException if the given query or properties are null.
/// Throws an ArgumentException if the given project file is null or does not exist.
/// NOTE: unloads the GlobalProjectCollection to force a cache clearing.
/// </summary>
private static T LoadAndApply<T>(string projectFile, IDictionary<string, string> properties, Func<Project, T> Query)
{
if (Query == null) throw new ArgumentNullException(nameof(Query));
if (properties == null) throw new ArgumentNullException(nameof(properties));
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.
/// Returns a dictionary with additional project information (e.g. for telemetry) as out parameter.
/// </summary>
private ProjectInstance QsProjectInstance(string projectFile, out Dictionary<string, string> metadata)
{
metadata = new Dictionary<string, string>();
if (!File.Exists(projectFile)) return null;
var loggers = new ILogger[] { new Utils.MSBuildLogger(this.Log) };
int preferSupportedFrameworks(string x, string y) => (IsSupportedQsFramework(y) ? 1 : 0) - (IsSupportedQsFramework(x) ? 1 : 0);
var properties = this.DesignTimeBuildProperties(projectFile, out metadata, 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();
succeed = instance.Build("ResolveAssemblyReferencesDesignTime", 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.
/// Returns a dictionary with additional project information (e.g. for telemetry) as out parameter.
/// 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.
/// Throws an ArgumentNullException if the given project file is null.
/// </summary>
public ProjectInstance TryGetQsProjectInstance(string projectFile, out Dictionary<string, string> metadata)
{
if (projectFile == null) throw new ArgumentNullException(nameof(projectFile));
metadata = new Dictionary<string, string>();
if (!projectFile.ToLowerInvariant().EndsWith(".csproj")) return null;
ProjectInstance instance = null;
try { instance = QsProjectInstance(projectFile, out metadata); }
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;
}
}
}