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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,124 @@
<TizenProjectFolder>$([MSBuild]::EnsureTrailingSlash('$(TizenProjectFolder)'))</TizenProjectFolder>
</PropertyGroup>

<!--
Built-in folder mappings declare only the legacy singular TargetPlatformIdentifier
metadata so that _MauiNormalizePlatformSpecificFolders is the single source of
truth for back-filling the plural TargetPlatformIdentifiers contract. Keeping
one source avoids drift between the two metadata fields and ensures the
normalization code path is exercised on every SingleProject build (so a
regression in the back-fill is caught immediately).

BackendIdentity records the stable, MAUI-owned key for each built-in backend so
the built-in platforms describe themselves through exactly the same registration
metadata that an external backend NuGet contributes. For recognized platform TFMs
activation still flows through TargetPlatformIdentifier(s); BackendIdentity here is
purely descriptive (built-ins do not declare an ActivationValue, so the neutral-TFM
activation branch never fires for them).
-->
<ItemGroup>
<MauiPlatformSpecificFolder Include="$(AndroidProjectFolder)" TargetPlatformIdentifier="android" />
<MauiPlatformSpecificFolder Include="$(iOSProjectFolder)" TargetPlatformIdentifier="ios" />
<MauiPlatformSpecificFolder Include="$(MacCatalystProjectFolder)" TargetPlatformIdentifier="maccatalyst" />
<MauiPlatformSpecificFolder Include="$(WindowsProjectFolder)" TargetPlatformIdentifier="windows" />
<MauiPlatformSpecificFolder Include="$(TizenProjectFolder)" TargetPlatformIdentifier="tizen" />
<MauiPlatformSpecificFolder Include="$(AndroidProjectFolder)" TargetPlatformIdentifier="android" BackendIdentity="android" />
<MauiPlatformSpecificFolder Include="$(iOSProjectFolder)" TargetPlatformIdentifier="ios" BackendIdentity="ios" />
<MauiPlatformSpecificFolder Include="$(MacCatalystProjectFolder)" TargetPlatformIdentifier="maccatalyst" BackendIdentity="maccatalyst" />
<MauiPlatformSpecificFolder Include="$(WindowsProjectFolder)" TargetPlatformIdentifier="windows" BackendIdentity="windows" />
<MauiPlatformSpecificFolder Include="$(TizenProjectFolder)" TargetPlatformIdentifier="tizen" BackendIdentity="tizen" />
</ItemGroup>

<!--
Normalize MauiPlatformSpecificFolder items so downstream targets and any
third-party extension can rely on a uniform shape:
- TargetPlatformIdentifiers is back-filled from the legacy singular
TargetPlatformIdentifier when only the singular is set.
- ActivationValue is back-filled from BackendIdentity so a backend that
only declares a stable identity key (BackendIdentity="gtk") can be
activated for a neutral TFM via <MauiActiveBackend>gtk</MauiActiveBackend>
without having to repeat the value. Built-in platforms declare a
BackendIdentity but no ActivationValue is derived for them here because
they are TPI-recognized (see the guard below), so they never take the
neutral-activation branch.
- ActivationProperty defaults to the well-known MauiActiveBackend selector
whenever an ActivationValue is present but no explicit property was named.
- _MauiResolvedActivationValue captures the *current value* of the property
named by ActivationProperty. This is the neutral-TFM (non-TPI) activation
signal consumed by _MauiCollectPlatformSpecificCompileItems.
Identity (folder path) is intentionally not rewritten here because MSBuild
item-Identity rewriting requires a remove/re-add dance that loses caller
metadata. Instead, downstream consumers apply EnsureTrailingSlash() at the
use site (see _MauiCollectPlatformSpecificCompileItems).
-->
<!--
BeforeTargets is intentionally limited to _MauiRemovePlatformCompileItems
because _MauiCollectPlatformSpecificCompileItems already declares
DependsOnTargets="_MauiNormalizePlatformSpecificFolders" (single source of
truth for that ordering). The Remove target does not depend on Collect (so
Normalize-Before-Remove is the only way to guarantee Remove sees the
back-filled TargetPlatformIdentifiers metadata when Collect is short-circuited).
-->
<Target Name="_MauiNormalizePlatformSpecificFolders"
BeforeTargets="_MauiRemovePlatformCompileItems"
Condition=" '$(SingleProject)' == 'true' ">
<ItemGroup>
<MauiPlatformSpecificFolder
Update="@(MauiPlatformSpecificFolder)"
Condition=" '%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' and '%(MauiPlatformSpecificFolder.TargetPlatformIdentifier)' != '' ">
<TargetPlatformIdentifiers>%(MauiPlatformSpecificFolder.TargetPlatformIdentifier)</TargetPlatformIdentifiers>
</MauiPlatformSpecificFolder>

<!--
Back-fill ActivationValue from BackendIdentity ONLY for folders that are not
already TPI-recognized. Built-in platforms carry both a TargetPlatformIdentifier
and a BackendIdentity; deriving an ActivationValue for them would let a stray
MauiActiveBackend value pull an unrelated built-in folder into a neutral build.
Gating on empty TargetPlatformIdentifier(s) keeps the neutral-activation branch
exclusive to backends that actually rely on it.
-->
<MauiPlatformSpecificFolder
Update="@(MauiPlatformSpecificFolder)"
Condition=" '%(MauiPlatformSpecificFolder.ActivationValue)' == '' and '%(MauiPlatformSpecificFolder.BackendIdentity)' != '' and '%(MauiPlatformSpecificFolder.TargetPlatformIdentifier)' == '' and '%(MauiPlatformSpecificFolder.TargetPlatformIdentifiers)' == '' ">
<ActivationValue>%(MauiPlatformSpecificFolder.BackendIdentity)</ActivationValue>
</MauiPlatformSpecificFolder>

<!-- MauiActiveBackend is the well-known neutral-TFM selector; default to it when unset. -->
<MauiPlatformSpecificFolder
Update="@(MauiPlatformSpecificFolder)"
Condition=" '%(MauiPlatformSpecificFolder.ActivationProperty)' == '' and '%(MauiPlatformSpecificFolder.ActivationValue)' != '' ">
<ActivationProperty>MauiActiveBackend</ActivationProperty>
</MauiPlatformSpecificFolder>

<!--
Resolve the *value* of the property named by ActivationProperty into a
dedicated metadata field. The '$(%(...))' form is MSBuild dynamic property
dereference: '%(...ActivationProperty)' expands to the property name first
(e.g. MauiActiveBackend), then '$(...)' expands that property's value. This
supports both the default MauiActiveBackend selector and a custom
ActivationProperty a backend NuGet may prefer. An unset property resolves
to empty and simply fails to activate, so a non-selected backend is excluded.
-->
<MauiPlatformSpecificFolder
Update="@(MauiPlatformSpecificFolder)"
Condition=" '%(MauiPlatformSpecificFolder.ActivationProperty)' != '' ">
<_MauiResolvedActivationValue>$(%(MauiPlatformSpecificFolder.ActivationProperty))</_MauiResolvedActivationValue>
</MauiPlatformSpecificFolder>

<!--
Pre-compute whitespace-stripped, lower-cased forms of the wanted
ActivationValue and the resolved property value so the activation branch of
_MauiCollectPlatformSpecificCompileItems is a plain metadata equality check.
Doing the Regex.Replace/ToLowerInvariant normalization here (as metadata
values) rather than inline in the collect Condition keeps that Condition
parseable — nesting property functions that reference metadata on both sides
of an '==' inside a single item Condition trips MSBuild's condition parser
(MSB4092). Whitespace tolerance and case-insensitivity mirror the TPI match.
-->
<MauiPlatformSpecificFolder
Update="@(MauiPlatformSpecificFolder)"
Condition=" '%(MauiPlatformSpecificFolder.ActivationValue)' != '' ">
<_MauiActivationValueNormalized>$([System.Text.RegularExpressions.Regex]::Replace('%(MauiPlatformSpecificFolder.ActivationValue)', '\s+', '').ToLowerInvariant())</_MauiActivationValueNormalized>
<_MauiResolvedActivationValueNormalized>$([System.Text.RegularExpressions.Regex]::Replace('%(MauiPlatformSpecificFolder._MauiResolvedActivationValue)', '\s+', '').ToLowerInvariant())</_MauiResolvedActivationValueNormalized>
</MauiPlatformSpecificFolder>
</ItemGroup>
</Target>

<PropertyGroup Condition=" '$(SingleProject)' == 'true' and '$([MSBuild]::GetTargetPlatformIdentifier($(TargetFramework)))' == 'android' ">
<AndroidManifest Condition=" Exists('$(AndroidProjectFolder)AndroidManifest.xml') ">$(AndroidProjectFolder)AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>$(AndroidProjectFolder)Resources</MonoAndroidResourcePrefix>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,109 @@
</Compile>
</ItemGroup>

<!--
Build the per-platform allow-list of Compile items that should survive the
blanket Platforms/** removal performed by _MauiRemovePlatformCompileItems.

Identity normalization: EnsureTrailingSlash() guards against the
foot-gun where a user-supplied item like
<MauiPlatformSpecificFolder Include="Platforms\Apple" .../>
(no trailing separator) would expand to the glob "Platforms\Apple**/*.cs"
and silently match sibling folders such as "Platforms\AppleX\" or
"Platforms\AppleLegacy\".

Inclusion contract — a folder is kept when ANY of:
(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 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.
Comment thread
kubaflo marked this conversation as resolved.
(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,
LF) around each entry; the Regex.Replace(\s+, '') strip is deliberate,
not Replace(' ', ''), so values formatted across multiple lines or
copy-pasted with tabs continue to match. The '$(TargetPlatformIdentifier)'
!= '' guard prevents non-empty lists from accidentally matching during
non-platform (e.g. netstandard / design-time) builds where
$(TargetPlatformIdentifier) is empty.
(C) Neutral-TFM activation: the folder declares an ActivationValue and the
value of its ActivationProperty (resolved+normalized during normalization
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 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.
-->
<Target Name="_MauiCollectPlatformSpecificCompileItems"
DependsOnTargets="_MauiNormalizePlatformSpecificFolders"
BeforeTargets="_MauiRemovePlatformCompileItems"
Condition=" '$(SingleProject)' == 'true' and '@(MauiPlatformSpecificFolder)' != '' ">
<ItemGroup>
<_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)') " />
Comment thread
kubaflo marked this conversation as resolved.
</ItemGroup>
</Target>

<!--
Flip ExcludeFromCurrentConfiguration back to false on the kept items so
Visual Studio's Solution Explorer / IntelliSense renders them as part of
the active configuration. The blanket <Compile Update> at the top of this
file unconditionally marks every $(PlatformsProjectFolder)/** file as
ExcludeFromCurrentConfiguration=true; the per-TPI flips at the top only
cover the built-in $(<TPI>ProjectFolder) paths. Without this target,
files in shared folders like Platforms/Apple/ (and neutral-activated
backend folders like Platforms/Gtk/) compile correctly but appear
grayed-out in the IDE.

This MUST run AFTER _MauiRemovePlatformCompileItems because that target
relies on cross-item-type batching of %(Compile.ExcludeFromCurrentConfiguration);
flipping kept items to false beforehand would remove them from the 'true'
batch and prevent the to-remove list from being computed correctly.
-->
<Target Name="_MauiUnflipKeptCompileItemMetadata"
Comment thread
kubaflo marked this conversation as resolved.
AfterTargets="_MauiRemovePlatformCompileItems"
Condition=" '$(SingleProject)' == 'true' and '@(_MauiPlatformSpecificCompileItems)' != '' ">
<ItemGroup>
<!--
Dynamic Compile Update item specs apply the metadata to unrelated Compile
items. Intersect the current Compile list with the kept platform items,
then replace only those matches while preserving their existing metadata.
-->
<_MauiCompileItemsNotToUnflip Include="@(Compile)" />
<_MauiCompileItemsNotToUnflip
Remove="@(_MauiPlatformSpecificCompileItems)"
MatchOnMetadata="FullPath"
MatchOnMetadataOptions="PathLike" />
<!--
Built-in active-platform items were already flipped to false during
evaluation. Skip them here so the remove/re-add below does not perturb
Compile ordering and generated WinRT metadata.
-->
<_MauiCompileItemsToUnflip
Include="@(Compile)"
Condition=" '%(Compile.ExcludeFromCurrentConfiguration)' == 'true' " />
<_MauiCompileItemsToUnflip
Remove="@(_MauiCompileItemsNotToUnflip)"
MatchOnMetadata="FullPath"
MatchOnMetadataOptions="PathLike" />
<Compile
Remove="@(_MauiCompileItemsToUnflip)"
MatchOnMetadata="FullPath"
MatchOnMetadataOptions="PathLike" />
<Compile Include="@(_MauiCompileItemsToUnflip)">
<ExcludeFromCurrentConfiguration>false</ExcludeFromCurrentConfiguration>
</Compile>
</ItemGroup>
</Target>

<!--
Run before both _MauiInjectXamlCssAdditionalFiles and GenerateMSBuildEditorConfigFileShouldRun because
if for some reason the _MauiInjectXamlCssAdditionalFiles target is not run, we still get in at the
Expand All @@ -46,10 +149,25 @@

<!-- Removals -->
<ItemGroup>
<!-- Remove everything that isn't part of this platform -->
<Compile
<!--
Remove every Compile item under $(PlatformsProjectFolder) that is not
part of the current build configuration AND has not been explicitly
kept by _MauiCollectPlatformSpecificCompileItems above.

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. The blanket
<Compile Update> 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)" />
<Compile Remove="@(_MauiPlatformCompileToRemove)" />

<!-- Remove all Windows (WinUI) XAML Files from the Windows folder -->
<_MauiXamlToRemove
Expand Down
Loading
Loading