diff --git a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/RemoveDirectoryBackups.cs b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/RemoveDirectoryBackups.cs
new file mode 100644
index 00000000000..0e687a7f387
--- /dev/null
+++ b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/RemoveDirectoryBackups.cs
@@ -0,0 +1,36 @@
+using System;
+using System.IO;
+using Microsoft.Build.Framework;
+
+namespace Xamarin.Android.Tools.BootstrapTasks
+{
+ public sealed class RemoveDirectoryBackups : RetryingDirectoryTask
+ {
+ [Required]
+ public ITaskItem [] Directories { get; set; } = [];
+
+ public override bool Execute ()
+ {
+ if (!ValidateRetryParameters ())
+ return false;
+
+ foreach (var item in Directories) {
+ var directory = NormalizeDirectoryPath (item.ItemSpec);
+ var parentDirectory = Path.GetDirectoryName (directory);
+ if (string.IsNullOrEmpty (parentDirectory) || !Directory.Exists (parentDirectory))
+ continue;
+
+ var directoryName = Path.GetFileName (directory);
+ foreach (var backupDirectory in Directory.GetDirectories (parentDirectory, $"{directoryName}.old-*", SearchOption.TopDirectoryOnly)) {
+ if (!TryDeleteDirectoryWithRetry (backupDirectory, out var deleteError)) {
+ Log.LogWarning (
+ $"Could not remove old directory '{backupDirectory}' after {RetryCount + 1} attempts: " +
+ $"{deleteError.Message}{GetRemainingEntries (backupDirectory)}");
+ }
+ }
+ }
+
+ return !Log.HasLoggedErrors;
+ }
+ }
+}
diff --git a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/ReplaceDirectory.cs b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/ReplaceDirectory.cs
new file mode 100644
index 00000000000..53e090f7e67
--- /dev/null
+++ b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/ReplaceDirectory.cs
@@ -0,0 +1,84 @@
+using System;
+using System.IO;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Xamarin.Android.Tools.BootstrapTasks
+{
+ public class ReplaceDirectory : RetryingDirectoryTask
+ {
+ [Required]
+ public string SourceDirectory { get; set; } = "";
+
+ [Required]
+ public string DestinationDirectory { get; set; } = "";
+
+ [Required]
+ public string RequiredFile { get; set; } = "";
+
+ public override bool Execute ()
+ {
+ if (!ValidateRetryParameters ())
+ return false;
+
+ var sourceDirectory = NormalizeDirectoryPath (SourceDirectory);
+ var destinationDirectory = NormalizeDirectoryPath (DestinationDirectory);
+ if (!Directory.Exists (sourceDirectory)) {
+ Log.LogError ($"Source directory '{sourceDirectory}' does not exist.");
+ return false;
+ }
+ if (!File.Exists (Path.Combine (sourceDirectory, RequiredFile))) {
+ Log.LogError ($"Source directory '{sourceDirectory}' does not contain required file '{RequiredFile}'.");
+ return false;
+ }
+
+ var parentDirectory = Path.GetDirectoryName (destinationDirectory);
+ if (!string.IsNullOrEmpty (parentDirectory))
+ Directory.CreateDirectory (parentDirectory);
+
+ string backupDirectory = null;
+ try {
+ if (Directory.Exists (destinationDirectory)) {
+ var candidate = destinationDirectory + $".old-{Guid.NewGuid ():N}";
+ MoveDirectoryWithRetry (destinationDirectory, candidate);
+ backupDirectory = candidate;
+ }
+
+ try {
+ MoveDirectoryWithRetry (sourceDirectory, destinationDirectory);
+ } catch {
+ RestoreBackup (backupDirectory, destinationDirectory);
+ throw;
+ }
+ } catch (Exception e) {
+ Log.LogError ($"Failed to replace directory '{destinationDirectory}' with '{sourceDirectory}': {e.Message}");
+ return false;
+ }
+
+ if (backupDirectory != null && !TryDeleteDirectoryWithRetry (backupDirectory, out var deleteError)) {
+ Log.LogWarning (
+ $"Installed '{destinationDirectory}', but could not remove old directory '{backupDirectory}' after {RetryCount + 1} attempts: " +
+ $"{deleteError.Message}{GetRemainingEntries (backupDirectory)}");
+ }
+
+ return !Log.HasLoggedErrors;
+ }
+
+ void RestoreBackup (string backupDirectory, string destinationDirectory)
+ {
+ if (backupDirectory == null || !Directory.Exists (backupDirectory))
+ return;
+ if (Directory.Exists (destinationDirectory)) {
+ Log.LogError ($"Cannot restore previous directory from '{backupDirectory}' because '{destinationDirectory}' exists.");
+ return;
+ }
+ try {
+ MoveDirectoryWithRetry (backupDirectory, destinationDirectory);
+ Log.LogMessage (MessageImportance.Normal, $"Restored previous directory from '{backupDirectory}'.");
+ } catch (Exception e) {
+ Log.LogError ($"Failed to restore previous directory from '{backupDirectory}': {e.Message}");
+ }
+ }
+
+ }
+}
diff --git a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/RetryingDirectoryTask.cs b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/RetryingDirectoryTask.cs
new file mode 100644
index 00000000000..895004ef9ed
--- /dev/null
+++ b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/RetryingDirectoryTask.cs
@@ -0,0 +1,103 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Xamarin.Android.Tools.BootstrapTasks
+{
+ public abstract class RetryingDirectoryTask : Task
+ {
+ public int RetryCount { get; set; } = 10;
+
+ public int RetryDelayMilliseconds { get; set; } = 200;
+
+ protected bool ValidateRetryParameters ()
+ {
+ if (RetryCount < 0) {
+ Log.LogError ($"{nameof (RetryCount)} must be greater than or equal to zero.");
+ return false;
+ }
+ if (RetryDelayMilliseconds < 0) {
+ Log.LogError ($"{nameof (RetryDelayMilliseconds)} must be greater than or equal to zero.");
+ return false;
+ }
+ return true;
+ }
+
+ protected void MoveDirectoryWithRetry (string source, string destination)
+ {
+ for (int attempt = 0; ; attempt++) {
+ try {
+ MoveDirectory (source, destination);
+ return;
+ } catch (Exception e) when ((e is IOException || e is UnauthorizedAccessException) && attempt < RetryCount) {
+ Log.LogMessage (
+ MessageImportance.Normal,
+ $"Could not move directory '{source}' to '{destination}' (attempt {attempt + 1} of {RetryCount + 1}): {e.Message} Retrying.");
+ Delay (RetryDelayMilliseconds * (attempt + 1));
+ }
+ }
+ }
+
+ protected bool TryDeleteDirectoryWithRetry (string directory, out Exception error)
+ {
+ error = null;
+ for (int attempt = 0; ; attempt++) {
+ try {
+ if (!Directory.Exists (directory))
+ return true;
+ if (Path.DirectorySeparatorChar == '\\')
+ Files.SetDirectoryWriteable (directory);
+ DeleteDirectory (directory);
+ return true;
+ } catch (DirectoryNotFoundException) {
+ return true;
+ } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) {
+ error = e;
+ if (attempt >= RetryCount)
+ return false;
+ Log.LogMessage (
+ MessageImportance.Normal,
+ $"Could not remove directory '{directory}' (attempt {attempt + 1} of {RetryCount + 1}): {e.Message} Retrying.");
+ Delay (RetryDelayMilliseconds * (attempt + 1));
+ }
+ }
+ }
+
+ protected static string GetRemainingEntries (string directory)
+ {
+ try {
+ var entries = Directory.EnumerateFileSystemEntries (directory, "*", SearchOption.AllDirectories)
+ .Take (10)
+ .Select (path => $"'{path}'")
+ .ToArray ();
+ return entries.Length == 0 ? "" : $" Remaining entries: {string.Join (", ", entries)}.";
+ } catch (Exception e) {
+ return $" Remaining entries could not be enumerated: {e.Message}";
+ }
+ }
+
+ protected static string NormalizeDirectoryPath (string directory)
+ {
+ return Path.TrimEndingDirectorySeparator (Path.GetFullPath (directory));
+ }
+
+ protected virtual void MoveDirectory (string source, string destination)
+ {
+ Directory.Move (source, destination);
+ }
+
+ protected virtual void DeleteDirectory (string directory)
+ {
+ Directory.Delete (directory, recursive: true);
+ }
+
+ protected virtual void Delay (int milliseconds)
+ {
+ Thread.Sleep (milliseconds);
+ }
+ }
+}
diff --git a/src/androidsdk/androidsdk.targets b/src/androidsdk/androidsdk.targets
index b9974e08caf..b3028f3f328 100644
--- a/src/androidsdk/androidsdk.targets
+++ b/src/androidsdk/androidsdk.targets
@@ -48,6 +48,8 @@
+
+
-
<_StagedTopDir Include="$([System.IO.Directory]::GetDirectories('$(_StagingDir)'))" />
-
- <_GlobRoot Condition=" '$(_StripComponents)' == '1' ">@(_StagedTopDir)
- <_GlobRoot Condition=" '$(_StripComponents)' == '0' ">$(_StagingDir)
+
+ <_InstallSourceDir Condition=" '$(HostOS)' == 'Windows' Or '$(_StripComponents)' == '0' ">$(_StagingDir)
+ <_InstallSourceDir Condition=" '$(HostOS)' != 'Windows' And '$(_StripComponents)' == '1' ">@(_StagedTopDir)
-
- <_StagedFile Remove="@(_StagedFile)" />
- <_StagedFile Include="$(_GlobRoot)\**\*" />
-
-
-
-
+
+
+
+
+
+
+
+
+
+