From bd575a7134b6b84315718486c2886596de106d0e Mon Sep 17 00:00:00 2001 From: Emanuel Fernandez Dell'Oca Date: Fri, 8 Aug 2025 20:22:32 -0400 Subject: [PATCH 1/5] [Windows] Fix _CompileNativeExecutable inputs/outputs `_CompileNativeExecutable` was always being executed from Windows being its inputs and outputs were never generated: - **ILLink**: we were not creating outputs for the `linker-cache` directory. Also, the previous behavior would always create an output file for all the Macs for the analyzed directories, but not all of them actually changed on each run, making the Windows side outputs to be incorrectly updated (and forcing other targets execution). - **CompileNativeCode**: was not reporting the compiled output files, forcing `_CompileNativeExecutable` to always run. Finally, removed the `Xamarin.MacDev.Tasks.ILLink` output parameters we don't use from the targets. The task contains those outputs mainly to create the empty output files on Windows. I'll add a test for this in an upcoming PR. Fixes https://github.com/dotnet/macios/issues/19609 --- .../Tasks/CompileNativeCode.cs | 12 +++ msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs | 77 +++++++++++++++---- .../Xamarin.iOS.Common.After.targets | 3 +- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs index cc3332f6b61d..a5b092d9dd71 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs @@ -37,6 +37,11 @@ public class CompileNativeCode : XamarinTask, ICancelableTask, ITaskCallback { public string DotNetRoot { get; set; } = ""; #endregion + #region Outputs + [Output] + public ITaskItem [] CompiledOutputFiles { get; set; } = Array.Empty (); + #endregion + public override bool Execute () { if (ShouldExecuteRemotely ()) { @@ -70,6 +75,7 @@ public override bool Execute () var compileInfo = sortedCompileInfo.Select (v => v.Item).ToArray (); var processes = new Task [compileInfo.Length]; + var outputFiles = new string [compileInfo.Length]; for (var i = 0; i < compileInfo.Length; i++) { var info = compileInfo [i]; @@ -127,6 +133,7 @@ public override bool Execute () var outputFile = info.GetMetadata ("OutputFile"); if (string.IsNullOrEmpty (outputFile)) outputFile = Path.ChangeExtension (src, ".o"); + outputFiles [i] = outputFile; // We keep the relative path for remote builds outputFile = Path.GetFullPath (outputFile); arguments.Add ("-o"); arguments.Add (outputFile); @@ -139,6 +146,11 @@ public override bool Execute () System.Threading.Tasks.Task.WaitAll (processes); + // Collect all output files (regardless of compilation success) + CompiledOutputFiles = outputFiles + .Select (file => new TaskItem (file)) + .ToArray (); + return !Log.HasLoggedErrors; } diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs index 1973eb57cc52..45f9295bb1de 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Tasks; using Microsoft.Build.Utilities; @@ -10,7 +11,7 @@ #nullable enable namespace Xamarin.MacDev.Tasks { - public class ILLink : global::ILLink.Tasks.ILLink { + public class ILLink : global::ILLink.Tasks.ILLink, ITaskCallback { public string SessionId { get; set; } = string.Empty; public ITaskItem [] DebugSymbols { get; set; } = Array.Empty (); @@ -18,9 +19,15 @@ public class ILLink : global::ILLink.Tasks.ILLink { [Required] public string LinkerItemsDirectory { get; set; } = string.Empty; + [Required] + public string LinkerCacheDirectory { get; set; } = string.Empty; + [Output] public ITaskItem [] LinkerOutputItems { get; set; } = Array.Empty (); + [Output] + public ITaskItem [] LinkerCacheItems { get; set; } = Array.Empty (); + [Output] public ITaskItem [] LinkedItems { get; set; } = Array.Empty (); @@ -29,26 +36,17 @@ public override bool Execute () if (this.ShouldExecuteRemotely (SessionId)) return new TaskRunner (SessionId, BuildEngine4).RunAsync (this).Result; + // Capture execution start time for Mac-side detection + var executionStartTime = DateTime.UtcNow; var result = base.Execute (); - var linkerItems = new List (); - var linkedItems = new List (); - if (result) { - // Adds all the files in the linker-items dir - foreach (var item in Directory.EnumerateFiles (LinkerItemsDirectory)) { - linkerItems.Add (new TaskItem (item)); - } - - // Adds all the files in the linked output dir - foreach (var item in Directory.EnumerateFiles (OutputDirectory.ItemSpec)) { - linkedItems.Add (new TaskItem (item)); - } + // Collect all files and tag those modified during this execution + LinkerOutputItems = GetAllFilesWithMetadata (LinkerItemsDirectory, executionStartTime); + LinkedItems = GetAllFilesWithMetadata (OutputDirectory.ItemSpec, executionStartTime); + LinkerCacheItems = GetAllFilesWithMetadata (LinkerCacheDirectory, executionStartTime); } - LinkerOutputItems = linkerItems.ToArray (); - LinkedItems = linkedItems.ToArray (); - return result; } @@ -59,5 +57,52 @@ public override void Cancel () else base.Cancel (); } + + ITaskItem [] GetAllFilesWithMetadata (string directory, DateTime executionStartTime) + { + if (string.IsNullOrEmpty (directory) || !Directory.Exists (directory)) + return Array.Empty (); + + return Directory.EnumerateFiles (directory) + .Select (file => { + var fileInfo = new FileInfo (file); + var item = new TaskItem (file); + + // Check if file was created or modified during this execution + var wasModified = fileInfo.CreationTimeUtc >= executionStartTime || + fileInfo.LastWriteTimeUtc >= executionStartTime; + + // Tag files that were modified during this execution + item.SetMetadata ("Modified", wasModified.ToString ()); + + return item; + }) + .ToArray (); + } + + // ITaskCallback implementation + public bool ShouldCopyToBuildServer (ITaskItem item) => true; + + public bool ShouldCreateOutputFile (ITaskItem item) + { + var modifiedMetadata = item.GetMetadata ("Modified"); + var wasModified = bool.TryParse (modifiedMetadata, out var modified) && modified; + + // Create output file if it was modified during this execution + if (wasModified) { + Log.LogMessage (MessageImportance.Low, "Output file '{0}' was modified during execution", item.ItemSpec); + return true; + } + + // Create output file if it doesn't exist on Windows. We assume if it exists on the Mac we also need it on Windows. + if (!File.Exists (item.ItemSpec)) { + Log.LogMessage (MessageImportance.Low, "Output file '{0}' does not exist", item.ItemSpec); + return true; + } + + return false; + } + + public IEnumerable GetAdditionalItemsToBeCopied () => Array.Empty (); } } diff --git a/msbuild/Xamarin.iOS.Tasks.Windows/Xamarin.iOS.Common.After.targets b/msbuild/Xamarin.iOS.Tasks.Windows/Xamarin.iOS.Common.After.targets index 781953fb30e3..18f47cefeb98 100644 --- a/msbuild/Xamarin.iOS.Tasks.Windows/Xamarin.iOS.Common.After.targets +++ b/msbuild/Xamarin.iOS.Tasks.Windows/Xamarin.iOS.Common.After.targets @@ -419,11 +419,10 @@ Copyright (C) 2011-2013 Xamarin. All rights reserved. ToolPath="$(_DotNetHostDirectory)" ILLinkPath="$(_RemoteILLinkPath)" LinkerItemsDirectory="$(_LinkerItemsDirectory)" + LinkerCacheDirectory="$(_LinkerCacheDirectory)" DebugSymbols="@(_ILLinkDebugSymbols)" ContinueOnError="ErrorAndContinue"> - - From e0db112259f0fa9e499d6eeda89a8d3e5aa0eb0f Mon Sep 17 00:00:00 2001 From: GitHub Actions Autoformatter Date: Sat, 9 Aug 2025 00:29:41 +0000 Subject: [PATCH 2/5] Auto-format source code --- msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs index 45f9295bb1de..75d859311b95 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs @@ -67,14 +67,14 @@ ITaskItem [] GetAllFilesWithMetadata (string directory, DateTime executionStartT .Select (file => { var fileInfo = new FileInfo (file); var item = new TaskItem (file); - + // Check if file was created or modified during this execution - var wasModified = fileInfo.CreationTimeUtc >= executionStartTime || - fileInfo.LastWriteTimeUtc >= executionStartTime; - + var wasModified = fileInfo.CreationTimeUtc >= executionStartTime || + fileInfo.LastWriteTimeUtc >= executionStartTime; + // Tag files that were modified during this execution item.SetMetadata ("Modified", wasModified.ToString ()); - + return item; }) .ToArray (); @@ -87,19 +87,19 @@ public bool ShouldCreateOutputFile (ITaskItem item) { var modifiedMetadata = item.GetMetadata ("Modified"); var wasModified = bool.TryParse (modifiedMetadata, out var modified) && modified; - + // Create output file if it was modified during this execution if (wasModified) { Log.LogMessage (MessageImportance.Low, "Output file '{0}' was modified during execution", item.ItemSpec); return true; } - + // Create output file if it doesn't exist on Windows. We assume if it exists on the Mac we also need it on Windows. if (!File.Exists (item.ItemSpec)) { Log.LogMessage (MessageImportance.Low, "Output file '{0}' does not exist", item.ItemSpec); return true; } - + return false; } From c081611603b7869ecd5497e5df1590a3ebc67634 Mon Sep 17 00:00:00 2001 From: Emanuel Fernandez Dell'Oca Date: Mon, 11 Aug 2025 15:07:41 -0400 Subject: [PATCH 3/5] Delay converting src to full path We do this after analyzing the outputFile path, because if the metadata is not set it will be inferred from src and for remote builds we need relative paths. --- msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs index a5b092d9dd71..4eb61aaf4d41 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs @@ -79,7 +79,6 @@ public override bool Execute () for (var i = 0; i < compileInfo.Length; i++) { var info = compileInfo [i]; - var src = Path.GetFullPath (info.ItemSpec); var arguments = new List (); arguments.Add ("clang"); @@ -130,10 +129,12 @@ public override bool Execute () arguments.AddRange (parsed_args); + var src = info.ItemSpec; var outputFile = info.GetMetadata ("OutputFile"); if (string.IsNullOrEmpty (outputFile)) outputFile = Path.ChangeExtension (src, ".o"); outputFiles [i] = outputFile; // We keep the relative path for remote builds + src = Path.GetFullPath (src); outputFile = Path.GetFullPath (outputFile); arguments.Add ("-o"); arguments.Add (outputFile); From b74f892b2448ddd5ed4b8b72881855404e8fb05f Mon Sep 17 00:00:00 2001 From: Emanuel Fernandez Dell'Oca Date: Mon, 11 Aug 2025 15:11:17 -0400 Subject: [PATCH 4/5] Log message when output did not change --- msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs index 75d859311b95..e560a965b6f1 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs @@ -100,6 +100,7 @@ public bool ShouldCreateOutputFile (ITaskItem item) return true; } + Log.LogMessage (MessageImportance.Low, "Output file '{0}' exists and was not modified", item.ItemSpec); return false; } From 494715ee21620d35f87409451811e873fc5a4abf Mon Sep 17 00:00:00 2001 From: Emanuel Fernandez Dell'Oca Date: Mon, 11 Aug 2025 15:38:18 -0400 Subject: [PATCH 5/5] ILLink - Scan output subdirectories --- msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs index e560a965b6f1..a8fd5b7828c5 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ILLink.cs @@ -63,7 +63,7 @@ ITaskItem [] GetAllFilesWithMetadata (string directory, DateTime executionStartT if (string.IsNullOrEmpty (directory) || !Directory.Exists (directory)) return Array.Empty (); - return Directory.EnumerateFiles (directory) + return Directory.EnumerateFiles (directory, "*", SearchOption.AllDirectories) .Select (file => { var fileInfo = new FileInfo (file); var item = new TaskItem (file);