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 811150bfc4b..b6ba7ecf35e 100644 --- a/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj +++ b/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Microsoft.DotNet.CMake.Sdk/README.md b/src/Microsoft.DotNet.CMake.Sdk/README.md index 8c8ef574b36..c913b486b68 100644 --- a/src/Microsoft.DotNet.CMake.Sdk/README.md +++ b/src/Microsoft.DotNet.CMake.Sdk/README.md @@ -118,6 +118,18 @@ All assets that are a direct output of this CMakeLists.txt will be copied to you By default, a NativeProjectReference will not build the native project. It assumes that the project has already been built. To build the project as part of the reference, you can opt-in by setting the `BuildNative="true"` metadata on the `NativeProjectReference`. +#### Referencing CMake targets with CMakeProjectReference + +To build and copy the artifacts for particular CMake targets, add a `CMakeProjectReference` item. Its `CMakeTargets` metadata contains the target names to build: + +```xml + + + +``` + +The target artifacts are read from the CMake File API target descriptions and copied to the consuming project's output folder. Multiple target references to the same CMake SDK project are supported. CMake configure and build executions are serialized per CMake build tree across processes to avoid concurrent native tool access. + ### Generating a raw build script for bringup scenarios This SDK also supports outputting the script that the SDK runs to configure and build your CMake project. This feature can be used to generate a simple script for use in bringup scenarios where we don't have MSBuild available for the device we are building on. This would enable teams to generate bringup build scripts when needed instead of using bringup-style scripts at all times or having unused bringup scripts that quickly bitrot. 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 00ce4d0abcd..8b4d2334c36 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 @@ -9,6 +9,7 @@ + @@ -182,10 +183,13 @@ install <_CMakeParallelizationArgument Condition="'$(CMakeParallelization)' != ''">-g $(CMakeParallelization) + + <_CMakeBuildTargets Include="$(CMakeBuildTarget)" /> + - cmake --build "$(CMakeOutputDir)" --target $(CMakeBuildTarget) $(_CMakeParallelizationArgument) --config $(Configuration) -- @(CMakeNativeToolArguments->'%(Identity)',' ') + cmake --build "$(CMakeOutputDir)" --target @(_CMakeBuildTargets->'%(Identity)',' ') $(_CMakeParallelizationArgument) --config $(Configuration) -- @(CMakeNativeToolArguments->'%(Identity)',' ') @@ -198,7 +202,9 @@ - @@ -208,7 +214,9 @@ - diff --git a/src/Microsoft.DotNet.CMake.Sdk/sdk/ProjectReference.targets b/src/Microsoft.DotNet.CMake.Sdk/sdk/ProjectReference.targets index f69c4c0058a..231d697b9f9 100644 --- a/src/Microsoft.DotNet.CMake.Sdk/sdk/ProjectReference.targets +++ b/src/Microsoft.DotNet.CMake.Sdk/sdk/ProjectReference.targets @@ -12,12 +12,20 @@ + + + + + + + + @@ -46,20 +54,46 @@ + + + + + + + + + + DependsOnTargets="NormalizeCMakeProjectReferences" + BeforeTargets="Build"> - <_NativeProjectReferenceToBuild Include="%(NativeProjectReferenceNormalized.CMakeProject)" - Condition="'%(NativeProjectReferenceNormalized.BuildNative)' == 'true'" - AdditionalProperties="%(NativeProjectReferenceNormalized.AdditionalProperties)" /> + <_NativeProjectsToBuild Include="%(NativeProjectReferenceNormalized.CMakeProject)" + Condition="'%(NativeProjectReferenceNormalized.BuildNative)' == 'true'"> + %(NativeProjectReferenceNormalized.AdditionalProperties) + + <_CMakeProjectsToBuild Include="%(CMakeProjectReferenceNormalized.Identity)"> + %(CMakeProjectReferenceNormalized.CMakeTargets) + %(CMakeProjectReferenceNormalized.AdditionalProperties) + - - + + - - + + + @@ -70,6 +104,13 @@ %(NativeProjectReferenceNormalized.AdditionalProperties)" Condition="'@(NativeProjectReference)' != ''" /> + + @@ -93,6 +134,7 @@ diff --git a/src/Microsoft.DotNet.CMake.Sdk/src/ExecWithMutex.cs b/src/Microsoft.DotNet.CMake.Sdk/src/ExecWithMutex.cs new file mode 100644 index 00000000000..ca90a04aa10 --- /dev/null +++ b/src/Microsoft.DotNet.CMake.Sdk/src/ExecWithMutex.cs @@ -0,0 +1,67 @@ +// 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.Tasks; +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading; + +namespace Microsoft.DotNet.CMake.Sdk; + +/// +/// Executes a command while holding a named, cross-process mutex. +/// +public sealed class ExecWithMutex : Exec +{ + [Required] + public string MutexName { get; set; } + + public override bool Execute() + { + using var mutex = CreateMutex(MutexName); + bool ownsMutex = false; + + try + { + try + { + mutex.WaitOne(); + } + catch (AbandonedMutexException) + { + } + + ownsMutex = true; + return base.Execute(); + } + finally + { + if (ownsMutex) + { + mutex.ReleaseMutex(); + } + } + } + + private Mutex CreateMutex(string path) + { + string key = TaskEnvironment.GetAbsolutePath(path).GetCanonicalForm().Value; + using var hasher = SHA256.Create(); + string hash = Convert.ToBase64String(hasher.ComputeHash(Encoding.UTF8.GetBytes(key))) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + +#if NETFRAMEWORK + return new Mutex(false, $"Local\\Microsoft.DotNet.CMake.Sdk.{hash}"); +#else + return new Mutex( + false, + $"Microsoft.DotNet.CMake.Sdk.{hash}", + new NamedWaitHandleOptions { CurrentUserOnly = true }, + out _); +#endif + } +} diff --git a/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs b/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs index 7a51dbcd883..7b6c3ba51f0 100644 --- a/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs +++ b/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs @@ -12,7 +12,7 @@ namespace Microsoft.DotNet.CMake.Sdk; /// -/// Reads CMake File API response to find artifacts for a specific source directory. +/// Reads CMake File API response to find artifacts for a specific source directory or target. /// [MSBuildMultiThreadableTask] public class GetCMakeArtifactsFromFileApi : Task, IMultiThreadableTask @@ -29,9 +29,13 @@ public class GetCMakeArtifactsFromFileApi : Task, IMultiThreadableTask /// /// The source directory of the CMakeLists.txt to find artifacts for. /// - [Required] public string SourceDirectory { get; set; } + /// + /// Semicolon-separated CMake target names to find artifacts for. + /// + public string CMakeTargets { get; set; } + /// /// The configuration name (e.g., Debug, Release). /// @@ -105,12 +109,6 @@ public override bool Execute() // 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)); @@ -123,84 +121,107 @@ public override bool Execute() Log.LogMessage(MessageImportance.Low, "Found configuration: {0}", Configuration); - if (config.Directories == null || config.Targets == null) + if (config.Targets == null || (string.IsNullOrEmpty(CMakeTargets) && config.Directories == 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 => + // Get artifacts + var artifacts = new List(); + IEnumerable targets; + if (!string.IsNullOrEmpty(CMakeTargets)) { - string dirSource = d.Source?.Replace('\\', '/').TrimEnd('/') ?? ""; - - // Make the directory source path absolute - if (!Path.IsPathRooted(dirSource)) + var requestedTargets = CMakeTargets.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var missingTargets = requestedTargets + .Where(requestedTarget => !config.Targets.Any(target => + string.Equals(target.Name, requestedTarget, StringComparison.OrdinalIgnoreCase))) + .ToArray(); + if (missingTargets.Length > 0) { - dirSource = Path.Combine(sourceRoot, dirSource); - dirSource = TaskEnvironment.GetAbsolutePath(dirSource).GetCanonicalForm().Value.Replace('\\', '/').TrimEnd('/'); + Log.LogError( + "The requested CMake target(s) were not found in the File API response: {0}.", + string.Join(", ", missingTargets)); + return false; } - - 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; + targets = config.Targets.Where(t => requestedTargets.Contains(t.Name, StringComparer.OrdinalIgnoreCase)); + Log.LogMessage(MessageImportance.Low, "Found {0} requested CMake target(s) for configuration '{1}'.", targets.Count(), Configuration); } - - Log.LogMessage(MessageImportance.Low, "Found matching directory: {0}", SourceDirectory); - - // Get artifacts - var artifacts = new List(); - - if (directory.TargetIndexes != null) + else { - foreach (int targetIndex in directory.TargetIndexes) + if (string.IsNullOrEmpty(SourceDirectory)) { - if (targetIndex < 0 || targetIndex >= config.Targets.Count) - { - continue; - } + Log.LogError("Either SourceDirectory or CMakeTargets must be specified."); + return false; + } - var target = config.Targets[targetIndex]; - if (string.IsNullOrEmpty(target.JsonFile)) + // 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('/'); + var directory = config.Directories.FirstOrDefault(d => + { + string dirSource = d.Source?.Replace('\\', '/').TrimEnd('/') ?? ""; + if (!Path.IsPathRooted(dirSource)) { - continue; + dirSource = Path.Combine(sourceRoot, dirSource); } + dirSource = TaskEnvironment.GetAbsolutePath(dirSource).GetCanonicalForm().Value.Replace('\\', '/').TrimEnd('/'); - string targetFile = Path.Combine(replyDir, target.JsonFile); - AbsolutePath targetFilePath = TaskEnvironment.GetAbsolutePath(targetFile); - if (!File.Exists(targetFilePath)) - { - continue; - } + 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); + targets = directory.TargetIndexes? + .Where(targetIndex => targetIndex >= 0 && targetIndex < config.Targets.Count) + .Select(targetIndex => config.Targets[targetIndex]) + ?? Enumerable.Empty(); + } + + foreach (var target in targets) + { + if (string.IsNullOrEmpty(target.JsonFile)) + { + continue; + } - Log.LogMessage(MessageImportance.Low, "Reading target file: {0}", targetFile); + string targetFile = Path.Combine(replyDir, target.JsonFile); + AbsolutePath targetFilePath = TaskEnvironment.GetAbsolutePath(targetFile); + if (!File.Exists(targetFilePath)) + { + continue; + } - // Read target details - string targetJson = File.ReadAllText(targetFilePath); - var targetDetails = JsonSerializer.Deserialize(targetJson, options); + Log.LogMessage(MessageImportance.Low, "Reading target file: {0}", targetFile); - // Get artifacts - if (targetDetails?.Artifacts != null) + // Read target details + string targetJson = File.ReadAllText(targetFilePath); + var targetDetails = JsonSerializer.Deserialize(targetJson, options); + + // Get artifacts + if (targetDetails?.Artifacts != null) + { + foreach (var artifact in targetDetails.Artifacts) { - foreach (var artifact in targetDetails.Artifacts) + if (!string.IsNullOrEmpty(artifact.Path)) { - 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); - } + 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); } } } @@ -208,11 +229,23 @@ public override bool Execute() if (artifacts.Count == 0) { - Log.LogWarning("No artifacts found for source directory '{0}' in configuration '{1}'.", SourceDirectory, Configuration); + Log.LogWarning( + string.IsNullOrEmpty(CMakeTargets) + ? "No artifacts found for source directory '{0}' in configuration '{1}'." + : "No artifacts found for CMake target(s) '{0}' in configuration '{1}'.", + string.IsNullOrEmpty(CMakeTargets) ? SourceDirectory : CMakeTargets, + Configuration); } Artifacts = artifacts.ToArray(); - Log.LogMessage(MessageImportance.Normal, "Found {0} artifact(s) for source directory '{1}' in configuration '{2}'", Artifacts.Length, SourceDirectory, Configuration); + Log.LogMessage( + MessageImportance.Normal, + string.IsNullOrEmpty(CMakeTargets) + ? "Found {0} artifact(s) for source directory '{1}' in configuration '{2}'" + : "Found {0} artifact(s) for CMake target(s) '{1}' in configuration '{2}'", + Artifacts.Length, + string.IsNullOrEmpty(CMakeTargets) ? SourceDirectory : CMakeTargets, + Configuration); return true; }