From b10273f526b2ba177b0d1399bb21339c91a5112d Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 21 Oct 2026 04:04:50 +0800 Subject: [PATCH] Honor explicit binlog SelfContained value --- .../MSBuildBinaryLogComponentDetector.cs | 11 ++++--- .../MSBuildBinaryLogComponentDetectorTests.cs | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/nuget/MSBuildBinaryLogComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/nuget/MSBuildBinaryLogComponentDetector.cs index f155b0bbe..5c2bfbfaa 100644 --- a/src/Microsoft.ComponentDetection.Detectors/nuget/MSBuildBinaryLogComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/nuget/MSBuildBinaryLogComponentDetector.cs @@ -278,8 +278,8 @@ private static MSBuildProjectInfo GetInnerBuildOrDefault(MSBuildProjectInfo proj /// Determines if a project is self-contained using MSBuild properties from the binlog. /// Reads the SelfContained and PublishAot properties directly. /// - private static bool IsSelfContainedFromProjectInfo(MSBuildProjectInfo projectInfo) => - projectInfo.SelfContained == true || projectInfo.PublishAot == true; + private static bool? IsSelfContainedFromProjectInfo(MSBuildProjectInfo projectInfo) => + projectInfo.SelfContained ?? (projectInfo.PublishAot == true ? true : null); private void ProcessBinlogFile(string binlogPath, CancellationToken cancellationToken) { @@ -389,7 +389,8 @@ private void RegisterDotNetComponent(MSBuildProjectInfo projectInfo, string reco var singleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder(recorderLocation); - // Primary self-contained check from binlog properties (SelfContained, PublishAot) + // Prefer an explicit SelfContained value from the binlog; use PublishAot as a positive signal. + // A null result means the lock file heuristic may be used as a fallback. var isSelfContainedFromBinlog = IsSelfContainedFromProjectInfo(projectInfo); if (lockFile != null) @@ -401,7 +402,7 @@ private void RegisterDotNetComponent(MSBuildProjectInfo projectInfo, string reco { var innerInfo = GetInnerBuildOrDefault(projectInfo, target.TargetFramework); var targetType = GetTargetType(innerInfo.OutputType); - var isSelfContained = isSelfContainedFromBinlog || + var isSelfContained = isSelfContainedFromBinlog ?? LockFileUtilities.IsSelfContainedFromLockFile(lockFile.PackageSpec, target.TargetFramework, target); var projectType = LockFileUtilities.GetTargetTypeWithSelfContained(targetType, isSelfContained); var frameworkName = target.TargetFramework?.GetShortFolderName(); @@ -420,7 +421,7 @@ private void RegisterDotNetComponent(MSBuildProjectInfo projectInfo, string reco // Binlog-only path: no lock file available or no targets in lock file // Determine target type from outer build OutputType property. var targetTypeFromBinlog = GetTargetType(projectInfo.OutputType); - var projectTypeFromBinlog = LockFileUtilities.GetTargetTypeWithSelfContained(targetTypeFromBinlog, isSelfContainedFromBinlog); + var projectTypeFromBinlog = LockFileUtilities.GetTargetTypeWithSelfContained(targetTypeFromBinlog, isSelfContainedFromBinlog ?? false); // Get target frameworks from binlog properties var targetFrameworks = new List(); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/nuget/MSBuildBinaryLogComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/nuget/MSBuildBinaryLogComponentDetectorTests.cs index fbf82a9c1..dc7998be9 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/nuget/MSBuildBinaryLogComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/nuget/MSBuildBinaryLogComponentDetectorTests.cs @@ -472,6 +472,25 @@ public async Task WithBinlog_NotSelfContained_RegistersPlainApplication() dotNet.ProjectType.Should().Be("application"); } + [TestMethod] + public async Task WithBinlog_ExplicitSelfContainedFalse_OverridesLockFileRuntimePackHeuristic() + { + var info = CreateProjectInfo(); + info.NETCoreSdkVersion = "8.0.100"; + info.OutputType = "Library"; + info.SelfContained = false; + var assetsJson = RuntimePackDownloadAssetsJson(); + + var (result, recorder) = await ExecuteWithBinlogAsync([info], assetsJson); + + result.ResultCode.Should().Be(ProcessingResultCode.Success); + var dotNet = recorder.GetDetectedComponents() + .Where(c => c.Component is DotNetComponent) + .Select(c => (DotNetComponent)c.Component) + .Single(); + dotNet.ProjectType.Should().Be("library"); + } + [TestMethod] public async Task WithBinlog_MultiTarget_PerTfmDevDependency() { @@ -934,6 +953,18 @@ private static string SimpleAssetsJson(string packageName, string version) => $$ } """; + private static string RuntimePackDownloadAssetsJson() + { + const string frameworkDependencies = "\"dependencies\": {"; + const string runtimePackDownload = """ + "frameworkReferences": { "Microsoft.NETCore.App": { "privateAssets": "all" } }, + "downloadDependencies": [ { "name": "Microsoft.NETCore.App.Runtime.linux-x64", "version": "[8.0.0, 8.0.0]" } ], + "dependencies": { + """; + return SimpleAssetsJson("Newtonsoft.Json", "13.0.1") + .Replace(frameworkDependencies, runtimePackDownload, StringComparison.Ordinal); + } + /// /// Assets JSON with a top-level package that has a transitive dependency. /// Microsoft.Extensions.Logging → Microsoft.Extensions.Logging.Abstractions.