diff --git a/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj b/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj index 47b6a076af6..aa548e94ed5 100644 --- a/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj +++ b/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj @@ -1,19 +1,47 @@ - + - $(NetToolCurrent) + $(NetToolCurrent);$(NetFrameworkToolCurrent) true + true Common toolset for calling into CMake from MSBuild and easily reference native assets from managed projects. MSBuildSdk - $(NoWarn);NU5128 + $(NoWarn);NU5128;CS0649 true + false - + + + + + + + + + + + + + + + + + + + + + + + + + - + PackagePath="sdk\%(RecursiveDir)%(Filename)%(Extension)" /> diff --git a/src/Microsoft.DotNet.CMake.Sdk/build/Microsoft.DotNet.CMake.Sdk.targets b/src/Microsoft.DotNet.CMake.Sdk/build/Microsoft.DotNet.CMake.Sdk.targets index c34dc5d02ed..b5a2339b04d 100644 --- a/src/Microsoft.DotNet.CMake.Sdk/build/Microsoft.DotNet.CMake.Sdk.targets +++ b/src/Microsoft.DotNet.CMake.Sdk/build/Microsoft.DotNet.CMake.Sdk.targets @@ -1,6 +1,14 @@ + + $(MSBuildThisFileDirectory)..\tools\net\Microsoft.DotNet.CMake.Sdk.dll + $(MSBuildThisFileDirectory)..\tools\netframework\Microsoft.DotNet.CMake.Sdk.dll + + + + + @@ -92,6 +100,12 @@ + + + + + + @@ -175,8 +189,14 @@ + + + + + DependsOnTargets="GetConfigScript;CreateFileApiQuery"> diff --git a/src/Microsoft.DotNet.CMake.Sdk/sdk/ProjectReference.targets b/src/Microsoft.DotNet.CMake.Sdk/sdk/ProjectReference.targets index ad3c2720d8c..4ef4ca1c9c5 100644 --- a/src/Microsoft.DotNet.CMake.Sdk/sdk/ProjectReference.targets +++ b/src/Microsoft.DotNet.CMake.Sdk/sdk/ProjectReference.targets @@ -1,6 +1,13 @@ + + $(MSBuildThisFileDirectory)..\tools\net\Microsoft.DotNet.CMake.Sdk.dll + $(MSBuildThisFileDirectory)..\tools\netframework\Microsoft.DotNet.CMake.Sdk.dll + + + + @@ -11,80 +18,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -116,12 +49,6 @@ - - - - <_NativeProjectReferenceToBuild Include="%(NativeProjectReferenceNormalized.CMakeProject)" Condition="'%(NativeProjectReferenceNormalized.BuildNative)' == 'true'" @@ -135,17 +62,62 @@ - + + + + + + + + + + + + + <_SourceDirectory>$([System.IO.Path]::GetDirectoryName($(ReferencedCMakeLists))) + + + + + + + + + + <_ArtifactsToCopy Include="@(NativeProjectBinaries)" Condition="Exists('%(Identity)')" /> + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.CMake.Sdk/src/CMakeFileApiModels.cs b/src/Microsoft.DotNet.CMake.Sdk/src/CMakeFileApiModels.cs new file mode 100644 index 00000000000..96173cd1894 --- /dev/null +++ b/src/Microsoft.DotNet.CMake.Sdk/src/CMakeFileApiModels.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.DotNet.CMake.Sdk +{ + /// + /// C# object models for the CMake File API. + /// These types represent a subset of the CMake File API that we use for discovering build artifacts. + /// + /// For the full CMake File API specification, see: + /// https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html + /// + /// Specifically, we use: + /// - Index file: https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html#index-file + /// - Codemodel object (v2): https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html#codemodel-version-2 + /// + + internal sealed class CMakeFileApiIndex + { + public CMakeFileApiIndexReply Reply { get; set; } + } + + internal sealed class CMakeFileApiIndexReply + { + [JsonPropertyName("client-Microsoft.DotNet.CMake.Sdk")] + public CMakeFileApiClientReply ClientReply { get; set; } + } + + internal sealed class CMakeFileApiClientReply + { + [JsonPropertyName("codemodel-v2")] + public CMakeFileApiIndexCodeModel CodemodelV2 { get; set; } + } + + internal sealed class CMakeFileApiIndexCodeModel + { + public string JsonFile { get; set; } + } + + internal sealed class CMakeCodeModel + { + public CMakeCodeModelPaths Paths { get; set; } + public List Configurations { get; set; } + } + + internal sealed class CMakeCodeModelPaths + { + public string Source { get; set; } + public string Build { get; set; } + } + + internal sealed class CMakeConfiguration + { + public string Name { get; set; } + public List Directories { get; set; } + public List Targets { get; set; } + } + + internal sealed class CMakeDirectory + { + public string Source { get; set; } + public string Build { get; set; } + public List TargetIndexes { get; set; } + } + + internal sealed class CMakeTarget + { + public string Name { get; set; } + public string JsonFile { get; set; } + } + + internal sealed class CMakeTargetDetails + { + public List Artifacts { get; set; } + } + + internal sealed class CMakeArtifact + { + public string Path { get; set; } + } +} diff --git a/src/Microsoft.DotNet.CMake.Sdk/src/CreateCMakeFileApiQuery.cs b/src/Microsoft.DotNet.CMake.Sdk/src/CreateCMakeFileApiQuery.cs new file mode 100644 index 00000000000..56e6fe8abbc --- /dev/null +++ b/src/Microsoft.DotNet.CMake.Sdk/src/CreateCMakeFileApiQuery.cs @@ -0,0 +1,47 @@ +// 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 Microsoft.DotNet.Build.Tasks; +using System; +using System.IO; + +namespace Microsoft.DotNet.CMake.Sdk +{ + /// + /// Creates a CMake File API query file to request codemodel information. + /// + public class CreateCMakeFileApiQuery : BuildTask + { + /// + /// The CMake build output directory where the query should be created. + /// + [Required] + public string CMakeOutputDir { get; set; } + + public override bool Execute() + { + try + { + // Create a client stateless query file with client name "Microsoft.DotNet.CMake.Sdk" + string queryDir = Path.Combine(CMakeOutputDir, ".cmake", "api", "v1", "query", "client-Microsoft.DotNet.CMake.Sdk"); + Directory.CreateDirectory(queryDir); + + string queryFile = Path.Combine(queryDir, "codemodel-v2"); + + // Create an empty file to request codemodel-v2 information + File.WriteAllText(queryFile, string.Empty); + + Log.LogMessage(LogImportance.Low, "Created CMake File API query at: {0}", queryFile); + + return true; + } + catch (Exception ex) + { + Log.LogErrorFromException(ex, showStackTrace: false); + return false; + } + } + } +} diff --git a/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs b/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs new file mode 100644 index 00000000000..8baeea06470 --- /dev/null +++ b/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs @@ -0,0 +1,214 @@ +// 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 Microsoft.DotNet.Build.Tasks; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace Microsoft.DotNet.CMake.Sdk +{ + /// + /// Reads CMake File API response to find artifacts for a specific source directory. + /// + public class GetCMakeArtifactsFromFileApi : BuildTask + { + /// + /// The CMake build output directory containing the File API response. + /// + [Required] + public string CMakeOutputDir { get; set; } + + /// + /// The source directory of the CMakeLists.txt to find artifacts for. + /// + [Required] + public string SourceDirectory { get; set; } + + /// + /// The configuration name (e.g., Debug, Release). + /// + [Required] + public string Configuration { get; set; } + + /// + /// Output: The list of artifact file paths. + /// + [Output] + public ITaskItem[] Artifacts { get; set; } + + public override bool Execute() + { + try + { + string replyDir = Path.Combine(CMakeOutputDir, ".cmake", "api", "v1", "reply"); + + if (!Directory.Exists(replyDir)) + { + Log.LogError("CMake File API reply directory does not exist: {0}", replyDir); + return false; + } + + // Find the latest index file + var indexFiles = Directory.GetFiles(replyDir, "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(LogImportance.Low, "Reading CMake File API index: {0}", indexFile); + + string indexJson = File.ReadAllText(indexFile); + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var index = JsonSerializer.Deserialize(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); + if (!File.Exists(codeModelFile)) + { + Log.LogError("Codemodel file not found: {0}", codeModelFile); + return false; + } + + Log.LogMessage(LogImportance.Low, "Reading codemodel: {0}", codeModelFile); + + string codeModelJson = File.ReadAllText(codeModelFile); + var codeModel = JsonSerializer.Deserialize(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 + string normalizedSourceDir = Path.GetFullPath(SourceDirectory).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(LogImportance.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 = Path.GetFullPath(dirSource).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(LogImportance.Low, "Found matching directory: {0}", SourceDirectory); + + // Get artifacts + var artifacts = new List(); + + 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); + if (!File.Exists(targetFile)) + { + continue; + } + + Log.LogMessage(LogImportance.Low, "Reading target file: {0}", targetFile); + + // Read target details + string targetJson = File.ReadAllText(targetFile); + var targetDetails = JsonSerializer.Deserialize(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); + fullPath = Path.GetFullPath(fullPath); + + var item = new TaskItem(fullPath); + artifacts.Add(item); + + Log.LogMessage(LogImportance.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(LogImportance.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; + } + } + } +}