Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1749,5 +1749,109 @@ static void Main()
output.ShouldNotContain("A=1");
output.ShouldContain("B=1");
}

[Fact]
public void CachePluginsFeatureFlagDisablesCache()
{
// This test verifies that when the CachePlugins feature is disabled via environment variable,
// the cache plugin is not used even when configured.

_env.SetEnvironmentVariable("MSBUILD_CACHEPLUGINS_DISABLED", "1");

try
{
var testData = new GraphCacheResponse(
new Dictionary<int, int[]?>
{
{1, null}
},
new Dictionary<int, CacheResult>
{
{1, GraphCacheResponse.SuccessfulProxyTargetResult()}
});

var graph = testData.CreateGraph(_env);

var mockCache = new InstanceMockCache(testData);

var buildParameters = new BuildParameters
{
ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(mockCache)
};

MockLogger logger;
GraphBuildResult graphResult;
using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
{
logger = buildSession.Logger;
graphResult = buildSession.BuildGraph(graph);
}

graphResult.OverallResult.ShouldBe(BuildResultCode.Success);

// Verify that cache was NOT used (plugin methods were not called)
mockCache.BeginBuildCalled.ShouldBeFalse("BeginBuildAsync should not be called when feature is disabled");
mockCache.Requests.ShouldBeEmpty("GetCacheResultAsync should not be called when feature is disabled");
mockCache.EndBuildCalled.ShouldBeFalse("EndBuildAsync should not be called when feature is disabled");

// The build should have succeeded, but without using the cache
logger.FullLog.ShouldNotContain("MockCache: BeginBuildAsync");
logger.FullLog.ShouldNotContain("MockCache: GetCacheResultAsync");
logger.FullLog.ShouldNotContain("MockCache: EndBuildAsync");
}
finally
{
_env.SetEnvironmentVariable("MSBUILD_CACHEPLUGINS_DISABLED", null);
}
}

[Fact]
public void CachePluginsFeatureFlagEnabled()
{
// This test verifies that when the CachePlugins feature is enabled (default),
// the cache plugin is used as expected.

// Ensure the environment variable is not set (feature enabled by default)
_env.SetEnvironmentVariable("MSBUILD_CACHEPLUGINS_DISABLED", null);

var testData = new GraphCacheResponse(
new Dictionary<int, int[]?>
{
{1, null}
},
new Dictionary<int, CacheResult>
{
{1, GraphCacheResponse.SuccessfulProxyTargetResult()}
});

var graph = testData.CreateGraph(_env);

var mockCache = new InstanceMockCache(testData);

var buildParameters = new BuildParameters
{
ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(mockCache)
};

MockLogger logger;
GraphBuildResult graphResult;
using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
{
logger = buildSession.Logger;
graphResult = buildSession.BuildGraph(graph);
}

graphResult.OverallResult.ShouldBe(BuildResultCode.Success);

// Verify that cache WAS used (plugin methods were called)
mockCache.BeginBuildCalled.ShouldBeTrue("BeginBuildAsync should be called when feature is enabled");
mockCache.Requests.ShouldNotBeEmpty("GetCacheResultAsync should be called when feature is enabled");
mockCache.EndBuildCalled.ShouldBeTrue("EndBuildAsync should be called when feature is enabled");

// The build should have succeeded using the cache
logger.FullLog.ShouldContain("MockCache: BeginBuildAsync");
logger.FullLog.ShouldContain("MockCache: GetCacheResultAsync");
logger.FullLog.ShouldContain("MockCache: EndBuildAsync");
}
}
}
18 changes: 18 additions & 0 deletions src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ public void InitializePluginsForGraph(
{
EnsureNotDisposed();

// Check if cache plugins feature is enabled
if (Framework.Features.CheckFeatureAvailability("CachePlugins") == Framework.FeatureStatus.NotAvailable)
{
return;
}

// Performing this in a Task.Run to break away from the main thread and prevent hangs
Task.Run(
() =>
Expand Down Expand Up @@ -133,6 +139,12 @@ public void InitializePluginsForVsScenario(
{
EnsureNotDisposed();

// Check if cache plugins feature is enabled
if (Framework.Features.CheckFeatureAvailability("CachePlugins") == Framework.FeatureStatus.NotAvailable)
{
return;
}

_isVsScenario = true;

// Bail out for design-time builds
Expand Down Expand Up @@ -467,6 +479,12 @@ IEnumerable<Type> GetTypes<T>(Assembly assembly)

public bool ShouldUseCache(BuildRequestConfiguration buildRequestConfiguration)
{
// Check if cache plugins feature is enabled via remote feature flag
if (Framework.Features.CheckFeatureAvailability("CachePlugins") == Framework.FeatureStatus.NotAvailable)
{
return false;
}

if (IsDesignTimeBuild(buildRequestConfiguration))
{
return false;
Expand Down
55 changes: 49 additions & 6 deletions src/Framework/Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@ public enum FeatureStatus
/// </summary>
public static class Features
{
private static readonly Dictionary<string, FeatureStatus> _featureStatusMap = new Dictionary<string, FeatureStatus>
private static readonly Dictionary<string, FeatureStatus> _featureStatusMap = CreateDefaultFeatureStatusMap();

private static Dictionary<string, FeatureStatus> CreateDefaultFeatureStatusMap()
{
{ "BuildCheck.Beta", FeatureStatus.Preview },
{ "EvaluationContext_SharedSDKCachePolicy", FeatureStatus.Available }, // EvaluationContext supports the SharingPolicy.SharedSDKCache flag.
{ "TerminalLogger_MultiLineHandler", FeatureStatus.Available }, // TerminalLogger has better explicit support for rendering multi-line messages
// Add more features here.
};
return new Dictionary<string, FeatureStatus>
{
{ "BuildCheck.Beta", FeatureStatus.Preview },
{ "EvaluationContext_SharedSDKCachePolicy", FeatureStatus.Available }, // EvaluationContext supports the SharingPolicy.SharedSDKCache flag.
{ "TerminalLogger_MultiLineHandler", FeatureStatus.Available }, // TerminalLogger has better explicit support for rendering multi-line messages
// Add more features here.
};
}

/// <summary>
/// Checks if a feature is available or not.
Expand All @@ -51,8 +56,46 @@ public static class Features
/// <returns>A feature status <see cref="FeatureStatus"/>.</returns>
public static FeatureStatus CheckFeatureAvailability(string featureName)
{
// Special handling for CachePlugins feature - controlled via environment variable
// This allows remote feature flags in VS to disable cache plugins by setting the environment variable
if (featureName == "CachePlugins")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it's not a part of CreateDefaultFeatureStatusMap?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why it's not a part of CreateDefaultFeatureStatusMap?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CachePlugins feature needs to check the MSBUILD_CACHEPLUGINS_DISABLED environment variable at runtime, which is different from other features that have static values in the map. The environment variable approach allows VS to dynamically control the feature through remote flags by setting the env var before MSBuild starts, following the same pattern as MSBUILD_TELEMETRY_OPTOUT. If we put it in CreateDefaultFeatureStatusMap(), it would only check the env var once at initialization, rather than on each call to CheckFeatureAvailability().

{
string? envValue = System.Environment.GetEnvironmentVariable("MSBUILD_CACHEPLUGINS_DISABLED");
if (!string.IsNullOrEmpty(envValue) &&
(envValue.Equals("1", System.StringComparison.OrdinalIgnoreCase) ||
envValue.Equals("true", System.StringComparison.OrdinalIgnoreCase)))
{
return FeatureStatus.NotAvailable;
}
return FeatureStatus.Available;
}

return _featureStatusMap.TryGetValue(featureName, out FeatureStatus status) ?
status : FeatureStatus.Undefined;
}

#if DEBUG
/// <summary>
/// Sets the status of a feature. Used for testing only.
/// </summary>
/// <param name="featureName">The name of the feature.</param>
/// <param name="status">The status to set.</param>
internal static void SetFeatureAvailability(string featureName, FeatureStatus status)
{
_featureStatusMap[featureName] = status;
}

/// <summary>
/// Resets the feature status map to its default state. Used for testing only.
/// </summary>
internal static void ResetFeatureStatusForTests()
{
_featureStatusMap.Clear();
foreach (var kvp in CreateDefaultFeatureStatusMap())
{
_featureStatusMap[kvp.Key] = kvp.Value;
}
}
#endif
}
}