Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
Expand All @@ -18,6 +20,8 @@
using Microsoft.TypeSpec.Generator.SourceInput;
using Microsoft.TypeSpec.Generator.Utilities;
using NuGet.Configuration;
using NuGet.Frameworks;
using NuGet.Versioning;
using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection;

namespace Microsoft.TypeSpec.Generator
Expand Down Expand Up @@ -261,6 +265,98 @@ internal static Project AddDirectory(Project project, string directory, Func<str
return project;
}

internal static async Task<Dictionary<string, Dictionary<string, string>>> ReadProjectAssets()
{
Dictionary<string, Dictionary<string, string>> hshFrameworks = [];

// Read in the resolved direct dependencies.
// We first try the default location of project.assets.json, which is %project_dir%/obj/.
string assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json");
if (!File.Exists(assetsJson))
{
// If it does not exists, try the artifacts/obj/%project_name%/ three directoreies above projects directory.
// If this directory does not extists or does not contain artifacts/obj/%project_name%/roject.assets.json, give up.
DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent;
if (directory == null)
{
return hshFrameworks;
}
assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json");
if (!File.Exists(assetsJson))
{
return hshFrameworks;
}
}
Utf8JsonReader reader = new Utf8JsonReader(await File.ReadAllBytesAsync(assetsJson));
using JsonDocument document = JsonDocument.ParseValue(ref reader);
foreach (JsonProperty prop in document.RootElement.EnumerateObject())
{
if (prop.Value.ValueKind == JsonValueKind.Object && prop.NameEquals("projectFileDependencyGroups"))
{
foreach (JsonProperty targetFramework in prop.Value.EnumerateObject())
{
NuGetFramework currentFramework = new(targetFramework.Name);
if (!hshFrameworks.ContainsKey(currentFramework.Framework))
{
hshFrameworks[currentFramework.Framework] = [];
}
if (targetFramework.Value.ValueKind == JsonValueKind.Array)
{
// Parse dependencies. They are structured as SomePackage/package.version
foreach (JsonElement packageAndVersion in targetFramework.Value.EnumerateArray())
{
if (packageAndVersion.ValueKind == JsonValueKind.String)
{
string[] packageVersionRelation = (packageAndVersion.GetString() ?? "").Split();
// We only support the greater-than-or-equal relation.
// Example: "My.Package >= 1.1.1"
if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">="))
{
hshFrameworks[currentFramework.Framework][packageVersionRelation[0].ToLower()] = packageVersionRelation[2];
Comment thread
nick863 marked this conversation as resolved.
}
}
}
}
}
}
}
return hshFrameworks;
}

internal static string GetLatestTargetFramework(IEnumerable<string> shortNames)
{
// Assume framework order as follows:
// netstandardX.X, net462, netX.X
// Q: Why not to use NuGetFramework object here?
// A: Because it does not parse/recognize version and under the hood tries to compare Versions, which are all 0.0.0.
double maxFramework = 0.0;
string maxFrameworkName = string.Empty;
foreach (string name in shortNames)
{
double current = 0.0;
Match numeral = Regex.Match(name, "\\d+[.]*\\d*$");
if (numeral.Success)
{
current = double.Parse(numeral.Value);
}
if (name.StartsWith("net4", StringComparison.InvariantCultureIgnoreCase))
{
current /= 100;
current += 2000.0;
Comment thread
nick863 marked this conversation as resolved.
}
else if (!name.StartsWith("netstandard", StringComparison.InvariantCultureIgnoreCase))
{
current += 2000.0;
}
if (current >= maxFramework)
{
maxFramework = current;
maxFrameworkName = name;
}
}
return maxFrameworkName;
}

/// <summary>
/// Resolves PackageReference items from the project's .csproj file and adds their assemblies
/// as metadata references so that custom code referencing external NuGet types compiles correctly.
Expand All @@ -275,19 +371,63 @@ internal static async Task AddPackageReferencesFromProject()
{
return;
}

// Use the dotnet restore mechanism to get all the dependent packages.
Process restore = new();
Comment thread
nick863 marked this conversation as resolved.
ProcessStartInfo info = new()
{
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "dotnet",
ArgumentList = {"restore", projectFilePath},
RedirectStandardOutput = true,
RedirectStandardError = true,
};
restore.StartInfo = info;
if (restore.Start())
{
Task<string> outputTask = restore.StandardOutput.ReadToEndAsync();
Task<string> errorTask = restore.StandardError.ReadToEndAsync();
await restore.WaitForExitAsync();
string output = await outputTask;
string error = await errorTask;
if (restore.ExitCode != 0)
{
CodeModelGenerator.Instance.Emitter.ReportDiagnostic(
code: "unable-to-restore-target-package",
message: $"The dotnet restore {projectFilePath} command exited with {restore.ExitCode}.\n" +
$"Standard output: {output}\n" +
$"Error output: {error}",
severity: EmitterRpc.EmitterDiagnosticSeverity.Error
);
}
}
else
{
CodeModelGenerator.Instance.Emitter.ReportDiagnostic(
code: "unable-to-run-dotnet-restore",
message: $"Unable to run dotnet restore on the project {projectFilePath}",
severity: EmitterRpc.EmitterDiagnosticSeverity.Error
);
}
var projectRoot = ProjectRootElement.Open(projectFilePath, new MSBuildProjectCollection());

var nugetSettings = Settings.LoadDefaultSettings(projectFilePath);
var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(nugetSettings);

// Read in the resolved direct dependencies for all frameworks
Dictionary<string, Dictionary<string, string>> hshFrameworks = await ReadProjectAssets();
// Get the latest framework.
Dictionary<string, string> hshNameVersion = [];
if (hshFrameworks.Count > 0)
{
hshNameVersion = hshFrameworks[GetLatestTargetFramework(hshFrameworks.Keys.AsEnumerable())];
}
// Build a set of assembly names already registered so we can skip them
var existingRefs = new HashSet<string>(
CodeModelGenerator.Instance.AdditionalMetadataReferences
.Where(r => r.Display is not null)
.Select(r => Path.GetFileNameWithoutExtension(r.Display!))
.Where(n => !string.IsNullOrEmpty(n)),
StringComparer.OrdinalIgnoreCase);
CodeModelGenerator.Instance.AdditionalMetadataReferences
.Where(r => r.Display is not null)
.Select(r => Path.GetFileNameWithoutExtension(r.Display!))
.Where(n => !string.IsNullOrEmpty(n)),
StringComparer.OrdinalIgnoreCase);

foreach (var item in projectRoot.Items.Where(i => i.ItemType == "PackageReference"))
{
Expand All @@ -305,29 +445,27 @@ internal static async Task AddPackageReferencesFromProject()
}

// Search the NuGet global packages folder for any cached version of this package.
string? resolvedAssemblyPath = NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName);

// If not found in cache, download the latest version from NuGet feeds
string? version = default;
hshNameVersion.TryGetValue(refPackageName.ToLower(), out version);
string? resolvedAssemblyPath = version is null
? NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName)
: NugetPackageResolver.FindPackageAssemblyInVersion(globalPackagesFolder, refPackageName, version);
Comment thread
nick863 marked this conversation as resolved.
if (resolvedAssemblyPath == null)
{
try
{
var latestVersion = await NugetPackageResolver.ResolveLatestPackageVersion(refPackageName, nugetSettings);
if (latestVersion != null)
{
var downloader = new NugetPackageDownloader(refPackageName, latestVersion, null, nugetSettings);
var downloadedPath = await downloader.DownloadAndInstallPackage();
var downloadedAssembly = Path.Combine(downloadedPath, $"{refPackageName}.dll");
if (File.Exists(downloadedAssembly))
{
resolvedAssemblyPath = downloadedAssembly;
}
}
}
catch (Exception ex)
CodeModelGenerator.Instance.Emitter.Debug(
$"The package {refPackageName}{(version != null ? " v. "+ version : "")} was not restored.");
}
else if (version is null)
{
string packageDir = Path.Combine(globalPackagesFolder, refPackageName.ToLowerInvariant());
string[] allDirs = Directory.GetDirectories(packageDir);
NuGetVersion? maxVersion = allDirs.Select(dir => NuGetVersion.TryParse(Path.GetFileName(dir), out var v) ? v : null)
.Where(t => t != null)
.Max();
if (maxVersion != null)
{
CodeModelGenerator.Instance.Emitter.Debug(
$"Could not download package {refPackageName}: {ex.Message}");
$"Using cached {refPackageName} v. {maxVersion.Version}.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ namespace Microsoft.TypeSpec.Generator.Utilities
{
/// <summary>
/// Resolves <see cref="InputExternalTypeMetadata"/> entries to <see cref="Type"/> instances by
/// looking up the package in the NuGet global cache (or downloading it from configured feeds when
/// missing) and loading the assembly via reflection. Used by <c>TypeFactory.CreateExternalType</c>
/// looking up the package in the NuGet global cache and loading the assembly via reflection.
/// Used by <c>TypeFactory.CreateExternalType</c>
/// as a fallback after <c>CreateFrameworkType</c> returns <c>null</c>.
/// </summary>
/// <remarks>
Expand Down Expand Up @@ -234,32 +234,6 @@ private static async Task<ResolutionResult> ResolveResultAsync(InputExternalType
string? assemblyPath = NugetPackageResolver.FindPackageAssembly(
globalPackagesFolder, external.Package!, external.MinVersion);
Comment thread
nick863 marked this conversation as resolved.
Comment thread
nick863 marked this conversation as resolved.

if (assemblyPath == null)
{
try
{
var resolvedVersion = !string.IsNullOrEmpty(external.MinVersion)
? external.MinVersion!
: await NugetPackageResolver.ResolveLatestPackageVersion(external.Package!, nugetSettings);

if (!string.IsNullOrEmpty(resolvedVersion))
{
var downloader = new NugetPackageDownloader(external.Package!, resolvedVersion!, null, nugetSettings);
var downloadedPath = await downloader.DownloadAndInstallPackage();
var downloadedAssembly = Path.Combine(downloadedPath, $"{external.Package}.dll");
if (File.Exists(downloadedAssembly))
{
assemblyPath = downloadedAssembly;
}
}
}
catch (Exception ex)
{
generator.Emitter?.Debug(
$"Could not download package '{external.Package}' for external type '{external.Identity}': {ex.Message}");
}
}

if (assemblyPath == null || !File.Exists(assemblyPath))
{
var versionQualifier = string.IsNullOrEmpty(external.MinVersion)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
Expand Down
Loading
Loading