From 905878b0b5b44e82d588bc139cb59a21de83ce0f Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 2 Mar 2021 09:44:30 -0600 Subject: [PATCH] [One .NET] fix for incremental CoreCompile (#5661) Context: https://github.com/xamarin/Xamarin.Forms/tree/main-handler I noticed that when building Maui, `CoreCompile` seems to be running on every build no matter what: Building target "CoreCompile" completely. Input file "obj\Debug\net6.0-android\Core-net6.csproj.CoreCompileInputs.cache" is newer than output file "bin\Debug\net6.0-android\Microsoft.Maui.xml". I could reproduce this in a test: * Build `AppA` & `LibraryB` * Build `AppA` & `LibraryB` again, `LibraryB` will run `CoreCompile` *every* time. There is a `_GenerateCompileDependencyCache` target that basically does: https://github.com/dotnet/msbuild/blob/83cd7d4e36b71d5b2cefd02cb9a5a58d27dd6a75/src/Tasks/Microsoft.Common.CurrentVersion.targets#L3529 This `*.CoreCompileInputs.cache` file triggers `CoreCompile` to run again when it needs to. However, this file is actually updating on every build, because: 1. Our "outer" build has all our preprocessor defines listed in `@(CoreCompileCache)` like `__MOBILE__`, `__ANDROID__`, etc. 2. The "inner" build for each `$(RuntimeIdentifier)` does *not* have these symbols! And so we get into a situation where `CoreCompile` will always run. The inner & outer builds write different values in this file. To solve this problem, I added our `_AddAndroidDefines` to run before `CoreCompile` in inner builds. I also needed some changes to our MSBuild test framework: * Make `IsTargetSkipped()` and `AssertTargetIsSkipped()` supported for new project types. * Make `IsTargetSkipped()` return `false` if a `Building target "{target}" completely.` message is found. --- ...oft.Android.Sdk.AssemblyResolution.targets | 4 +++ .../Utilities/AssertionExtensions.cs | 18 ++++++++++ .../Xamarin.Android.Build.Tests/XASdkTests.cs | 35 +++++++++++++++++++ .../Common/BuildOutput.cs | 8 +++-- .../Xamarin.ProjectTools/Common/DotNetCLI.cs | 2 ++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets index d105eb5e38c..0efba7c98d9 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets @@ -24,6 +24,10 @@ _ResolveAssemblies MSBuild target. $([MSBuild]::Unescape($(CoreBuildDependsOn.Replace('IncrementalClean;', '')))) + + _AddAndroidDefines; + $(CompileDependsOn); + Lib B + var path = Path.Combine ("temp", TestName); + + var libB = new XASdkProject (outputType: "Library") { + ProjectName = "LibraryB" + }; + libB.Sources.Clear (); + libB.Sources.Add (new BuildItem.Source ("Foo.cs") { + TextContent = () => "public class Foo { }", + }); + + // Will save the project, does not need to build it + CreateDotNetBuilder (libB, Path.Combine (path, libB.ProjectName)); + + var appA = new XASdkProject { + ProjectName = "AppA", + Sources = { + new BuildItem.Source ("Bar.cs") { + TextContent = () => "public class Bar : Foo { }", + } + } + }; + appA.AddReference (libB); + var appBuilder = CreateDotNetBuilder (appA, Path.Combine (path, appA.ProjectName)); + Assert.IsTrue (appBuilder.Build (), $"{appA.ProjectName} should succeed"); + appBuilder.AssertTargetIsNotSkipped ("CoreCompile"); + + // Build again, no changes + Assert.IsTrue (appBuilder.Build (), $"{appA.ProjectName} should succeed"); + appBuilder.AssertTargetIsSkipped ("CoreCompile"); + } + DotNetCLI CreateDotNetBuilder (string relativeProjectDir = null) { if (string.IsNullOrEmpty (relativeProjectDir)) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/BuildOutput.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/BuildOutput.cs index 568a42bb632..853003056f7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/BuildOutput.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/BuildOutput.cs @@ -52,10 +52,14 @@ public List GetAssemblyMapCache () return File.ReadLines (path).ToList (); } - public bool IsTargetSkipped (string target) + public bool IsTargetSkipped (string target) => IsTargetSkipped (Builder.LastBuildOutput, target); + + public static bool IsTargetSkipped (IEnumerable output, string target) { bool found = false; - foreach (var line in Builder.LastBuildOutput) { + foreach (var line in output) { + if (line.Contains ($"Building target \"{target}\" completely.")) + return false; found = line.Contains ($"Target {target} skipped due to ") || line.Contains ($"Skipping target \"{target}\" because it has no ") //NOTE: message can say `inputs` or `outputs` || line.Contains ($"Target \"{target}\" skipped, due to") diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs index 622c1de4d87..06296dfb09b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs @@ -126,6 +126,8 @@ public IEnumerable LastBuildOutput { } } + public bool IsTargetSkipped (string target) => BuildOutput.IsTargetSkipped (LastBuildOutput, target); + List GetDefaultCommandLineArgs (string verb, string target = null, string [] parameters = null) { string testDir = Path.GetDirectoryName (projectOrSolution);