| File: src\GetCMakeArtifactsFromFileApi.cs | Web Access |
| Project: Microsoft.DotNet.CMake.Sdk.csproj (Microsoft.DotNet.CMake.Sdk) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; namespace Microsoft.DotNet.CMake.Sdk; /// <summary> /// Reads CMake File API response to find artifacts for a specific source directory. /// </summary> [MSBuildMultiThreadableTask] public class GetCMakeArtifactsFromFileApi : Task, IMultiThreadableTask { /// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary> public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; /// <summary> /// The CMake build output directory containing the File API response. /// </summary> [Required] public string CMakeOutputDir { get; set; } /// <summary> /// The source directory of the CMakeLists.txt to find artifacts for. /// </summary> [Required] public string SourceDirectory { get; set; } /// <summary> /// The configuration name (e.g., Debug, Release). /// </summary> [Required] public string Configuration { get; set; } /// <summary> /// Output: The list of artifact file paths. /// </summary> [Output] public ITaskItem[] Artifacts { get; set; } public override bool Execute() { try { string replyDir = Path.Combine(CMakeOutputDir, ".cmake", "api", "v1", "reply"); AbsolutePath replyDirPath = TaskEnvironment.GetAbsolutePath(replyDir); if (!Directory.Exists(replyDirPath)) { Log.LogError("CMake File API reply directory does not exist: {0}", replyDir); return false; } // Find the latest index file var indexFiles = Directory.GetFiles(replyDirPath, "index-*.json"); if (indexFiles.Length == 0) { Log.LogError("No CMake File API index files found."); return false; } string indexFile = indexFiles.OrderByDescending(f => f).First(); Log.LogMessage(MessageImportance.Low, "Reading CMake File API index: {0}", indexFile); string indexJson = File.ReadAllText(TaskEnvironment.GetAbsolutePath(indexFile)); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var index = JsonSerializer.Deserialize<CMakeFileApiIndex>(indexJson, options); if (index?.Reply?.ClientReply?.CodemodelV2?.JsonFile == null) { Log.LogError("No 'codemodel-v2' found in File API index reply."); return false; } string codeModelFile = Path.Combine(replyDir, index.Reply.ClientReply.CodemodelV2.JsonFile); AbsolutePath codeModelFilePath = TaskEnvironment.GetAbsolutePath(codeModelFile); if (!File.Exists(codeModelFilePath)) { Log.LogError("Codemodel file not found: {0}", codeModelFile); return false; } Log.LogMessage(MessageImportance.Low, "Reading codemodel: {0}", codeModelFile); string codeModelJson = File.ReadAllText(codeModelFilePath); var codeModel = JsonSerializer.Deserialize<CMakeCodeModel>(codeModelJson, options); if (codeModel == null) { Log.LogError("Failed to deserialize codemodel."); return false; } // Get the source root from the codemodel string sourceRoot = codeModel.Paths?.Source?.Replace('\\', '/').TrimEnd('/') ?? ""; // Normalize source directory for comparison // GetAbsolutePath does not canonicalize, but this value is string-compared against // dirSource below, and Path.GetFullPath used to resolve the "." and ".." segments that // CMake's file API routinely emits. string normalizedSourceDir = TaskEnvironment.GetAbsolutePath(SourceDirectory).GetCanonicalForm().Value.Replace('\\', '/').TrimEnd('/'); // Find the configuration using LINQ var config = codeModel.Configurations?.FirstOrDefault(c => string.Equals(c.Name, Configuration, StringComparison.OrdinalIgnoreCase)); if (config == null) { Log.LogError("Configuration '{0}' not found in CMake File API response.", Configuration); return false; } Log.LogMessage(MessageImportance.Low, "Found configuration: {0}", Configuration); if (config.Directories == null || config.Targets == null) { Log.LogError("Configuration '{0}' has no directories or targets.", Configuration); return false; } // Find the matching directory using LINQ var directory = config.Directories.FirstOrDefault(d => { string dirSource = d.Source?.Replace('\\', '/').TrimEnd('/') ?? ""; // Make the directory source path absolute if (!Path.IsPathRooted(dirSource)) { dirSource = Path.Combine(sourceRoot, dirSource); dirSource = TaskEnvironment.GetAbsolutePath(dirSource).GetCanonicalForm().Value.Replace('\\', '/').TrimEnd('/'); } return string.Equals(dirSource, normalizedSourceDir, StringComparison.OrdinalIgnoreCase); }); if (directory == null) { Log.LogError("Source directory '{0}' not found in CMake File API response.", SourceDirectory); return false; } Log.LogMessage(MessageImportance.Low, "Found matching directory: {0}", SourceDirectory); // Get artifacts var artifacts = new List<ITaskItem>(); if (directory.TargetIndexes != null) { foreach (int targetIndex in directory.TargetIndexes) { if (targetIndex < 0 || targetIndex >= config.Targets.Count) { continue; } var target = config.Targets[targetIndex]; if (string.IsNullOrEmpty(target.JsonFile)) { continue; } string targetFile = Path.Combine(replyDir, target.JsonFile); AbsolutePath targetFilePath = TaskEnvironment.GetAbsolutePath(targetFile); if (!File.Exists(targetFilePath)) { continue; } Log.LogMessage(MessageImportance.Low, "Reading target file: {0}", targetFile); // Read target details string targetJson = File.ReadAllText(targetFilePath); var targetDetails = JsonSerializer.Deserialize<CMakeTargetDetails>(targetJson, options); // Get artifacts if (targetDetails?.Artifacts != null) { foreach (var artifact in targetDetails.Artifacts) { if (!string.IsNullOrEmpty(artifact.Path)) { string fullPath = Path.Combine(CMakeOutputDir, artifact.Path); // Emitted as an item spec, and combining the output dir with a // CMake-relative artifact path routinely produces ".." segments // that Path.GetFullPath used to resolve. fullPath = TaskEnvironment.GetAbsolutePath(fullPath).GetCanonicalForm(); var item = new TaskItem(fullPath); artifacts.Add(item); Log.LogMessage(MessageImportance.Low, "Found artifact: {0}", fullPath); } } } } } if (artifacts.Count == 0) { Log.LogWarning("No artifacts found for source directory '{0}' in configuration '{1}'.", SourceDirectory, Configuration); } Artifacts = artifacts.ToArray(); Log.LogMessage(MessageImportance.Normal, "Found {0} artifact(s) for source directory '{1}' in configuration '{2}'", Artifacts.Length, SourceDirectory, Configuration); return true; } catch (Exception ex) { Log.LogErrorFromException(ex, showStackTrace: false); return false; } } }