diff --git a/src/BuildPrediction/Predictors/DotnetSdkPredictor.cs b/src/BuildPrediction/Predictors/DotnetSdkPredictor.cs
new file mode 100644
index 0000000..4095bff
--- /dev/null
+++ b/src/BuildPrediction/Predictors/DotnetSdkPredictor.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using Microsoft.Build.Execution;
+
+namespace Microsoft.Build.Prediction.Predictors;
+
+///
+/// Makes predictions based on using Microsoft.NET.Sdk.
+///
+public sealed class DotnetSdkPredictor : IProjectPredictor
+{
+ internal const string UsingMicrosoftNETSdkPropertyName = "UsingMicrosoftNETSdk";
+
+ private readonly ConcurrentDictionary _globalJsonExistenceCache = new(PathComparer.Instance);
+
+ ///
+ public void PredictInputsAndOutputs(
+ ProjectInstance projectInstance,
+ ProjectPredictionReporter predictionReporter)
+ {
+ var usingMicrosoftNETSdk = projectInstance.GetPropertyValue(UsingMicrosoftNETSdkPropertyName);
+ if (!usingMicrosoftNETSdk.Equals("true", StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ // Microsoft.NET.Sdk reads global.json.
+ string currentProbeDirectory = projectInstance.Directory;
+ while (currentProbeDirectory != null)
+ {
+ string globalJsonPath = Path.Combine(currentProbeDirectory, "global.json");
+ bool globalJsonPathExists = _globalJsonExistenceCache.GetOrAdd(globalJsonPath, File.Exists);
+ if (globalJsonPathExists)
+ {
+ predictionReporter.ReportInputFile(globalJsonPath);
+ break;
+ }
+
+ currentProbeDirectory = Path.GetDirectoryName(currentProbeDirectory);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BuildPrediction/ProjectPredictors.cs b/src/BuildPrediction/ProjectPredictors.cs
index 4cd04ea..e61f721 100644
--- a/src/BuildPrediction/ProjectPredictors.cs
+++ b/src/BuildPrediction/ProjectPredictors.cs
@@ -60,6 +60,7 @@ public static class ProjectPredictors
///
///
///
+ ///
///
///
/// A collection of .
@@ -106,6 +107,7 @@ public static class ProjectPredictors
new ModuleDefinitionFilePredictor(),
new CppContentFilesProjectOutputGroupPredictor(),
new LinkItemsPredictor(),
+ new DotnetSdkPredictor(),
//// NOTE! When adding a new predictor here, be sure to update the doc comment above.
};
diff --git a/src/BuildPredictionTests/Predictors/DotnetSdkPredictorTests.cs b/src/BuildPredictionTests/Predictors/DotnetSdkPredictorTests.cs
new file mode 100644
index 0000000..f21d38e
--- /dev/null
+++ b/src/BuildPredictionTests/Predictors/DotnetSdkPredictorTests.cs
@@ -0,0 +1,115 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using Microsoft.Build.Construction;
+using Microsoft.Build.Execution;
+using Microsoft.Build.Prediction.Predictors;
+using Xunit;
+
+namespace Microsoft.Build.Prediction.Tests.Predictors;
+
+public class DotnetSdkPredictorTests
+{
+ private readonly string _rootDir;
+
+ public DotnetSdkPredictorTests()
+ {
+ // Isolate each test into its own folder
+ _rootDir = Path.Combine(Directory.GetCurrentDirectory(), nameof(DotnetSdkPredictorTests), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(_rootDir);
+ }
+
+ [Fact]
+ public void GlobalJsonExistsAdjacent()
+ {
+ ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"src\project.csproj"));
+ projectRootElement.AddProperty(DotnetSdkPredictor.UsingMicrosoftNETSdkPropertyName, "true");
+
+ Directory.CreateDirectory(Path.Combine(_rootDir, "src"));
+ File.WriteAllText(Path.Combine(_rootDir, @"src\global.json"), "{}");
+
+ // Extraneous one above, which is not predicted as an input.
+ File.WriteAllText(Path.Combine(_rootDir, "global.json"), "{}");
+
+ ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement);
+
+ var expectedInputFiles = new[]
+ {
+ new PredictedItem(@"src\global.json", nameof(DotnetSdkPredictor)),
+ };
+
+ new DotnetSdkPredictor()
+ .GetProjectPredictions(projectInstance)
+ .AssertPredictions(
+ projectInstance,
+ expectedInputFiles.MakeAbsolute(_rootDir),
+ null,
+ null,
+ null);
+ }
+
+ [Fact]
+ public void GlobalJsonExistsAbove()
+ {
+ ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"src\project.csproj"));
+ projectRootElement.AddProperty(DotnetSdkPredictor.UsingMicrosoftNETSdkPropertyName, "true");
+
+ File.WriteAllText(Path.Combine(_rootDir, "global.json"), "{}");
+
+ ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement);
+
+ var expectedInputFiles = new[]
+ {
+ new PredictedItem(@"global.json", nameof(DotnetSdkPredictor)),
+ };
+
+ new DotnetSdkPredictor()
+ .GetProjectPredictions(projectInstance)
+ .AssertPredictions(
+ projectInstance,
+ expectedInputFiles.MakeAbsolute(_rootDir),
+ null,
+ null,
+ null);
+ }
+
+ [Fact]
+ public void NoGlobalJsonExists()
+ {
+ ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"src\project.csproj"));
+ projectRootElement.AddProperty(DotnetSdkPredictor.UsingMicrosoftNETSdkPropertyName, "true");
+
+ ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement);
+
+ new DotnetSdkPredictor()
+ .GetProjectPredictions(projectInstance)
+ .AssertPredictions(
+ projectInstance,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ [Fact]
+ public void NotUsingDotnetSdk()
+ {
+ ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"src\project.csproj"));
+
+ Directory.CreateDirectory(Path.Combine(_rootDir, "src"));
+ File.WriteAllText(Path.Combine(_rootDir, "global.json"), "{}");
+
+ ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement);
+
+ new DotnetSdkPredictor()
+ .GetProjectPredictions(projectInstance)
+ .AssertPredictions(
+ projectInstance,
+ null,
+ null,
+ null,
+ null);
+ }
+}
\ No newline at end of file