From cee58b1ffc7b41f37fff46f2420374d980c63059 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:52:13 +0200 Subject: [PATCH 01/13] [SingleProject] Register and activate external backends for neutral TFMs Extends the SingleProject MauiPlatformSpecificFolder registration contract so an external backend NuGet can identify and activate a backend for both recognized platform TFMs and a neutral TFM, keying compile selection off one resolved backend identity. Registration identity + compile activation only (Phase 1/2/3 scope of #35021). App-head manifest/resource hooks are intentionally out of scope for this PR. Changes: - MauiPlatformSpecificFolder gains additive metadata: BackendIdentity (stable backend key), ActivationProperty (neutral-TFM selector property name, defaults to the well-known MauiActiveBackend), and ActivationValue (defaults to BackendIdentity). Built-in platforms now also declare BackendIdentity so they describe themselves through the same registration shape. - _MauiNormalizePlatformSpecificFolders back-fills the plural TargetPlatformIdentifiers contract and resolves the activation property value (MSBuild dynamic property dereference), pre-normalizing whitespace/case so the collect condition stays parseable. - _MauiCollectPlatformSpecificCompileItems keeps a folder when its recognized TargetPlatformIdentifier(s) match, OR when its ActivationProperty value matches ActivationValue (neutral-TFM backend activation, e.g. net11.0 + GTK), OR when it is an unconditioned/condition-gated folder. Only the active backend's Platforms/ files compile. - Existing built-in, shared-folder (e.g. Platforms/Apple for ios;maccatalyst), singular-metadata backward-compat, and design-time ExcludeFromCurrentConfiguration flow are preserved exactly. - Adds focused MSBuild unit tests: recognized TPI + shared folders, neutral net11.0 GTK-style activation, non-matching backend exclusion, custom activation property/value, singular-metadata backward compatibility, trailing-slash sibling safety, and non-platform/condition-gated participation. Part of #35021 Part of #36650 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- ...Maui.Controls.SingleProject.Before.targets | 120 ++++- ...rosoft.Maui.Controls.SingleProject.targets | 92 +++- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 497 ++++++++++++++++++ 3 files changed, 701 insertions(+), 8 deletions(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.Before.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.Before.targets index 17b91db2f42b..56dee6a5b1e0 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.Before.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.Before.targets @@ -28,14 +28,124 @@ $([MSBuild]::EnsureTrailingSlash('$(TizenProjectFolder)')) + - - - - - + + + + + + + + + + + %(MauiPlatformSpecificFolder.TargetPlatformIdentifier) + + + + + %(MauiPlatformSpecificFolder.BackendIdentity) + + + + + MauiActiveBackend + + + + + <_MauiResolvedActivationValue>$(%(MauiPlatformSpecificFolder.ActivationProperty)) + + + + + <_MauiActivationValueNormalized>$([System.Text.RegularExpressions.Regex]::Replace('%(MauiPlatformSpecificFolder.ActivationValue)', '\s+', '').ToLowerInvariant()) + <_MauiResolvedActivationValueNormalized>$([System.Text.RegularExpressions.Regex]::Replace('%(MauiPlatformSpecificFolder._MauiResolvedActivationValue)', '\s+', '').ToLowerInvariant()) + + + + $(AndroidProjectFolder)AndroidManifest.xml $(AndroidProjectFolder)Resources diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index 6f4069e6d36d..754de28d5fa5 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -35,6 +35,76 @@ + + + + <_MauiPlatformSpecificCompileItems + Include="$([MSBuild]::EnsureTrailingSlash('%(MauiPlatformSpecificFolder.Identity)'))**/*$(DefaultLanguageSourceExtension)" + Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('%(MauiPlatformSpecificFolder.ActivationValue)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> + + + + + + + + false + + + + - - at the top of this file always marks + every $(PlatformsProjectFolder)/** file as ExcludeFromCurrentConfiguration=true, + so this batches into a single iteration; do NOT "simplify" away the + Condition without first re-verifying that contract still holds, or + files outside the active TPI may leak into the build. + --> + <_MauiPlatformCompileToRemove Condition=" '%(Compile.ExcludeFromCurrentConfiguration)' == 'true' " - Remove="$(PlatformsProjectFolder)**/*$(DefaultLanguageSourceExtension)" /> + Include="$(PlatformsProjectFolder)**/*$(DefaultLanguageSourceExtension)" + Exclude="@(_MauiPlatformSpecificCompileItems)" /> + <_MauiXamlToRemove diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index a78e9e6e5359..67774a485858 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -283,6 +283,71 @@ void AssertDoesNotExist(string path) Assert.False(File.Exists(path), $"{path} should *not* exist!"); } + void WriteFile(string name, string contents) + { + var filePath = IOPath.Combine(tempDirectory, name.Replace('\\', IOPath.DirectorySeparatorChar).Replace('/', IOPath.DirectorySeparatorChar)); + Directory.CreateDirectory(IOPath.GetDirectoryName(filePath)); + File.WriteAllText(filePath, contents); + } + + void AssertTypeExists(string assemblyPath, string fullTypeName) + { + using var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); + Assert.Contains(assembly.MainModule.Types.Select(t => t.FullName), t => t == fullTypeName); + } + + void AssertTypeDoesNotExist(string assemblyPath, string fullTypeName) + { + using var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); + Assert.DoesNotContain(assembly.MainModule.Types.Select(t => t.FullName), t => t == fullTypeName); + } + + void AddSingleProjectBeforeTargetsImport(XElement project) + { + var beforeTargetsPath = AssemblyInfoTests.GetFilePathFromRoot(IOPath.Combine("src", "Controls", "src", "Build.Tasks", "nuget", "buildTransitive", "netstandard2.0", "Microsoft.Maui.Controls.SingleProject.Before.targets")); + project.Add(NewElement("Import").WithAttribute("Project", beforeTargetsPath)); + } + + void AddSingleProjectTargetsImport(XElement project) + { + var targetsPath = AssemblyInfoTests.GetFilePathFromRoot(IOPath.Combine("src", "Controls", "src", "Build.Tasks", "nuget", "buildTransitive", "netstandard2.0", "Microsoft.Maui.Controls.SingleProject.targets")); + project.Add(NewElement("Import").WithAttribute("Project", targetsPath)); + + // Assign the synthetic TargetPlatformIdentifier from a private test-only property + // inside a target rather than as a global 'dotnet build' property. Passing a platform + // TPI globally makes the SDK attempt workload resolution during evaluation for what is a + // plain net11.0 (non-platform) project, which fails on CI agents without that workload + // (NETSDK1208 / NETSDK1178) before the SingleProject targets under test ever run. Setting + // it here — after SDK evaluation but before the SingleProject compile-filtering targets — + // keeps the tests workload-neutral while still exercising the TPI-dependent logic (the + // allow-list built by _MauiCollectPlatformSpecificCompileItems determines which files are + // compiled, so it does not rely on the evaluation-time per-TPI Compile metadata flip). + var applyTpiTarget = NewElement("Target") + .WithAttribute("Name", "_ApplyTestTargetPlatformIdentifier") + .WithAttribute("BeforeTargets", "_MauiNormalizePlatformSpecificFolders;_MauiCollectPlatformSpecificCompileItems;_MauiRemovePlatformCompileItems") + .WithAttribute("Condition", " '$(_SingleProjectTestTargetPlatformIdentifier)' != '' "); + var tpiPropertyGroup = NewElement("PropertyGroup"); + tpiPropertyGroup.Add(NewElement("TargetPlatformIdentifier").WithValue("$(_SingleProjectTestTargetPlatformIdentifier)")); + applyTpiTarget.Add(tpiPropertyGroup); + project.Add(applyTpiTarget); + } + + void AddMauiReferences(XElement project) + { + var itemGroup = NewElement("ItemGroup"); + foreach (var assembly in references) + { + var reference = NewElement("Reference").WithAttribute("Include", assembly); + if (assembly.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + reference.Add(NewElement("HintPath").WithValue(IOPath.Combine("..", "..", assembly))); + } + itemGroup.Add(reference); + } + + project.Add(itemGroup); + } + [Fact] public void BuildAProject() { @@ -710,5 +775,437 @@ public void SingleProject_DefaultEntitlementsUsedWhenNoCustomSet() log.Contains("CodesignEntitlements = Platforms\\iOS\\Entitlements.plist", StringComparison.Ordinal), "Default Entitlements.plist should be used when no custom CodesignEntitlements is set"); } + + // --- SingleProject platform-folder registration & activation --------------- + // + // These tests exercise the data-driven MauiPlatformSpecificFolder compile + // selection contract: recognized TargetPlatformIdentifier(s) matching, shared + // folders, backward-compatible singular metadata, condition-gated folders, and + // the neutral-TFM backend activation added for external backends (Part of + // #35021 / Part of #36650). They build a minimal SingleProject csproj that + // imports the real SingleProject Before/After targets so the actual shipping + // logic is under test rather than a copy. + + [Theory] + [InlineData("ios", "ios;maccatalyst", true)] + [InlineData("maccatalyst", "ios;maccatalyst", true)] + [InlineData("android", "ios;maccatalyst", false)] + [InlineData("ios", "ios; maccatalyst", true)] + [InlineData("maccatalyst", "ios; maccatalyst", true)] + [InlineData("android", "ios; maccatalyst", false)] + // Tab and mixed whitespace in the list — see Regex.Replace(\s+, '') in + // _MauiCollectPlatformSpecificCompileItems. ASCII-space-only stripping + // (.Replace(' ', '')) would silently miss these and break shared folders. + [InlineData("ios", "ios;\tmaccatalyst", true)] + [InlineData("maccatalyst", "ios;\tmaccatalyst", true)] + [InlineData("ios", "ios; \t maccatalyst", true)] + [InlineData("maccatalyst", "ios; \t maccatalyst", true)] + public void SingleProject_SharedPlatformFolderMappingsAreRespected(string targetPlatformIdentifier, string targetPlatformIdentifiers, bool shouldIncludeAppleSharedFile) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Apple\\") + .WithAttribute("TargetPlatformIdentifiers", targetPlatformIdentifiers)); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Apple\\AppleSharedMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class AppleSharedMarker +{ + public static string Value => ""Apple""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + Build(projectFile, additionalArgs: $"-p:_SingleProjectTestTargetPlatformIdentifier={targetPlatformIdentifier}"); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + + if (shouldIncludeAppleSharedFile) + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AppleSharedMarker"); + else + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AppleSharedMarker"); + } + + // Regression test: a user-supplied folder declared without a trailing slash + // must NOT match sibling folders sharing a common prefix. Without + // EnsureTrailingSlash() in _MauiCollectPlatformSpecificCompileItems, the + // glob "Platforms\Apple**/*.cs" would silently include AppleX/AppleLegacy. + [Theory] + [InlineData("Platforms\\Apple", "ios")] + [InlineData("Platforms\\Apple\\", "ios")] + [InlineData("Platforms\\Apple", "maccatalyst")] + [InlineData("Platforms\\Apple\\", "maccatalyst")] + public void SingleProject_PlatformFolderWithoutTrailingSlashDoesNotMatchSiblingFolders(string includePath, string targetPlatformIdentifier) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", includePath) + .WithAttribute("TargetPlatformIdentifiers", "ios;maccatalyst")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Apple\\AppleSharedMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class AppleSharedMarker +{ + public static string Value => ""Apple""; +}"); + + // Sibling folder with a common prefix — must NOT be picked up by the + // "Apple" mapping regardless of trailing-slash authoring. + WriteFile("Platforms\\AppleX\\AppleXMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class AppleXMarker +{ + public static string Value => ""AppleX""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + Build(projectFile, additionalArgs: $"-p:_SingleProjectTestTargetPlatformIdentifier={targetPlatformIdentifier}"); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AppleSharedMarker"); + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AppleXMarker"); + } + + // Non-platform builds (TargetPlatformIdentifier empty, e.g. design-time + // or netstandard TFM) must keep removing platform folders that declare a + // non-empty TargetPlatformIdentifiers. Condition-gated folders with empty + // TargetPlatformIdentifiers must still participate when their condition + // evaluates to true — locks in the "empty TPI + empty ActivationValue = + // always include" branch. + [Fact] + public void SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeepsConditionGated() + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Apple\\") + .WithAttribute("TargetPlatformIdentifiers", "ios;maccatalyst")); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Shared\\")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Apple\\AppleSharedMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class AppleSharedMarker +{ + public static string Value => ""Apple""; +}"); + + WriteFile("Platforms\\Shared\\SharedMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class SharedMarker +{ + public static string Value => ""Shared""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + // No -p:_SingleProjectTestTargetPlatformIdentifier — simulates the + // non-platform TFM / design-time evaluation scenario. + Build(projectFile); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AppleSharedMarker"); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.SharedMarker"); + } + + // Backward compatibility: a folder that declares only the legacy singular + // TargetPlatformIdentifier metadata must continue to match exactly that TPI. + [Theory] + [InlineData("ios", true)] + [InlineData("maccatalyst", false)] + [InlineData("android", false)] + public void SingleProject_SingularPlatformFolderMetadataRemainsBackwardCompatible(string targetPlatformIdentifier, bool shouldIncludeLegacyFile) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\LegacyiOS\\") + .WithAttribute("TargetPlatformIdentifier", "ios")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\LegacyiOS\\LegacyIosMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class LegacyIosMarker +{ + public static string Value => ""LegacyiOS""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + Build(projectFile, additionalArgs: $"-p:_SingleProjectTestTargetPlatformIdentifier={targetPlatformIdentifier}"); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + + if (shouldIncludeLegacyFile) + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.LegacyIosMarker"); + else + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.LegacyIosMarker"); + } + + // Neutral-TFM activation (the GTK scenario from #35021/#36650). On a plain + // net11.0 inner build no TargetPlatformIdentifier is recognized; a backend + // declares a stable BackendIdentity and is activated via the well-known + // MauiActiveBackend selector. Only the activated backend's Platforms/ + // files compile; a recognized built-in folder (iOS) stays excluded because + // its TPI does not match the (empty) neutral TPI. + [Fact] + public void SingleProject_NeutralTfmActivatesBackendByIdentity() + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Gtk\\") + .WithAttribute("BackendIdentity", "gtk")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Gtk\\GtkMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class GtkMarker +{ + public static string Value => ""Gtk""; +}"); + + // A built-in recognized folder (iOS) that must NOT come in on a neutral + // build — proves activation is exclusive to the selected backend. + WriteFile("Platforms\\iOS\\IosMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class IosMarker +{ + public static string Value => ""iOS""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + // Neutral TFM (no _SingleProjectTestTargetPlatformIdentifier) + backend selector. + Build(projectFile, additionalArgs: "-p:MauiActiveBackend=gtk"); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.GtkMarker"); + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.IosMarker"); + } + + // A backend that is registered but not selected must be excluded: either + // MauiActiveBackend names a different backend, or it is unset entirely. + [Theory] + [InlineData("cocoa")] + [InlineData("")] + public void SingleProject_NonMatchingBackendIsExcludedOnNeutralTfm(string activeBackend) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Gtk\\") + .WithAttribute("BackendIdentity", "gtk")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Gtk\\GtkMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class GtkMarker +{ + public static string Value => ""Gtk""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + var args = string.IsNullOrEmpty(activeBackend) ? "" : $"-p:MauiActiveBackend={activeBackend}"; + Build(projectFile, additionalArgs: args); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.GtkMarker"); + } + + // A backend may name a custom activation property instead of the default + // MauiActiveBackend selector. The folder compiles only when the named + // property equals the declared ActivationValue. + [Theory] + [InlineData("on", true)] + [InlineData("off", false)] + [InlineData("", false)] + public void SingleProject_CustomActivationPropertyAndValueActivateBackend(string switchValue, bool shouldIncludeFooFile) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Foo\\") + .WithAttribute("BackendIdentity", "foo") + .WithAttribute("ActivationProperty", "MyBackendSwitch") + .WithAttribute("ActivationValue", "on")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Foo\\FooMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class FooMarker +{ + public static string Value => ""Foo""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + var args = string.IsNullOrEmpty(switchValue) ? "" : $"-p:MyBackendSwitch={switchValue}"; + Build(projectFile, additionalArgs: args); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + + if (shouldIncludeFooFile) + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.FooMarker"); + else + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.FooMarker"); + } } } From 98f31a0aac59cccd0cefd586908c1b0937bcd9a4 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:51:38 +0200 Subject: [PATCH 02/13] Limit backend selector to neutral TFMs Prevent MauiActiveBackend from adding neutral backend files to recognized Android, iOS, and other platform inner builds, with focused regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- ...rosoft.Maui.Controls.SingleProject.targets | 6 +- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 58 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index 754de28d5fa5..d4c504cf4bf9 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -65,7 +65,9 @@ into _MauiResolvedActivationValueNormalized, default property MauiActiveBackend) matches the normalized ActivationValue. This is the path a backend like GTK takes on a plain net11.0 inner build where no - TargetPlatformIdentifier is recognized. The normalization target already + TargetPlatformIdentifier is recognized. The empty TargetPlatformIdentifier + guard prevents a neutral backend selector from adding its files to a + recognized Android/iOS/etc. inner build. The normalization target already stripped whitespace and lower-cased both sides, so this is a plain equality. --> <_MauiPlatformSpecificCompileItems Include="$([MSBuild]::EnsureTrailingSlash('%(MauiPlatformSpecificFolder.Identity)'))**/*$(DefaultLanguageSourceExtension)" - Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('%(MauiPlatformSpecificFolder.ActivationValue)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> + Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index 67774a485858..f2b60f138458 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -1098,6 +1098,64 @@ public static class IosMarker AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.IosMarker"); } + // The neutral backend selector must not add backend files to a recognized + // platform inner build. Android remains the only active platform here even + // when MauiActiveBackend names GTK. + [Fact] + public void SingleProject_RecognizedTfmIgnoresNeutralBackendSelector() + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Gtk\\") + .WithAttribute("BackendIdentity", "gtk")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Android\\AndroidMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class AndroidMarker +{ + public static string Value => ""Android""; +}"); + + WriteFile("Platforms\\Gtk\\GtkMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class GtkMarker +{ + public static string Value => ""Gtk""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + Build(projectFile, additionalArgs: "-p:_SingleProjectTestTargetPlatformIdentifier=android -p:MauiActiveBackend=gtk"); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AndroidMarker"); + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.GtkMarker"); + } + // A backend that is registered but not selected must be excluded: either // MauiActiveBackend names a different backend, or it is unset entirely. [Theory] From 52378d312cf4d8ff40dc36c121d7da2b68b56bcc Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:06:31 +0200 Subject: [PATCH 03/13] [SingleProject] Add real Condition-gating and ExcludeFromCurrentConfiguration test coverage Rename SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeepsConditionGated to ...KeepsSharedFolder and correct its comment: the test covers the always-include shared-folder branch, not item-Condition gating. Add SingleProject_ConditionGatedFolderParticipatesOnlyWhenConditionIsTrue (theory, true/false) proving a MauiPlatformSpecificFolder mapping that carries its own MSBuild item Condition participates only when the condition evaluates true and is stripped by _MauiRemovePlatformCompileItems when false. Add SingleProject_UnflipKeptCompileItemMetadata_SetsExcludeFromCurrentConfigurationFalseForKeptSharedFolder proving _MauiUnflipKeptCompileItemMetadata flips ExcludeFromCurrentConfiguration back to false on a kept shared folder's Compile item in a non-platform (design-time-style) build, asserted via a diagnostic Message target that runs after the shipping unflip target. No production behavior changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 170 +++++++++++++++++- 1 file changed, 165 insertions(+), 5 deletions(-) diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index f2b60f138458..fd1ca3664a54 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -916,12 +916,15 @@ public static class AppleXMarker // Non-platform builds (TargetPlatformIdentifier empty, e.g. design-time // or netstandard TFM) must keep removing platform folders that declare a - // non-empty TargetPlatformIdentifiers. Condition-gated folders with empty - // TargetPlatformIdentifiers must still participate when their condition - // evaluates to true — locks in the "empty TPI + empty ActivationValue = - // always include" branch. + // non-empty TargetPlatformIdentifiers. An unconditioned shared folder with + // empty TargetPlatformIdentifiers and empty ActivationValue must still + // participate — locks in the "empty TPI + empty ActivationValue = always + // include" branch of _MauiCollectPlatformSpecificCompileItems. (Genuine + // item-Condition gating — where the mapping carries its own Condition — is + // covered separately by + // SingleProject_ConditionGatedFolderParticipatesOnlyWhenConditionIsTrue.) [Fact] - public void SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeepsConditionGated() + public void SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeepsSharedFolder() { SetUp(); var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); @@ -979,6 +982,163 @@ public static class SharedMarker AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.SharedMarker"); } + // Genuine item-Condition gating: a MauiPlatformSpecificFolder mapping may + // carry its own MSBuild item Condition that decides participation at + // evaluation time. When the Condition is true the item materializes and — + // declaring neither TargetPlatformIdentifiers nor an ActivationValue — is + // kept via the "always include" branch of + // _MauiCollectPlatformSpecificCompileItems, so its Platforms//**/*.cs + // compile even on a non-platform build. When the Condition is false the item + // never exists and _MauiRemovePlatformCompileItems strips the folder like any + // other Platforms/** content. Exercises both branches through the actual + // shipping SingleProject targets. + [Theory] + [InlineData("true", true)] + [InlineData("false", false)] + public void SingleProject_ConditionGatedFolderParticipatesOnlyWhenConditionIsTrue(string conditionValue, bool shouldIncludeConditionalFile) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + // An unconditioned shared folder guarantees @(MauiPlatformSpecificFolder) + // is non-empty in both branches, so the false case still flows through the + // collect/remove pipeline rather than short-circuiting on an empty list. + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Shared\\")); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Conditional\\") + .WithAttribute("Condition", " '$(IncludeConditionalBackend)' == 'true' ")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Shared\\SharedMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class SharedMarker +{ + public static string Value => ""Shared""; +}"); + + WriteFile("Platforms\\Conditional\\ConditionalMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class ConditionalMarker +{ + public static string Value => ""Conditional""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + // No _SingleProjectTestTargetPlatformIdentifier — non-platform build; the + // property toggles the mapping's item Condition to true/false. + Build(projectFile, additionalArgs: $"-p:IncludeConditionalBackend={conditionValue}"); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + // The unconditioned shared folder is always kept regardless of the gate. + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.SharedMarker"); + + if (shouldIncludeConditionalFile) + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.ConditionalMarker"); + else + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.ConditionalMarker"); + } + + // Design-time metadata contract for _MauiUnflipKeptCompileItemMetadata. + // The blanket marks every Platforms/** file + // ExcludeFromCurrentConfiguration=true, and the per-TPI flips only cover the + // built-in $(ProjectFolder) paths. A kept shared folder (empty TPI + + // empty ActivationValue, so it survives the blanket removal) therefore relies + // on _MauiUnflipKeptCompileItemMetadata to flip its Compile item back to + // ExcludeFromCurrentConfiguration=false so Visual Studio does not grey it out. + // This asserts the actual metadata value on the kept item via a diagnostic + // Message target that runs after the shipping unflip target on a non-platform + // (design-time-style) build. + [Fact] + public void SingleProject_UnflipKeptCompileItemMetadata_SetsExcludeFromCurrentConfigurationFalseForKeptSharedFolder() + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Shared\\")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Shared\\SharedMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class SharedMarker +{ + public static string Value => ""Shared""; +}"); + + AddSingleProjectTargetsImport(project); + + // Diagnostic target: after the shipping unflip target runs, emit the + // ExcludeFromCurrentConfiguration metadata for each surviving Compile item + // (one Message per item via cross-item batching) so the test can assert the + // kept shared file was flipped back to false. + var dumpTarget = NewElement("Target") + .WithAttribute("Name", "_TestDumpExcludeFromCurrentConfiguration") + .WithAttribute("AfterTargets", "_MauiUnflipKeptCompileItemMetadata"); + dumpTarget.Add(NewElement("Message") + .WithAttribute("Importance", "high") + .WithAttribute("Condition", " '%(Compile.Identity)' != '' ") + .WithAttribute("Text", "COMPILE_META: %(Compile.Identity)|ExcludeFromCurrentConfiguration=%(Compile.ExcludeFromCurrentConfiguration)")); + project.Add(dumpTarget); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + // Non-platform build (no _SingleProjectTestTargetPlatformIdentifier) mirrors + // the design-time evaluation where the IDE would otherwise grey out the file. + var log = Build(projectFile); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.SharedMarker"); + + // The kept shared file must carry ExcludeFromCurrentConfiguration=false; the + // pipe format keeps the identity (which may use either path separator) and + // the metadata on the same emitted line. + Assert.True( + log.Contains("SharedMarker.cs|ExcludeFromCurrentConfiguration=false", StringComparison.OrdinalIgnoreCase), + "_MauiUnflipKeptCompileItemMetadata should flip ExcludeFromCurrentConfiguration back to false " + + "on the kept shared folder's Compile item. Build log:\n" + log); + } + // Backward compatibility: a folder that declares only the legacy singular // TargetPlatformIdentifier metadata must continue to match exactly that TPI. [Theory] From f6698a5296de8dac2023d3eafc7e9e113b15deaf Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 08:49:04 +0200 Subject: [PATCH 04/13] Scope SingleProject metadata updates Intersect kept platform files with the current Compile items before replacing metadata, preserving unrelated design-time metadata with regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- ...rosoft.Maui.Controls.SingleProject.targets | 21 ++++++++++++++++++- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 9 ++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index d4c504cf4bf9..915771e64bc6 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -101,7 +101,26 @@ AfterTargets="_MauiRemovePlatformCompileItems" Condition=" '$(SingleProject)' == 'true' and '@(_MauiPlatformSpecificCompileItems)' != '' "> - + + <_MauiCompileItemsNotToUnflip Include="@(Compile)" /> + <_MauiCompileItemsNotToUnflip + Remove="@(_MauiPlatformSpecificCompileItems)" + MatchOnMetadata="FullPath" + MatchOnMetadataOptions="PathLike" /> + <_MauiCompileItemsToUnflip Include="@(Compile)" /> + <_MauiCompileItemsToUnflip + Remove="@(_MauiCompileItemsNotToUnflip)" + MatchOnMetadata="FullPath" + MatchOnMetadataOptions="PathLike" /> + + false diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index fd1ca3664a54..ae1f84806f4e 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -1088,6 +1088,12 @@ public void SingleProject_UnflipKeptCompileItemMetadata_SetsExcludeFromCurrentCo .WithAttribute("Include", "Platforms\\Shared\\")); project.Add(customMappings); + var unrelatedCompileMetadata = NewElement("ItemGroup"); + var unrelatedCompile = NewElement("Compile").WithAttribute("Update", "Entry.cs"); + unrelatedCompile.Add(NewElement("ExcludeFromCurrentConfiguration").WithValue("true")); + unrelatedCompileMetadata.Add(unrelatedCompile); + project.Add(unrelatedCompileMetadata); + WriteFile("Entry.cs", @" namespace Microsoft.Maui.Controls.Xaml.UnitTests; @@ -1137,6 +1143,9 @@ public static class SharedMarker log.Contains("SharedMarker.cs|ExcludeFromCurrentConfiguration=false", StringComparison.OrdinalIgnoreCase), "_MauiUnflipKeptCompileItemMetadata should flip ExcludeFromCurrentConfiguration back to false " + "on the kept shared folder's Compile item. Build log:\n" + log); + Assert.True( + log.Contains("Entry.cs|ExcludeFromCurrentConfiguration=true", StringComparison.OrdinalIgnoreCase), + "_MauiUnflipKeptCompileItemMetadata must preserve unrelated Compile metadata. Build log:\n" + log); } // Backward compatibility: a folder that declares only the legacy singular From 51b6692a6af5fd9a0eeb279aa9dbccddd69fb6c7 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:19:09 +0200 Subject: [PATCH 05/13] Cover shared folders on recognized platforms Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index ae1f84806f4e..69bbce2bb196 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -993,9 +993,14 @@ public static class SharedMarker // other Platforms/** content. Exercises both branches through the actual // shipping SingleProject targets. [Theory] - [InlineData("true", true)] - [InlineData("false", false)] - public void SingleProject_ConditionGatedFolderParticipatesOnlyWhenConditionIsTrue(string conditionValue, bool shouldIncludeConditionalFile) + [InlineData("true", "", true)] + [InlineData("false", "", false)] + [InlineData("true", "android", true)] + [InlineData("false", "android", false)] + public void SingleProject_ConditionGatedFolderParticipatesOnlyWhenConditionIsTrue( + string conditionValue, + string targetPlatformIdentifier, + bool shouldIncludeConditionalFile) { SetUp(); var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); @@ -1046,9 +1051,13 @@ public static class ConditionalMarker var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); project.Save(projectFile); - // No _SingleProjectTestTargetPlatformIdentifier — non-platform build; the - // property toggles the mapping's item Condition to true/false. - Build(projectFile, additionalArgs: $"-p:IncludeConditionalBackend={conditionValue}"); + // Exercise both neutral and recognized-platform builds. The property toggles + // the mapping's item Condition to true/false, while the unconditioned shared + // folder must remain included in every case. + var additionalArgs = $"-p:IncludeConditionalBackend={conditionValue}"; + if (!string.IsNullOrEmpty(targetPlatformIdentifier)) + additionalArgs += $" -p:_SingleProjectTestTargetPlatformIdentifier={targetPlatformIdentifier}"; + Build(projectFile, additionalArgs: additionalArgs); var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); AssertExists(testDll, nonEmpty: true); From e66efdd243e1ce24bde93069e6f3d0a0ed57b9a0 Mon Sep 17 00:00:00 2001 From: kubaflo Date: Mon, 27 Jul 2026 19:58:57 +0200 Subject: [PATCH 06/13] Guard shared-folder Branch C on the normalized activation value (#36654 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PureWeen (2/3, both reproduced): Branch C guarded on the RAW '%(MauiPlatformSpecificFolder.ActivationValue)' != '', but the activation match on the same line compares the NORMALIZED values. An all-whitespace ActivationValue (or any value that normalizes to empty) passes the raw != '' guard, normalizes to "", and equals an unset selector's "" — so the folder is force-compiled into every neutral-TFM inner build and can't be deactivated by any backend selection. Guard on the normalized value instead so a whitespace-only ActivationValue is treated as unset, matching the comparison it gates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d --- .../Microsoft.Maui.Controls.SingleProject.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index 915771e64bc6..301f5c7e3517 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -77,7 +77,7 @@ <_MauiPlatformSpecificCompileItems Include="$([MSBuild]::EnsureTrailingSlash('%(MauiPlatformSpecificFolder.Identity)'))**/*$(DefaultLanguageSourceExtension)" - Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> + Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> From c2bd01e9dfc826d626b53617b502d620b7031275 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:29:30 +0200 Subject: [PATCH 07/13] Normalize unset backend activation values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- ...rosoft.Maui.Controls.SingleProject.targets | 10 ++++----- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 22 ++++++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index 301f5c7e3517..ae05f977e328 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -47,10 +47,10 @@ "Platforms\AppleLegacy\". Inclusion contract — a folder is kept when ANY of: - (A) It declares neither a TargetPlatformIdentifiers list nor an - ActivationValue. This is the legacy "always include" / condition-gated - folder (its MauiPlatformSpecificFolder item Condition already decided - participation at evaluation time). + (A) It declares neither a TargetPlatformIdentifiers list nor a non-empty + normalized ActivationValue. This is the legacy "always include" / + condition-gated folder (its MauiPlatformSpecificFolder item Condition + already decided participation at evaluation time). (B) TargetPlatformIdentifiers is a non-empty ';'-delimited list that contains the current recognized $(TargetPlatformIdentifier). Comparison is case-insensitive and tolerates any whitespace (ASCII space, tab, CR, @@ -77,7 +77,7 @@ <_MauiPlatformSpecificCompileItems Include="$([MSBuild]::EnsureTrailingSlash('%(MauiPlatformSpecificFolder.Identity)'))**/*$(DefaultLanguageSourceExtension)" - Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> + Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index 3c5670349964..ddec93a83ae0 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -920,11 +920,11 @@ public static class AppleXMarker // Non-platform builds (TargetPlatformIdentifier empty, e.g. design-time // or netstandard TFM) must keep removing platform folders that declare a // non-empty TargetPlatformIdentifiers. An unconditioned shared folder with - // empty TargetPlatformIdentifiers and empty ActivationValue must still - // participate — locks in the "empty TPI + empty ActivationValue = always - // include" branch of _MauiCollectPlatformSpecificCompileItems. (Genuine - // item-Condition gating — where the mapping carries its own Condition — is - // covered separately by + // empty TargetPlatformIdentifiers and an ActivationValue that is empty or + // normalizes to empty must still participate — locks in the "empty TPI + + // empty normalized ActivationValue = always include" branch of + // _MauiCollectPlatformSpecificCompileItems. (Genuine item-Condition gating — + // where the mapping carries its own Condition — is covered separately by // SingleProject_ConditionGatedFolderParticipatesOnlyWhenConditionIsTrue.) [Fact] public void SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeepsSharedFolder() @@ -944,6 +944,9 @@ public void SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeep .WithAttribute("TargetPlatformIdentifiers", "ios;maccatalyst")); customMappings.Add(NewElement("MauiPlatformSpecificFolder") .WithAttribute("Include", "Platforms\\Shared\\")); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Whitespace\\") + .WithAttribute("ActivationValue", " ")); project.Add(customMappings); WriteFile("Entry.cs", @" @@ -970,6 +973,14 @@ public static class SharedMarker public static string Value => ""Shared""; }"); + WriteFile("Platforms\\Whitespace\\WhitespaceMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class WhitespaceMarker +{ + public static string Value => ""Whitespace""; +}"); + AddSingleProjectTargetsImport(project); var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); @@ -983,6 +994,7 @@ public static class SharedMarker AssertExists(testDll, nonEmpty: true); AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AppleSharedMarker"); AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.SharedMarker"); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.WhitespaceMarker"); } // Genuine item-Condition gating: a MauiPlatformSpecificFolder mapping may From d99d5e6c1dd73f5d36130715ff4c2f9051d47cae Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:46:35 +0200 Subject: [PATCH 08/13] Fail closed on empty backend activation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- ...rosoft.Maui.Controls.SingleProject.targets | 12 +-- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 76 ++++++++++++++----- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index ae05f977e328..b697cf7208a0 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -47,10 +47,12 @@ "Platforms\AppleLegacy\". Inclusion contract — a folder is kept when ANY of: - (A) It declares neither a TargetPlatformIdentifiers list nor a non-empty - normalized ActivationValue. This is the legacy "always include" / - condition-gated folder (its MauiPlatformSpecificFolder item Condition - already decided participation at evaluation time). + (A) It declares neither a TargetPlatformIdentifiers list nor any authored + ActivationValue. This is the legacy "always include" / condition-gated + folder (its MauiPlatformSpecificFolder item Condition already decided + participation at evaluation time). A non-empty raw ActivationValue that + normalizes to empty fails closed instead of leaking backend files into + every target framework. (B) TargetPlatformIdentifiers is a non-empty ';'-delimited list that contains the current recognized $(TargetPlatformIdentifier). Comparison is case-insensitive and tolerates any whitespace (ASCII space, tab, CR, @@ -77,7 +79,7 @@ <_MauiPlatformSpecificCompileItems Include="$([MSBuild]::EnsureTrailingSlash('%(MauiPlatformSpecificFolder.Identity)'))**/*$(DefaultLanguageSourceExtension)" - Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> + Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index ddec93a83ae0..3318e266c2c8 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -381,7 +381,7 @@ public void HotReloadSupportForXSG(string configuration) var generatorDirectory = IOPath.Combine(tempDirectory, "Generated", "Microsoft.Maui.Controls.SourceGen", "Microsoft.Maui.Controls.SourceGen.XamlGenerator"); AssertExists(IOPath.Combine(generatorDirectory, "MainPage.xaml.sg.cs"), nonEmpty: true); AssertExists(IOPath.Combine(generatorDirectory, "MainPage.xaml.xsg.cs"), nonEmpty: true); - + var sg = File.ReadAllText(IOPath.Combine(generatorDirectory, "MainPage.xaml.sg.cs")); var xsg = File.ReadAllText(IOPath.Combine(generatorDirectory, "MainPage.xaml.xsg.cs")); if (configuration == "Debug") @@ -390,10 +390,10 @@ public void HotReloadSupportForXSG(string configuration) Assert.Contains("InitializeComponentRuntime", xsg, StringComparison.Ordinal); } else - { + { Assert.DoesNotContain("InitializeComponentRuntime", sg, StringComparison.Ordinal); Assert.DoesNotContain("InitializeComponentRuntime", xsg, StringComparison.Ordinal); - } + } } @@ -920,11 +920,11 @@ public static class AppleXMarker // Non-platform builds (TargetPlatformIdentifier empty, e.g. design-time // or netstandard TFM) must keep removing platform folders that declare a // non-empty TargetPlatformIdentifiers. An unconditioned shared folder with - // empty TargetPlatformIdentifiers and an ActivationValue that is empty or - // normalizes to empty must still participate — locks in the "empty TPI + - // empty normalized ActivationValue = always include" branch of - // _MauiCollectPlatformSpecificCompileItems. (Genuine item-Condition gating — - // where the mapping carries its own Condition — is covered separately by + // empty TargetPlatformIdentifiers and no authored ActivationValue must still + // participate — locks in the legacy "empty TPI + empty ActivationValue = + // always include" branch of _MauiCollectPlatformSpecificCompileItems. + // (Genuine item-Condition gating — where the mapping carries its own Condition — + // is covered separately by // SingleProject_ConditionGatedFolderParticipatesOnlyWhenConditionIsTrue.) [Fact] public void SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeepsSharedFolder() @@ -944,9 +944,6 @@ public void SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeep .WithAttribute("TargetPlatformIdentifiers", "ios;maccatalyst")); customMappings.Add(NewElement("MauiPlatformSpecificFolder") .WithAttribute("Include", "Platforms\\Shared\\")); - customMappings.Add(NewElement("MauiPlatformSpecificFolder") - .WithAttribute("Include", "Platforms\\Whitespace\\") - .WithAttribute("ActivationValue", " ")); project.Add(customMappings); WriteFile("Entry.cs", @" @@ -973,6 +970,52 @@ public static class SharedMarker public static string Value => ""Shared""; }"); + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + // No -p:_SingleProjectTestTargetPlatformIdentifier — simulates the + // non-platform TFM / design-time evaluation scenario. + Build(projectFile); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AppleSharedMarker"); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.SharedMarker"); + } + + // Authored activation metadata that normalizes to empty is invalid backend + // configuration, not a legacy shared folder. Fail closed on neutral and + // recognized TFMs so backend-only sources cannot leak into every inner build. + [Theory] + [InlineData("")] + [InlineData("ios")] + public void SingleProject_WhitespaceActivationValueIsExcluded(string targetPlatformIdentifier) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Whitespace\\") + .WithAttribute("ActivationValue", " ")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + WriteFile("Platforms\\Whitespace\\WhitespaceMarker.cs", @" namespace Microsoft.Maui.Controls.Xaml.UnitTests; @@ -986,15 +1029,14 @@ public static class WhitespaceMarker var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); project.Save(projectFile); - // No -p:_SingleProjectTestTargetPlatformIdentifier — simulates the - // non-platform TFM / design-time evaluation scenario. - Build(projectFile); + var args = string.IsNullOrEmpty(targetPlatformIdentifier) + ? "" + : $"-p:_SingleProjectTestTargetPlatformIdentifier={targetPlatformIdentifier}"; + Build(projectFile, additionalArgs: args); var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); AssertExists(testDll, nonEmpty: true); - AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.AppleSharedMarker"); - AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.SharedMarker"); - AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.WhitespaceMarker"); + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.WhitespaceMarker"); } // Genuine item-Condition gating: a MauiPlatformSpecificFolder mapping may From 7ab5012a5f5bd9b27f25a7fcbf22e70311b6e81f Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:36:40 +0200 Subject: [PATCH 09/13] Preserve active platform Compile item order Skip remove/re-add metadata updates for built-in platform items that are already active, while retaining shared backend metadata correction. Add a regression test for Compile ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- ...rosoft.Maui.Controls.SingleProject.targets | 9 +- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 87 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index b697cf7208a0..dff959e6983b 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -113,7 +113,14 @@ Remove="@(_MauiPlatformSpecificCompileItems)" MatchOnMetadata="FullPath" MatchOnMetadataOptions="PathLike" /> - <_MauiCompileItemsToUnflip Include="@(Compile)" /> + + <_MauiCompileItemsToUnflip + Include="@(Compile)" + Condition=" '%(Compile.ExcludeFromCurrentConfiguration)' == 'true' " /> <_MauiCompileItemsToUnflip Remove="@(_MauiCompileItemsNotToUnflip)" MatchOnMetadata="FullPath" diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index 3318e266c2c8..6d239ab66d86 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -1214,6 +1214,93 @@ public static class SharedMarker "_MauiUnflipKeptCompileItemMetadata must preserve unrelated Compile metadata. Build log:\n" + log); } + [Fact] + public void SingleProject_UnflipKeptCompileItemMetadata_PreservesAlreadyActiveCompileItemOrder() + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + propertyGroup.Add(NewElement("EnableDefaultCompileItems").WithValue("false")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var compileItems = NewElement("ItemGroup"); + compileItems.Add(NewElement("Compile").WithAttribute("Include", "Before.cs")); + var activePlatformCompile = NewElement("Compile").WithAttribute("Include", "Platforms\\Windows\\WindowsMarker.cs"); + activePlatformCompile.Add(NewElement("ExcludeFromCurrentConfiguration").WithValue("false")); + compileItems.Add(activePlatformCompile); + compileItems.Add(NewElement("Compile").WithAttribute("Include", "After.cs")); + project.Add(compileItems); + + WriteFile("Before.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Before +{ + public static string Value => ""Before""; +}"); + + WriteFile("Platforms\\Windows\\WindowsMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class WindowsMarker +{ + public static string Value => ""Windows""; +}"); + + WriteFile("After.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class After +{ + public static string Value => ""After""; +}"); + + AddSingleProjectTargetsImport(project); + + // The synthetic TPI is assigned during target execution, after the + // evaluation-time built-in folder metadata flips have already run. + // Model the resulting active-platform metadata before the shipping + // unflip target so this test covers its ordering behavior. + var markActivePlatformItemTarget = NewElement("Target") + .WithAttribute("Name", "_TestMarkActivePlatformCompile") + .WithAttribute("AfterTargets", "_MauiRemovePlatformCompileItems") + .WithAttribute("BeforeTargets", "_MauiUnflipKeptCompileItemMetadata"); + var markActivePlatformItemGroup = NewElement("ItemGroup"); + var markActivePlatformCompile = NewElement("Compile") + .WithAttribute("Update", "Platforms\\Windows\\WindowsMarker.cs"); + markActivePlatformCompile.Add(NewElement("ExcludeFromCurrentConfiguration").WithValue("false")); + markActivePlatformItemGroup.Add(markActivePlatformCompile); + markActivePlatformItemTarget.Add(markActivePlatformItemGroup); + project.Add(markActivePlatformItemTarget); + + var dumpTarget = NewElement("Target") + .WithAttribute("Name", "_TestDumpCompileOrder") + .WithAttribute("AfterTargets", "_MauiUnflipKeptCompileItemMetadata"); + dumpTarget.Add(NewElement("Message") + .WithAttribute("Importance", "high") + .WithAttribute("Text", "COMPILE_ORDER: @(Compile->'%(Filename)', '|')")); + project.Add(dumpTarget); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + var log = Build(projectFile, additionalArgs: "-p:_SingleProjectTestTargetPlatformIdentifier=windows"); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.Before"); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.WindowsMarker"); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.After"); + Assert.Contains( + "COMPILE_ORDER: Before|WindowsMarker|After", + log, + StringComparison.OrdinalIgnoreCase); + } + // Backward compatibility: a folder that declares only the legacy singular // TargetPlatformIdentifier metadata must continue to match exactly that TPI. [Theory] From 83ab69d62ecb72264f0237cecda8a03a0197efe1 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:40:12 +0200 Subject: [PATCH 10/13] Fail closed on unresolved backend activation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- ...rosoft.Maui.Controls.SingleProject.targets | 14 ++--- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 55 +++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index dff959e6983b..11f34067f9e4 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -47,12 +47,12 @@ "Platforms\AppleLegacy\". Inclusion contract — a folder is kept when ANY of: - (A) It declares neither a TargetPlatformIdentifiers list nor any authored - ActivationValue. This is the legacy "always include" / condition-gated - folder (its MauiPlatformSpecificFolder item Condition already decided - participation at evaluation time). A non-empty raw ActivationValue that - normalizes to empty fails closed instead of leaking backend files into - every target framework. + (A) It declares neither a TargetPlatformIdentifiers list nor any backend + identity/activation metadata. This is the legacy "always include" / + condition-gated folder (its MauiPlatformSpecificFolder item Condition + already decided participation at evaluation time). Malformed backend + metadata that resolves to an empty ActivationValue fails closed instead + of leaking backend files into every target framework. (B) TargetPlatformIdentifiers is a non-empty ';'-delimited list that contains the current recognized $(TargetPlatformIdentifier). Comparison is case-insensitive and tolerates any whitespace (ASCII space, tab, CR, @@ -79,7 +79,7 @@ <_MauiPlatformSpecificCompileItems Include="$([MSBuild]::EnsureTrailingSlash('%(MauiPlatformSpecificFolder.Identity)'))**/*$(DefaultLanguageSourceExtension)" - Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> + Condition=" ('%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' == '' and '%(MauiPlatformSpecificFolder.BackendIdentity)' == '' and '%(MauiPlatformSpecificFolder.ActivationProperty)' == '') or ('$(TargetPlatformIdentifier)' != '' and $([System.Text.RegularExpressions.Regex]::Replace(';%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers);', '\s+', '').ToLowerInvariant().Contains(';$(TargetPlatformIdentifier.ToLowerInvariant());'))) or ('$(TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)' != '' and '%(MauiPlatformSpecificFolder._MauiResolvedActivationValueNormalized)' == '%(MauiPlatformSpecificFolder._MauiActivationValueNormalized)') " /> diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index 6d239ab66d86..6a77e3d0a8c4 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -1039,6 +1039,61 @@ public static class WhitespaceMarker AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.WhitespaceMarker"); } + // An authored activation property whose value expands to empty is malformed + // backend metadata, not a legacy shared-folder mapping. Fail closed on both + // neutral and recognized TFMs instead of leaking its sources into every build. + [Theory] + [InlineData("")] + [InlineData("ios")] + public void SingleProject_UnresolvedActivationValueWithBackendMetadataIsExcluded(string targetPlatformIdentifier) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\Unresolved\\") + .WithAttribute("ActivationProperty", "MyBackendSwitch") + .WithAttribute("ActivationValue", "$(SomeUnsetProperty)")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\Unresolved\\UnresolvedMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class UnresolvedMarker +{ + public static string Value => ""Unresolved""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + var args = string.IsNullOrEmpty(targetPlatformIdentifier) + ? "" + : $"-p:_SingleProjectTestTargetPlatformIdentifier={targetPlatformIdentifier}"; + Build(projectFile, additionalArgs: args); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeDoesNotExist(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.UnresolvedMarker"); + } + // Genuine item-Condition gating: a MauiPlatformSpecificFolder mapping may // carry its own MSBuild item Condition that decides participation at // evaluation time. When the Condition is true the item materializes and — From dc4b4015966b699aaaa2ae884d5e7627edfc67a6 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:54:40 +0200 Subject: [PATCH 11/13] Document bare backend activation values Clarify that an unset bare ActivationValue is indistinguishable from a legacy shared mapping after MSBuild evaluation, and add neutral/iOS coverage for that compatibility boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- ...rosoft.Maui.Controls.SingleProject.targets | 7 ++- .../Xaml.UnitTests/MSBuild/MSBuildTests.cs | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index 11f34067f9e4..86d38eef26d6 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -51,8 +51,11 @@ identity/activation metadata. This is the legacy "always include" / condition-gated folder (its MauiPlatformSpecificFolder item Condition already decided participation at evaluation time). Malformed backend - metadata that resolves to an empty ActivationValue fails closed instead - of leaking backend files into every target framework. + metadata that still carries ActivationProperty or BackendIdentity fails + closed instead of leaking backend files into every target framework. + A bare ActivationValue="$(UnsetProperty)" expands to empty during project + evaluation and is therefore indistinguishable from a legacy shared folder; + conditional backends must also declare ActivationProperty or BackendIdentity. (B) TargetPlatformIdentifiers is a non-empty ';'-delimited list that contains the current recognized $(TargetPlatformIdentifier). Comparison is case-insensitive and tolerates any whitespace (ASCII space, tab, CR, diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index 6a77e3d0a8c4..4fea0dd04bc5 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -985,6 +985,62 @@ public static class SharedMarker AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.SharedMarker"); } + // A bare ActivationValue that expands to empty during project evaluation is + // indistinguishable from a legacy shared-folder mapping by the time the + // normalization target runs. Preserve that compatibility behavior on neutral + // and recognized TFMs; conditional backends must also declare an + // ActivationProperty or BackendIdentity so malformed metadata fails closed. + [Theory] + [InlineData("")] + [InlineData("ios")] + public void SingleProject_BareUnresolvedActivationValueUsesLegacySharedFolderBehavior(string targetPlatformIdentifier) + { + SetUp(); + var project = NewElement("Project").WithAttribute("Sdk", "Microsoft.NET.Sdk"); + var propertyGroup = NewElement("PropertyGroup"); + propertyGroup.Add(NewElement("TargetFramework").WithValue(GetTfm())); + propertyGroup.Add(NewElement("SingleProject").WithValue("true")); + project.Add(propertyGroup); + AddMauiReferences(project); + AddSingleProjectBeforeTargetsImport(project); + + var customMappings = NewElement("ItemGroup"); + customMappings.Add(NewElement("MauiPlatformSpecificFolder") + .WithAttribute("Include", "Platforms\\BareUnresolved\\") + .WithAttribute("ActivationValue", "$(SomeUnsetProperty)")); + project.Add(customMappings); + + WriteFile("Entry.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class Entry +{ + public static string Value => ""ok""; +}"); + + WriteFile("Platforms\\BareUnresolved\\BareUnresolvedMarker.cs", @" +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public static class BareUnresolvedMarker +{ + public static string Value => ""BareUnresolved""; +}"); + + AddSingleProjectTargetsImport(project); + + var projectFile = IOPath.Combine(tempDirectory, "test.csproj"); + project.Save(projectFile); + + var args = string.IsNullOrEmpty(targetPlatformIdentifier) + ? "" + : $"-p:_SingleProjectTestTargetPlatformIdentifier={targetPlatformIdentifier}"; + Build(projectFile, additionalArgs: args); + + var testDll = IOPath.Combine(intermediateDirectory, "test.dll"); + AssertExists(testDll, nonEmpty: true); + AssertTypeExists(testDll, "Microsoft.Maui.Controls.Xaml.UnitTests.BareUnresolvedMarker"); + } + // Authored activation metadata that normalizes to empty is invalid backend // configuration, not a legacy shared folder. Fail closed on neutral and // recognized TFMs so backend-only sources cannot leak into every inner build. From e354686b4ecad9f9235eabedcdf83447899e07e5 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:06:21 +0200 Subject: [PATCH 12/13] Remove fake region from activation tests Keep the SingleProject activation test overview as a normal explanatory comment per repository conventions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs index 4fea0dd04bc5..2a6551e1203e 100644 --- a/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs +++ b/src/Controls/tests/Xaml.UnitTests/MSBuild/MSBuildTests.cs @@ -779,8 +779,6 @@ public void SingleProject_DefaultEntitlementsUsedWhenNoCustomSet() "Default Entitlements.plist should be used when no custom CodesignEntitlements is set"); } - // --- SingleProject platform-folder registration & activation --------------- - // // These tests exercise the data-driven MauiPlatformSpecificFolder compile // selection contract: recognized TargetPlatformIdentifier(s) matching, shared // folders, backward-compatible singular metadata, condition-gated folders, and From c73b7025c187e76700973bb5cf8a3cc0869a8bb4 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:21:59 +0200 Subject: [PATCH 13/13] Clarify SingleProject removal batching Describe the true and false ExcludeFromCurrentConfiguration batches accurately so the removal guard is not simplified incorrectly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66f84348-6476-4097-8b7f-f240338e85c3 --- .../Microsoft.Maui.Controls.SingleProject.targets | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets index 86d38eef26d6..4cb5e4d95105 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.SingleProject.targets @@ -157,12 +157,11 @@ The Condition references %(Compile.ExcludeFromCurrentConfiguration) from inside an Include of a *different* item type — this is MSBuild cross-item-type batching: the filesystem glob is evaluated once per - unique value of Compile.ExcludeFromCurrentConfiguration. In practice - the blanket at the top of this file always marks - every $(PlatformsProjectFolder)/** file as ExcludeFromCurrentConfiguration=true, - so this batches into a single iteration; do NOT "simplify" away the - Condition without first re-verifying that contract still holds, or - files outside the active TPI may leak into the build. + unique value of Compile.ExcludeFromCurrentConfiguration. The blanket + initially marks every $(PlatformsProjectFolder)/** + file true, then active-platform updates flip matching items back to + false. Keep the Condition so only the true batch is removed; otherwise + active-platform files may be removed or inactive files may leak in. --> <_MauiPlatformCompileToRemove Condition=" '%(Compile.ExcludeFromCurrentConfiguration)' == 'true' "