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..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 @@ -35,6 +35,109 @@ + + + + <_MauiPlatformSpecificCompileItems + Include="$([MSBuild]::EnsureTrailingSlash('%(MauiPlatformSpecificFolder.Identity)'))**/*$(DefaultLanguageSourceExtension)" + 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)') " /> + + + + + + + + <_MauiCompileItemsNotToUnflip Include="@(Compile)" /> + <_MauiCompileItemsNotToUnflip + Remove="@(_MauiPlatformSpecificCompileItems)" + MatchOnMetadata="FullPath" + MatchOnMetadataOptions="PathLike" /> + + <_MauiCompileItemsToUnflip + Include="@(Compile)" + Condition=" '%(Compile.ExcludeFromCurrentConfiguration)' == 'true' " /> + <_MauiCompileItemsToUnflip + Remove="@(_MauiCompileItemsNotToUnflip)" + MatchOnMetadata="FullPath" + MatchOnMetadataOptions="PathLike" /> + + + false + + + + - - 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' " - 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 fa82c0d92690..2a6551e1203e 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() { @@ -316,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") @@ -325,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); - } + } } @@ -713,5 +778,923 @@ 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"); } + + // 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. An unconditioned shared folder with + // 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() + { + 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"); + } + + // 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. + [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; + +public static class WhitespaceMarker +{ + public static string Value => ""Whitespace""; +}"); + + 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.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 — + // 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)] + [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"); + 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); + + // 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); + // 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); + + 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; + +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); + Assert.True( + log.Contains("Entry.cs|ExcludeFromCurrentConfiguration=true", StringComparison.OrdinalIgnoreCase), + "_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] + [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"); + } + + // 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] + [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"); + } } }