From 9bd7da15a895801d960b182e2457b3b364e75ce9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Jun 2026 19:54:15 +0000
Subject: [PATCH 1/3] Initial plan
From df31a19e94fcf654941c7a924aa05d64a01fa8ac Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Jun 2026 20:22:06 +0000
Subject: [PATCH 2/3] [tests] Add delete-with-retry to fix flaky Windows IO
race
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Common/ProjectBuilder.cs | 10 ++-----
.../Common/SolutionBuilder.cs | 2 +-
.../Utilities/FileSystemUtils.cs | 30 +++++++++++++++++++
3 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs
index db607aa6475..0a74a6a2adc 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs
@@ -104,10 +104,7 @@ public void Save (XamarinProject project, bool doNotCleanupOnUpdate = false, boo
if (!BuiltBefore) {
if (project.ShouldPopulate) {
- if (Directory.Exists (ProjectDirectory)) {
- FileSystemUtils.SetDirectoryWriteable (ProjectDirectory);
- Directory.Delete (ProjectDirectory, true);
- }
+ FileSystemUtils.DeleteDirectoryWithRetry (ProjectDirectory);
project.Populate (ProjectDirectory, files);
}
@@ -202,10 +199,7 @@ public void Cleanup ()
BuiltBefore = false;
var projectDirectory = Path.Combine (XABuildPaths.TestOutputDirectory, ProjectDirectory);
- if (Directory.Exists (projectDirectory)) {
- FileSystemUtils.SetDirectoryWriteable (projectDirectory);
- Directory.Delete (projectDirectory, true);
- }
+ FileSystemUtils.DeleteDirectoryWithRetry (projectDirectory);
}
public struct RuntimeInfo
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs
index e66a1568c67..aeec60a3440 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs
@@ -99,7 +99,7 @@ protected override void Dispose (bool disposing)
if (disposing)
if (BuildSucceeded && !string.IsNullOrEmpty (SolutionPath))
try {
- Directory.Delete (SolutionPath, recursive: true);
+ FileSystemUtils.DeleteDirectoryWithRetry (SolutionPath);
} catch (Exception) {
// This happens on CI occasionally, let's not fail the test
}
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs
index 55fb9c0d199..cf83c06e478 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs
@@ -40,6 +40,36 @@ public static void SetDirectoryWriteable (string directory)
}
}
+ ///
+ /// Recursively deletes a directory, retrying on transient failures.
+ ///
+ /// The directory path to delete.
+ /// The maximum number of retries before giving up.
+ ///
+ /// On Windows, a handle to a just-written file can still be held by another process
+ /// (e.g. the Roslyn shared-compilation server, an anti-virus scanner, or the search
+ /// indexer), causing to throw
+ /// or . This method
+ /// backs off and retries to let the other process release the handle.
+ ///
+ ///
+ public static void DeleteDirectoryWithRetry (string directory, int retries = 10)
+ {
+ if (!Directory.Exists (directory))
+ return;
+
+ SetDirectoryWriteable (directory);
+ for (int i = 0; ; i++) {
+ try {
+ Directory.Delete (directory, true);
+ return;
+ } catch (Exception e) when ((e is UnauthorizedAccessException || e is IOException) && i < retries) {
+ Thread.Sleep (200 * (i + 1)); // back off; let AV/Roslyn release the handle
+ SetDirectoryWriteable (directory);
+ }
+ }
+ }
+
///
/// Sets a single file to be writable by removing the read-only attribute if present.
///
From 848ccdfb4fe85e014628eb3806f7741fc331000d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 30 Jun 2026 10:33:32 +0000
Subject: [PATCH 3/3] Handle DirectoryNotFoundException in delete-with-retry
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs
index cf83c06e478..b49eec03c4c 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs
@@ -58,14 +58,15 @@ public static void DeleteDirectoryWithRetry (string directory, int retries = 10)
if (!Directory.Exists (directory))
return;
- SetDirectoryWriteable (directory);
for (int i = 0; ; i++) {
try {
+ SetDirectoryWriteable (directory);
Directory.Delete (directory, true);
return;
+ } catch (DirectoryNotFoundException) {
+ return;
} catch (Exception e) when ((e is UnauthorizedAccessException || e is IOException) && i < retries) {
Thread.Sleep (200 * (i + 1)); // back off; let AV/Roslyn release the handle
- SetDirectoryWriteable (directory);
}
}
}