|
10 | 10 | using System.Threading.Tasks; |
11 | 11 | using Microsoft.Build.Construction; |
12 | 12 | using Microsoft.CodeAnalysis; |
| 13 | +using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; |
13 | 14 | using Microsoft.CodeAnalysis.CSharp; |
14 | 15 | using Microsoft.CodeAnalysis.Formatting; |
15 | 16 | using Microsoft.CodeAnalysis.Simplification; |
16 | 17 | using Microsoft.TypeSpec.Generator.Primitives; |
17 | 18 | using Microsoft.TypeSpec.Generator.Providers; |
18 | 19 | using Microsoft.TypeSpec.Generator.Utilities; |
19 | 20 | using NuGet.Configuration; |
| 21 | +using NuGet.Protocol; |
| 22 | +using NuGet.Protocol.Core.Types; |
20 | 23 |
|
21 | 24 | namespace Microsoft.TypeSpec.Generator |
22 | 25 | { |
@@ -280,6 +283,147 @@ public async Task PostProcessAsync() |
280 | 283 | } |
281 | 284 | } |
282 | 285 |
|
| 286 | + /// <summary> |
| 287 | + /// Resolves PackageReference items from the project's .csproj file and adds their assemblies |
| 288 | + /// as metadata references so that custom code referencing external NuGet types compiles correctly. |
| 289 | + /// </summary> |
| 290 | + internal static async Task AddPackageReferencesFromProject() |
| 291 | + { |
| 292 | + var packageName = CodeModelGenerator.Instance.Configuration.PackageName; |
| 293 | + string projectFilePath = Path.GetFullPath( |
| 294 | + Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, $"{packageName}.csproj")); |
| 295 | + |
| 296 | + if (!File.Exists(projectFilePath)) |
| 297 | + { |
| 298 | + return; |
| 299 | + } |
| 300 | + |
| 301 | + var projectRoot = ProjectRootElement.Open(projectFilePath, new MSBuildProjectCollection()); |
| 302 | + |
| 303 | + var nugetSettings = Settings.LoadDefaultSettings(projectFilePath); |
| 304 | + var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(nugetSettings); |
| 305 | + |
| 306 | + // Build a set of assembly names already registered so we can skip them |
| 307 | + var existingRefs = new HashSet<string>( |
| 308 | + CodeModelGenerator.Instance.AdditionalMetadataReferences |
| 309 | + .Where(r => r.Display is not null) |
| 310 | + .Select(r => Path.GetFileNameWithoutExtension(r.Display!)) |
| 311 | + .Where(n => !string.IsNullOrEmpty(n)), |
| 312 | + StringComparer.OrdinalIgnoreCase); |
| 313 | + |
| 314 | + foreach (var item in projectRoot.Items.Where(i => i.ItemType == "PackageReference")) |
| 315 | + { |
| 316 | + var refPackageName = item.Include; |
| 317 | + |
| 318 | + if (string.IsNullOrEmpty(refPackageName)) |
| 319 | + { |
| 320 | + continue; |
| 321 | + } |
| 322 | + |
| 323 | + // Skip packages already added as metadata references (e.g., by a plugin) |
| 324 | + if (existingRefs.Contains(refPackageName)) |
| 325 | + { |
| 326 | + continue; |
| 327 | + } |
| 328 | + |
| 329 | + // Search the NuGet global packages folder for any cached version of this package. |
| 330 | + string? resolvedAssemblyPath = FindPackageAssembly(globalPackagesFolder, refPackageName); |
| 331 | + |
| 332 | + // If not found in cache, download the latest version from NuGet feeds |
| 333 | + if (resolvedAssemblyPath == null) |
| 334 | + { |
| 335 | + try |
| 336 | + { |
| 337 | + var latestVersion = await ResolveLatestPackageVersion(refPackageName, nugetSettings); |
| 338 | + if (latestVersion != null) |
| 339 | + { |
| 340 | + var downloader = new NugetPackageDownloader(refPackageName, latestVersion, null, nugetSettings); |
| 341 | + var downloadedPath = await downloader.DownloadAndInstallPackage(); |
| 342 | + var downloadedAssembly = Path.Combine(downloadedPath, $"{refPackageName}.dll"); |
| 343 | + if (File.Exists(downloadedAssembly)) |
| 344 | + { |
| 345 | + resolvedAssemblyPath = downloadedAssembly; |
| 346 | + } |
| 347 | + } |
| 348 | + } |
| 349 | + catch (Exception ex) |
| 350 | + { |
| 351 | + CodeModelGenerator.Instance.Emitter.Debug( |
| 352 | + $"Could not download package {refPackageName}: {ex.Message}"); |
| 353 | + } |
| 354 | + } |
| 355 | + |
| 356 | + if (resolvedAssemblyPath != null) |
| 357 | + { |
| 358 | + CodeModelGenerator.Instance.AddMetadataReference( |
| 359 | + MetadataReference.CreateFromFile(resolvedAssemblyPath)); |
| 360 | + CodeModelGenerator.Instance.Emitter.Debug( |
| 361 | + $"Added metadata reference: {refPackageName} from {resolvedAssemblyPath}"); |
| 362 | + } |
| 363 | + } |
| 364 | + } |
| 365 | + |
| 366 | + /// <summary> |
| 367 | + /// Searches the NuGet global packages folder for a package assembly across all cached versions. |
| 368 | + /// Returns the first matching assembly found, preferring newer versions. |
| 369 | + /// </summary> |
| 370 | + private static string? FindPackageAssembly(string globalPackagesFolder, string packageName) |
| 371 | + { |
| 372 | + var packageDir = Path.Combine(globalPackagesFolder, packageName.ToLowerInvariant()); |
| 373 | + |
| 374 | + if (!Directory.Exists(packageDir)) |
| 375 | + { |
| 376 | + return null; |
| 377 | + } |
| 378 | + |
| 379 | + foreach (var versionDir in Directory.GetDirectories(packageDir).OrderDescending()) |
| 380 | + { |
| 381 | + foreach (var tfm in NugetPackageDownloader.PreferredDotNetFrameworkVersions) |
| 382 | + { |
| 383 | + var assemblyPath = Path.Combine(versionDir, "lib", tfm, $"{packageName}.dll"); |
| 384 | + if (File.Exists(assemblyPath)) |
| 385 | + { |
| 386 | + return assemblyPath; |
| 387 | + } |
| 388 | + } |
| 389 | + } |
| 390 | + |
| 391 | + return null; |
| 392 | + } |
| 393 | + |
| 394 | + /// <summary> |
| 395 | + /// Queries configured NuGet feeds to resolve the latest stable version of a package. |
| 396 | + /// </summary> |
| 397 | + private static async Task<string?> ResolveLatestPackageVersion(string packageName, ISettings nugetSettings) |
| 398 | + { |
| 399 | + var sources = SettingsUtility.GetEnabledSources(nugetSettings); |
| 400 | + using var cacheContext = new SourceCacheContext(); |
| 401 | + foreach (var source in sources) |
| 402 | + { |
| 403 | + try |
| 404 | + { |
| 405 | + var repository = Repository.Factory.GetCoreV3(source.Source); |
| 406 | + var resource = await repository.GetResourceAsync<FindPackageByIdResource>(); |
| 407 | + var versions = await resource.GetAllVersionsAsync( |
| 408 | + packageName, cacheContext, NuGet.Common.NullLogger.Instance, CancellationToken.None); |
| 409 | + var latest = versions? |
| 410 | + .Where(v => !v.IsPrerelease) |
| 411 | + .OrderByDescending(v => v) |
| 412 | + .FirstOrDefault(); |
| 413 | + if (latest != null) |
| 414 | + { |
| 415 | + return latest.ToString(); |
| 416 | + } |
| 417 | + } |
| 418 | + catch |
| 419 | + { |
| 420 | + // Skip sources that fail (auth, network, etc.) |
| 421 | + } |
| 422 | + } |
| 423 | + |
| 424 | + return null; |
| 425 | + } |
| 426 | + |
283 | 427 | internal static async Task<Compilation?> LoadBaselineContract() |
284 | 428 | { |
285 | 429 | var packageName = CodeModelGenerator.Instance.TypeFactory.PrimaryNamespace; |
|
0 commit comments