Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ public LockFile CreateLockFile(LockFile previousLockFile,
&& (target.TargetFramework is FallbackFramework
|| target.TargetFramework is AssetTargetFallbackFramework);

bool checkMonoAndroidDeprecation = MonoAndroidDeprecation.ShouldCheck(project, targetGraph.Framework);

foreach (var graphItem in targetGraph.Flattened.OrderBy(x => x.Key))
{
var library = graphItem.Key;
Expand Down Expand Up @@ -226,7 +228,7 @@ public LockFile CreateLockFile(LockFile previousLockFile,
var package = packageInfo.Package;
var libraryDependency = tfi.Dependencies.FirstOrDefault(e => e.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase));

(LockFileTargetLibrary targetLibrary, bool usedFallbackFramework) = LockFileUtils.CreateLockFileTargetLibrary(
(LockFileTargetLibrary targetLibrary, bool usedFallbackFramework, NuGetFramework compileAssetFramework, NuGetFramework runtimeAssetFramework) = LockFileUtils.CreateLockFileTargetLibrary(
libraryDependency?.Aliases,
libraries[ValueTuple.Create(library.Name, library.Version)],
package,
Expand Down Expand Up @@ -280,6 +282,29 @@ public LockFile CreateLockFile(LockFile previousLockFile,
librariesWithWarnings.Add(library);
}
}

// Log NU1703 warning if the package uses the deprecated MonoAndroid framework
if (checkMonoAndroidDeprecation
&& !librariesWithWarnings.Contains(library)
&& (MonoAndroidDeprecation.IsMonoAndroidFramework(compileAssetFramework)
|| MonoAndroidDeprecation.IsMonoAndroidFramework(runtimeAssetFramework)))
{
var message = string.Format(CultureInfo.CurrentCulture,
Strings.Warning_MonoAndroidFrameworkDeprecated,
library.Name,
library.Version);

var logMessage = RestoreLogMessage.CreateWarning(
NuGetLogCode.NU1703,
message,
library.Name,
targetGraph.TargetGraphName);

_logger.Log(logMessage);

// only log the warning once per library
librariesWithWarnings.Add(library);
Comment thread
sbomer marked this conversation as resolved.
Outdated
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class LockFileBuilderCache
private readonly ConcurrentDictionary<CriteriaKey, List<(List<SelectionCriteria>, bool)>> _criteriaSets =
new();

private readonly ConcurrentDictionary<(CriteriaKey, string path, string aliases, LibraryIncludeFlags, int dependencyCount), Lazy<(LockFileTargetLibrary, bool)>> _lockFileTargetLibraryCache =
private readonly ConcurrentDictionary<(CriteriaKey, string path, string aliases, LibraryIncludeFlags, int dependencyCount), Lazy<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)>> _lockFileTargetLibraryCache =
new();

/// <summary>
Expand Down Expand Up @@ -106,7 +106,7 @@ public ContentItemCollection GetContentItems(LockFileLibrary library, LocalPacka
/// <summary>
/// Try to get a LockFileTargetLibrary from the cache.
/// </summary>
internal (LockFileTargetLibrary, bool) GetLockFileTargetLibrary(RestoreTargetGraph graph, NuGetFramework framework, LocalPackageInfo localPackageInfo, string aliases, LibraryIncludeFlags libraryIncludeFlags, List<LibraryDependency> dependencies, Func<(LockFileTargetLibrary, bool)> valueFactory)
internal (LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework) GetLockFileTargetLibrary(RestoreTargetGraph graph, NuGetFramework framework, LocalPackageInfo localPackageInfo, string aliases, LibraryIncludeFlags libraryIncludeFlags, List<LibraryDependency> dependencies, Func<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)> valueFactory)
{
// Comparing RuntimeGraph for equality is very expensive,
// so in case of a request where the RuntimeGraph is not empty we avoid using the cache.
Expand All @@ -117,7 +117,7 @@ public ContentItemCollection GetContentItems(LockFileLibrary library, LocalPacka
var criteriaKey = new CriteriaKey(graph.TargetGraphName, framework);
var packagePath = localPackageInfo.ExpandedPath;
return _lockFileTargetLibraryCache.GetOrAdd((criteriaKey, packagePath, aliases, libraryIncludeFlags, dependencies.Count),
key => new Lazy<(LockFileTargetLibrary, bool)>(valueFactory)).Value;
key => new Lazy<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)>(valueFactory)).Value;
}

private class CriteriaKey : IEquatable<CriteriaKey>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using NuGet.Frameworks;
using NuGet.ProjectModel;

namespace NuGet.Commands
{
/// <summary>
/// Detects when a package uses the deprecated MonoAndroid framework instead of net6.0-android or later.
/// This warning is gated on .NET 11 SDK (SdkAnalysisLevel >= 11.0.100) and targeting net11.0-android or later.
/// </summary>
internal static class MonoAndroidDeprecation
{
/// <summary>
/// Determines whether the MonoAndroid deprecation check should be performed for the given project and target framework.
/// </summary>
/// <param name="project">The package spec containing restore metadata.</param>
/// <param name="framework">The target framework of the current graph.</param>
/// <returns>True if the deprecation check should be performed.</returns>
internal static bool ShouldCheck(PackageSpec project, NuGetFramework framework)
{
if (project.RestoreMetadata == null)
{
return false;
}

// Gate on SDK analysis level >= 11.0.100
if (!SdkAnalysisLevelMinimums.IsEnabled(
project.RestoreMetadata.SdkAnalysisLevel,
project.RestoreMetadata.UsingMicrosoftNETSdk,
SdkAnalysisLevelMinimums.V11_0_100))
{
return false;
}

// Only check for .NETCoreApp frameworks targeting android with version >= 11.0
return StringComparer.OrdinalIgnoreCase.Equals(framework.Framework, FrameworkConstants.FrameworkIdentifiers.NetCoreApp)
&& framework.Version.Major >= 11
&& framework.HasPlatform
&& framework.Platform.Equals("android", StringComparison.OrdinalIgnoreCase);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm wondering if NuGet is the right place to check for this. Can this logic be moved to the Android workload instead? It's probably easiest to do in NuGet, but this is code that will run for every restore, even when the Android workload isn't installed.

When I take a binlog of a normal build, the .NET SDK reads NuGet's assets file in the ResolvePackageAssets task. The RuntimeCopyLocalItems and ResolvedCompileFileDefinitions items have PathInPackage metadata, which can be searched for monoandroid, for example a regex something like [lib|ref]/monoandroid*. Checking the runtime assets will be tricker to validate the subdirectory that corresponds to the TFM.

It's not as precise as checking the NuGetFramework for the selected asset, but the question is if it's good enough.

Another design idea is to change the assets file to emit a "framework" metadata for every asset, and change the SDK's ResolvePackageAssets to copy that metadata into the MSBuild item, which can then be checked by the android workload's msbuild targets. However, I don't like this idea as the increased assets file size and additional metadata on every item will cause additional memory allocations and therefore more GC and will probably have worse perf impact than the small benefit in moving the check from NuGet to the android workload.

Given restore almost never breaks backwards compatibility, I just want to make sure that NuGet isn't stuck with this "forever" when it's fundamentally a non-NuGet concern. So, trying to do due diligence in checking if the owning component can feasibly implement the check, rather than NuGet.

Copy link
Copy Markdown
Author

@sbomer sbomer Apr 8, 2026

Choose a reason for hiding this comment

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

I'm not familiar enough with the design of the workloads to know whether it would make sense to handle it there. @jonathanpeppers what do you think?

My instinct is that this should warn during restore, not during build, which is why I thought it made the most sense to handle it in NuGet. Plus there was precedent in the (now removed) Xamarin.iOS warning.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The design says to emit this warning here in NuGet (but note the code was NU1701 back then):

I'm not aware of us running any MSBuild logic during restore from a workload.

If we had to do it from a workload, it would be easiest to do during a build -- which is maybe not what we'd want.

}

/// <summary>
/// Checks whether the given framework is a MonoAndroid framework.
/// </summary>
/// <param name="framework">The framework to check, or null.</param>
/// <returns>True if the framework uses the MonoAndroid framework identifier.</returns>
internal static bool IsMonoAndroidFramework(NuGetFramework framework)
{
return framework != null
&& StringComparer.OrdinalIgnoreCase.Equals(
framework.Framework,
FrameworkConstants.FrameworkIdentifiers.MonoAndroid);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static LockFileTargetLibrary CreateLockFileTargetLibrary(
RestoreTargetGraph targetGraph,
LibraryIncludeFlags dependencyType)
{
var (lockFileTargetLibrary, _) = CreateLockFileTargetLibrary(
var (lockFileTargetLibrary, _, _, _) = CreateLockFileTargetLibrary(
aliases: null,
library,
package,
Expand All @@ -57,8 +57,8 @@ public static LockFileTargetLibrary CreateLockFileTargetLibrary(
/// <param name="targetFrameworkOverride">The original framework if the asset selection is happening for a fallback framework.</param>
/// <param name="dependencies">The dependencies of this package.</param>
/// <param name="cache">The lock file build cache.</param>
/// <returns>The LockFileTargetLibrary, and whether a fallback framework criteria was used to select it.</returns>
internal static (LockFileTargetLibrary, bool) CreateLockFileTargetLibrary(
/// <returns>The LockFileTargetLibrary, whether a fallback framework criteria was used to select it, the framework selected for compile assets, and the framework selected for runtime assets.</returns>
internal static (LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework) CreateLockFileTargetLibrary(
string aliases,
LockFileLibrary library,
LocalPackageInfo package,
Expand All @@ -75,6 +75,8 @@ internal static (LockFileTargetLibrary, bool) CreateLockFileTargetLibrary(
() =>
{
LockFileTargetLibrary lockFileLib = null;
NuGetFramework compileAssetFramework = null;
NuGetFramework runtimeAssetFramework = null;
// This will throw an appropriate error if the nuspec is missing
var nuspec = package.Nuspec;

Expand All @@ -86,7 +88,7 @@ internal static (LockFileTargetLibrary, bool) CreateLockFileTargetLibrary(

for (var i = 0; i < orderedCriteriaSets.Count; i++)
{
lockFileLib = CreateLockFileTargetLibrary(aliases, library, package, targetGraph.Conventions, dependencyType,
(lockFileLib, compileAssetFramework, runtimeAssetFramework) = CreateLockFileTargetLibrary(aliases, library, package, targetGraph.Conventions, dependencyType,
framework, runtimeIdentifier, contentItems, nuspec, packageTypes, orderedCriteriaSets[i].orderedCriteria);
// Check if compatible assets were found.
// If no compatible assets were found and this is the last check
Expand All @@ -108,7 +110,7 @@ internal static (LockFileTargetLibrary, bool) CreateLockFileTargetLibrary(

lockFileLib.Freeze();

return (lockFileLib, fallbackUsed);
return (lockFileLib, fallbackUsed, compileAssetFramework, runtimeAssetFramework);
});
}

Expand Down Expand Up @@ -165,7 +167,7 @@ private static void ApplyAliases(string aliases, LockFileItem item)
/// <summary>
/// Populate assets for a <see cref="LockFileLibrary"/>.
/// </summary>
internal static LockFileTargetLibrary CreateLockFileTargetLibrary(
internal static (LockFileTargetLibrary lockFileLib, NuGetFramework compileAssetFramework, NuGetFramework runtimeAssetFramework) CreateLockFileTargetLibrary(
string aliases,
LockFileLibrary library,
LocalPackageInfo package,
Expand Down Expand Up @@ -198,13 +200,16 @@ internal static LockFileTargetLibrary CreateLockFileTargetLibrary(
orderedCriteria,
contentItems,
applyAliases,
out NuGetFramework compileFramework,
managedCodeConventions.Patterns.CompileRefAssemblies,
managedCodeConventions.Patterns.CompileLibAssemblies);

// Runtime
lockFileLib.RuntimeAssemblies = GetLockFileItems(
orderedCriteria,
contentItems,
additionalAction: null,
out NuGetFramework runtimeFramework,
managedCodeConventions.Patterns.RuntimeAssemblies);

// Embed
Expand Down Expand Up @@ -242,7 +247,7 @@ internal static LockFileTargetLibrary CreateLockFileTargetLibrary(
// Apply filters from the <references> node in the nuspec
ApplyReferenceFilter(lockFileLib, framework, nuspec);

return lockFileLib;
return (lockFileLib, compileFramework, runtimeFramework);
}

private static void AddMSBuildAssets(
Expand Down Expand Up @@ -659,15 +664,17 @@ private static List<LockFileItem> ConvertToProjectPaths(
}

/// <summary>
/// Create lock file items for the best matching group.
/// Create lock file items for the best matching group, and optionally output the selected framework.
/// </summary>
/// <remarks>Enumerate this once after calling.</remarks>
private static IList<LockFileItem> GetLockFileItems(
List<SelectionCriteria> criteria,
ContentItemCollection items,
Action<LockFileItem> additionalAction,
out NuGetFramework selectedFramework,
params PatternSet[] patterns)
{
selectedFramework = null;
List<LockFileItem> result = null;
// Loop through each criteria taking the first one that matches one or more items.
foreach (var managedCriteria in criteria)
Expand All @@ -678,6 +685,12 @@ private static IList<LockFileItem> GetLockFileItems(

if (group != null)
{
if (group.Properties.TryGetValue(
ManagedCodeConventions.PropertyNames.TargetFrameworkMoniker, out object tfmObj))
{
selectedFramework = tfmObj as NuGetFramework;
}

result = new(group.Items.Count);
foreach (var item in group.Items.NoAllocEnumerate())
{
Expand Down Expand Up @@ -711,7 +724,7 @@ private static IList<LockFileItem> GetLockFileItems(
ContentItemCollection items,
params PatternSet[] patterns)
{
return GetLockFileItems(criteria, items, additionalAction: null, patterns);
return GetLockFileItems(criteria, items, additionalAction: null, out _, patterns);
}

/// <summary>
Expand Down
9 changes: 9 additions & 0 deletions src/NuGet.Core/NuGet.Commands/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/NuGet.Core/NuGet.Commands/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@
<data name="Log_ImportsFallbackWarning" xml:space="preserve">
<value>Package '{0}' was restored using '{1}' instead of the project target framework '{2}'. This package may not be fully compatible with your project.</value>
</data>
<data name="Warning_MonoAndroidFrameworkDeprecated" xml:space="preserve">
<value>Package '{0}' {1} uses the deprecated MonoAndroid framework instead of 'net6.0-android' or later. Consider upgrading to a newer version of this package or contacting the package author.</value>
<comment>{0} - package id
{1} - package version</comment>
</data>
<data name="Log_CycleDetected" xml:space="preserve">
<value>Cycle detected.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal static class SdkAnalysisLevelMinimums
/// <summary>
/// Minimum SDK Analysis Level required for:
/// <list type="bullet">
/// <item>warning when packages use the deprecated MonoAndroid framework</item>
/// <item>error when TargetFramework alias contains non-ASCII characters</item>
/// </list>
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/NuGet.Core/NuGet.Commands/xlf/Strings.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,12 @@ NuGet vyžaduje zdroje HTTPS. Další informace najdete na https://aka.ms/nuget-
<target state="translated">{0} neposkytuje inkluzivní dolní mez pro závislost {1}. Místo toho bylo přeloženo: {2}.</target>
<note />
</trans-unit>
<trans-unit id="Warning_MonoAndroidFrameworkDeprecated">
<source>Package '{0}' {1} uses the deprecated MonoAndroid framework instead of 'net6.0-android' or later. Consider upgrading to a newer version of this package or contacting the package author.</source>
<target state="new">Package '{0}' {1} uses the deprecated MonoAndroid framework instead of 'net6.0-android' or later. Consider upgrading to a newer version of this package or contacting the package author.</target>
<note>{0} - package id
{1} - package version</note>
</trans-unit>
<trans-unit id="Warning_PackageWithKnownVulnerability">
<source>Package '{0}' {1} has a known {2} severity vulnerability, {3}</source>
<target state="translated">Balíček „{0}“ {1} má známé {2} ohrožení zabezpečení závažnosti, {3}</target>
Expand Down
6 changes: 6 additions & 0 deletions src/NuGet.Core/NuGet.Commands/xlf/Strings.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,12 @@ NuGet erfordert HTTPS-Quellen. Weitere Informationen finden Sie unter https://ak
<target state="translated">{0} stellt keine inklusive untere Grenze für Abhängigkeiten {1} bereit. {2} wurde stattdessen aufgelöst.</target>
<note />
</trans-unit>
<trans-unit id="Warning_MonoAndroidFrameworkDeprecated">
<source>Package '{0}' {1} uses the deprecated MonoAndroid framework instead of 'net6.0-android' or later. Consider upgrading to a newer version of this package or contacting the package author.</source>
<target state="new">Package '{0}' {1} uses the deprecated MonoAndroid framework instead of 'net6.0-android' or later. Consider upgrading to a newer version of this package or contacting the package author.</target>
<note>{0} - package id
{1} - package version</note>
</trans-unit>
<trans-unit id="Warning_PackageWithKnownVulnerability">
<source>Package '{0}' {1} has a known {2} severity vulnerability, {3}</source>
<target state="translated">Das Paket "{0}" {1} weist eine bekannte {2} Schweregrad-Sicherheitsanfälligkeit auf, {3}.</target>
Expand Down
6 changes: 6 additions & 0 deletions src/NuGet.Core/NuGet.Commands/xlf/Strings.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,12 @@ NuGet requiere orígenes HTTPS. Consulte https://aka.ms/nuget-https-everywhere p
<target state="translated">{0} no proporciona un límite inferior inclusivo para la dependencia {1}. {2} se resolvió en su lugar.</target>
<note />
</trans-unit>
<trans-unit id="Warning_MonoAndroidFrameworkDeprecated">
<source>Package '{0}' {1} uses the deprecated MonoAndroid framework instead of 'net6.0-android' or later. Consider upgrading to a newer version of this package or contacting the package author.</source>
<target state="new">Package '{0}' {1} uses the deprecated MonoAndroid framework instead of 'net6.0-android' or later. Consider upgrading to a newer version of this package or contacting the package author.</target>
<note>{0} - package id
{1} - package version</note>
</trans-unit>
<trans-unit id="Warning_PackageWithKnownVulnerability">
<source>Package '{0}' {1} has a known {2} severity vulnerability, {3}</source>
<target state="translated">El paquete "{0}" {1} tiene una vulnerabilidad de gravedad {2} conocida, {3}</target>
Expand Down
Loading