diff --git a/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj b/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj
index 7ca082ec..16e4ad6e 100644
--- a/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj
+++ b/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj
@@ -12,16 +12,23 @@
-
+
+
+
+
+
+
+
-
-
@@ -30,9 +37,5 @@
-
-
-
-
diff --git a/src/Microsoft.Tye.Core/OutputContext.cs b/src/Microsoft.Tye.Core/OutputContext.cs
index e21fc20b..8b766504 100644
--- a/src/Microsoft.Tye.Core/OutputContext.cs
+++ b/src/Microsoft.Tye.Core/OutputContext.cs
@@ -101,11 +101,21 @@ namespace Microsoft.Tye
WriteLine(Verbosity.Info, message);
}
+ public void WriteInfo(string message)
+ {
+ Write(Verbosity.Info, message);
+ }
+
public void WriteInfoLine(string message)
{
WriteLine(Verbosity.Info, message);
}
+ public void WriteDebug(string message)
+ {
+ Write(Verbosity.Debug, message);
+ }
+
public void WriteDebugLine(string message)
{
WriteLine(Verbosity.Debug, message);
diff --git a/src/Microsoft.Tye.Core/ProjectReader.cs b/src/Microsoft.Tye.Core/ProjectReader.cs
index 0dc3eb53..427b8d0b 100644
--- a/src/Microsoft.Tye.Core/ProjectReader.cs
+++ b/src/Microsoft.Tye.Core/ProjectReader.cs
@@ -3,17 +3,26 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.CommandLine.Invocation;
+using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.Loader;
using System.Threading.Tasks;
+using Microsoft.Build.Definition;
+using Microsoft.Build.Execution;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Locator;
+using Microsoft.Build.Logging;
using Semver;
namespace Microsoft.Tye
{
public static class ProjectReader
{
- public static async Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project)
+ private static bool registered;
+ public static Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project)
{
if (output is null)
{
@@ -30,94 +39,136 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(project));
}
+ EnsureMSBuildRegistered(projectFile);
+
using (var step = output.BeginStep("Reading Project Details..."))
{
- await EvaluateMSBuildAsync(output, projectFile, project);
+ EvaluateProject(output, projectFile, project);
if (!SemVersion.TryParse(project.Version, out var version))
{
- output.WriteInfoLine($"No version or invalid version 'application.Version' found, using default.");
+ output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default.");
version = new SemVersion(0, 1, 0);
project.Version = version.ToString();
}
step.MarkComplete();
}
+
+ return Task.CompletedTask;
+ }
+
+ private static void EnsureMSBuildRegistered(FileInfo projectFile)
+ {
+ if (!registered)
+ {
+ // It says VisualStudio - but we'll just use .NET SDK
+ var instances = MSBuildLocator.QueryVisualStudioInstances(new VisualStudioInstanceQueryOptions()
+ {
+ DiscoveryTypes = DiscoveryType.DotNetSdk,
+
+ // Using the project as the working directory. We're making the assumption that
+ // all of the projects want to use the same SDK version. This library is going
+ // load a single version of the SDK's assemblies into our process, so we can't
+ // use supprt SDKs at once without getting really tricky.
+ //
+ // The .NET SDK-based discovery uses `dotnet --info` and returns the SDK
+ // in use for the directory.
+ //
+ // https://github.com/microsoft/MSBuildLocator/blob/master/src/MSBuildLocator/MSBuildLocator.cs#L320
+ WorkingDirectory = projectFile.DirectoryName,
+ });
+
+ var instance = instances.SingleOrDefault();
+ if (instance == null)
+ {
+ throw new CommandException("Failed to find dotnet. Make sure the .NET SDK is installed and on the PATH.");
+ }
+
+ MSBuildLocator.RegisterInstance(instance);
+ registered = true;
+ }
}
- private static async Task EvaluateMSBuildAsync(OutputContext output, FileInfo projectFile, Project project)
+ // Do not load MSBuild types before using EnsureMSBuildRegistered.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void EvaluateProject(OutputContext output, FileInfo projectFile, Project project)
{
+ var sw = Stopwatch.StartNew();
+
+ ProjectInstance projectInstance;
try
{
- output.WriteDebugLine("Installing msbuild targets.");
- TargetInstaller.Install(projectFile.FullName);
- output.WriteDebugLine("Installed msbuild targets.");
+ output.WriteDebugLine($"Loading project '{projectFile.FullName}'.");
+ projectInstance = ProjectInstance.FromFile(projectFile.FullName, new ProjectOptions());
+ output.WriteDebugLine($"Loaded project '{projectFile.FullName}'.");
}
- catch (Exception ex)
+ catch
{
- throw new CommandException("Failed to install targets.", ex);
+ throw new CommandException($"Failed to load project: '{projectFile.FullName}'.");
}
- var outputFilePath = Path.GetTempFileName();
+ // Currently we only log at debug level.
+ var logger = new ConsoleLogger(
+ verbosity: LoggerVerbosity.Normal,
+ write: message => output.WriteDebug(message),
+ colorSet: null,
+ colorReset: null);
try
{
- var capture = output.Capture();
- var programRoot = Path.GetDirectoryName(typeof(Program).Assembly.Location);
-
- var restore = string.Empty;
- if (!File.Exists(Path.Combine(projectFile.DirectoryName, "obj", "project.assets.json")))
- {
- restore = "/restore";
- }
-
- output.WriteDebugLine("Running 'dotnet msbuild'.");
- var msbuildCommand = $"msbuild {restore} /t:EvaluateTyeProjectInfo \"/p:TyeTargetLocation={programRoot}\" \"/p:TyeOutputFilePath={outputFilePath}\"";
-
- output.WriteCommandLine("dotnet", msbuildCommand);
- var exitCode = await Process.ExecuteAsync(
- $"dotnet",
- msbuildCommand,
- workingDir: projectFile.DirectoryName,
- stdOut: capture.StdOut,
- stdErr: capture.StdErr);
-
- output.WriteDebugLine($"Done running 'dotnet msbuild' exit code: {exitCode}");
- if (exitCode != 0)
- {
- throw new CommandException("'dotnet msbuild' failed.");
- }
+ AssemblyLoadContext.Default.Resolving += ResolveAssembly;
+ var result = projectInstance.Build(
+ targets: new[] { "Restore", "ResolveReferences", "ResolvePackageDependenciesDesignTime", "PrepareResources", "GetAssemblyAttributes", },
+ loggers: new[] { logger, });
+
+ // If the build fails, we're not really blocked from doing our work.
+ // For now we just log the output to debug. There are errors that occur during
+ // running these targets we don't really care as long as we get the data.
+ }
+ finally
+ {
+ AssemblyLoadContext.Default.Resolving -= ResolveAssembly;
+ }
- var lines = await File.ReadAllLinesAsync(outputFilePath);
- for (var i = 0; i < lines.Length; i++)
+ // Reading both InformationalVersion and Version is more resilant in the face of build failures.
+ var version = projectInstance.GetProperty("InformationalVersion")?.EvaluatedValue ?? projectInstance.GetProperty("Version").EvaluatedValue;
+ project.Version = version;
+ output.WriteDebugLine($"Found application version: {version}");
+
+ var targetFramework = projectInstance.GetPropertyValue("TargetFramework");
+ project.TargetFramework = targetFramework;
+ output.WriteDebugLine($"Found target framework: {targetFramework}");
+
+ var sharedFrameworks = projectInstance.GetItems("FrameworkReference").Select(i => i.EvaluatedInclude).ToList();
+ project.Frameworks.AddRange(sharedFrameworks.Select(s => new Framework(s)));
+ output.WriteDebugLine($"Found shared frameworks: {string.Join(", ", sharedFrameworks)}");
+
+ output.WriteDebugLine($"Evaluation Took: {sw.Elapsed.TotalMilliseconds}ms");
+
+ // The Microsoft.Build.Locator doesn't handle the loading of other assemblies
+ // that are shipped with MSBuild (ex NuGet).
+ //
+ // This means that the set of assemblies that need special handling depends on the targets
+ // that we run :(
+ //
+ // This is workaround for this limitation based on the targets we need to run
+ // to resolve references and versions.
+ //
+ // See: https://github.com/microsoft/MSBuildLocator/issues/86
+ Assembly? ResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName)
+ {
+ if (assemblyName.Name is object && assemblyName.Name.StartsWith("NuGet."))
{
- var line = lines[i];
- if (line.StartsWith("version="))
+ var msbuildDirectory = Environment.GetEnvironmentVariable("MSBuildExtensionsPath")!;
+ var assemblyFilePath = Path.Combine(msbuildDirectory, assemblyName.Name + ".dll");
+ if (File.Exists(assemblyFilePath))
{
- project.Version = line.Substring("version=".Length).Trim();
- output.WriteDebugLine($"Found application version: {line}");
- continue;
- }
-
- if (line.StartsWith("tfm"))
- {
- project.TargetFramework = line.Substring("tfm=".Length).Trim();
- output.WriteDebugLine($"Found target framework: {line}");
- continue;
- }
-
- if (line.StartsWith("frameworks="))
- {
- var right = line.Substring("frameworks=".Length).Trim();
- project.Frameworks.AddRange(right.Split(",").Select(s => new Framework(s)));
- output.WriteDebugLine($"Found shared frameworks: {line}");
- continue;
+ return context.LoadFromAssemblyPath(assemblyFilePath);
}
}
- }
- finally
- {
- File.Delete(outputFilePath);
+
+ return default;
}
}
}
diff --git a/src/Microsoft.Tye.Core/Resources/Imports.targets b/src/Microsoft.Tye.Core/Resources/Imports.targets
deleted file mode 100644
index e695c12c..00000000
--- a/src/Microsoft.Tye.Core/Resources/Imports.targets
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Microsoft.Tye.Core/TargetInstaller.cs b/src/Microsoft.Tye.Core/TargetInstaller.cs
deleted file mode 100644
index 49261a44..00000000
--- a/src/Microsoft.Tye.Core/TargetInstaller.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.IO;
-using System.Reflection;
-
-namespace Microsoft.Tye
-{
- public static class TargetInstaller
- {
- public static void Install(string projectFilePath)
- {
- if (projectFilePath is null)
- {
- throw new ArgumentNullException(nameof(projectFilePath));
- }
-
- var projectDirectory = Path.GetDirectoryName(projectFilePath);
- var intermediateDirectory = Path.Combine(projectDirectory!, "obj");
-
- Directory.CreateDirectory(intermediateDirectory);
-
- var fileName = $"{Path.GetFileName(projectFilePath)}.Tye.targets";
- var targetFilePath = Path.Combine(intermediateDirectory, fileName);
-
- if (File.Exists(targetFilePath))
- {
- return;
- }
-
- var toolType = typeof(TargetInstaller);
- var toolAssembly = toolType.GetTypeInfo().Assembly;
- var toolImportTargetsResourceName = $"Tye.Resources.Imports.targets";
-
- using var stream = toolAssembly.GetManifestResourceStream(toolImportTargetsResourceName);
- if (stream == null)
- {
- throw new CommandException("Failed to find resource. Valid names: " + string.Join(", ", toolAssembly.GetManifestResourceNames()));
- }
-
- var targetBytes = new byte[stream.Length];
- stream.Read(targetBytes, 0, targetBytes.Length);
- File.WriteAllBytes(targetFilePath, targetBytes);
- }
- }
-}
diff --git a/src/Microsoft.Tye.Core/build/Tye.targets b/src/Microsoft.Tye.Core/build/Tye.targets
deleted file mode 100644
index 80be5c4d..00000000
--- a/src/Microsoft.Tye.Core/build/Tye.targets
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
- $(EvaluateTyeProjectInfoDependsOn);
- ResolveReferences;
- ResolvePackageDependenciesDesignTime;
- PrepareResources;
- GetAssemblyAttributes;
-
-
-
-
-
-
-
- <_Frameworks>@(FrameworkReference, ',')
-
-
- <_Lines Include="version=$(InformationalVersion)" />
- <_Lines Include="tfm=$(TargetFramework)" />
- <_Lines Include="frameworks=$(_Frameworks)" />
-
-
-
-
-
-
diff --git a/src/Microsoft.Tye.Core/buildMultiTargeting/Tye.targets b/src/Microsoft.Tye.Core/buildMultiTargeting/Tye.targets
deleted file mode 100644
index ad18d2af..00000000
--- a/src/Microsoft.Tye.Core/buildMultiTargeting/Tye.targets
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file