Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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}");
}
}

}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
62 changes: 34 additions & 28 deletions src/androidsdk/androidsdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

<UsingTask AssemblyFile="$(BootstrapTasksAssembly)" TaskName="Xamarin.Android.Tools.BootstrapTasks.UnzipDirectoryChildren" TaskFactory="TaskHostFactory" Runtime="NET" />
<UsingTask AssemblyFile="$(BootstrapTasksAssembly)" TaskName="Xamarin.Android.Tools.BootstrapTasks.EnsureAndroidSdkLicense" TaskFactory="TaskHostFactory" Runtime="NET" />
<UsingTask AssemblyFile="$(BootstrapTasksAssembly)" TaskName="Xamarin.Android.Tools.BootstrapTasks.ReplaceDirectory" TaskFactory="TaskHostFactory" Runtime="NET" />
<UsingTask AssemblyFile="$(BootstrapTasksAssembly)" TaskName="Xamarin.Android.Tools.BootstrapTasks.RemoveDirectoryBackups" TaskFactory="TaskHostFactory" Runtime="NET" />
Comment on lines +51 to +52

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.

Is there a way we could implement this without using two new MSBuild tasks? Can the built-in MSBuild ones work for this?


<!--
Catalog of platform APIs. Each item is downloaded as `<Identity>.zip`.
Expand Down Expand Up @@ -275,7 +277,7 @@
</ItemGroup>

<Target Name="_AddPlatformPackagesToInstall"
BeforeTargets="_DownloadAndroidSdkPackages;_ExtractAndroidSdkPackages;_GenerateAndroidPackageXmls;_CleanAndroidSdkComponents">
BeforeTargets="_DownloadAndroidSdkPackages;_ExtractAndroidSdkPackages;_GenerateAndroidPackageXmls;_CleanAndroidSdkPackageBackups;_CleanAndroidSdkComponents">
<ItemGroup>
<_AndroidSdkPackage Include="@(_PlatformPackage->'%(Identity).zip')"
Condition=" '$(_InstallAllPlatforms)' == 'true' or
Expand Down Expand Up @@ -332,28 +334,25 @@
<_StripComponents>%(_AndroidSdkPackage.StripComponents)</_StripComponents>
<_StripComponents Condition=" '$(_StripComponents)' == '' ">1</_StripComponents>
<_ZipPath>$([System.IO.Path]::Combine('$(AndroidToolchainCacheDirectory)', '%(_AndroidSdkPackage.Identity)'))</_ZipPath>
<_StagingDir>%(_AndroidSdkPackage.Destination).staging</_StagingDir>
<_DestDir>%(_AndroidSdkPackage.Destination)</_DestDir>
<_DestDir>$([System.Text.RegularExpressions.Regex]::Replace('%(_AndroidSdkPackage.Destination)', '[\\/]+$', ''))</_DestDir>
<_StagingDir>$(_DestDir).staging-$([System.Guid]::NewGuid().ToString('N'))</_StagingDir>
<_NoSubdirectory Condition=" '$(_StripComponents)' == '1' ">false</_NoSubdirectory>
<_NoSubdirectory Condition=" '$(_StripComponents)' == '0' ">true</_NoSubdirectory>
</PropertyGroup>
<RemoveDir Directories="$(_DestDir);$(_StagingDir)" />
<MakeDir Directories="$(_DestDir)" />
<MakeDir Directories="$(_StagingDir)" />
<!-- Windows: LibZipSharp via UnzipDirectoryChildren BootstrapTask. -->
<UnzipDirectoryChildren
Condition=" '$(HostOS)' == 'Windows' "
SourceFiles="$(_ZipPath)"
DestinationFolder="$(_DestDir)"
DestinationFolder="$(_StagingDir)"
NoSubdirectory="$(_NoSubdirectory)"
/>
<!--
Unix: `unzip` (preserves symlinks; LibZipSharp does not, which the NDK needs)
into a sibling .staging directory, then `<Move/>` every file into
$(_DestDir). When StripComponents=1, glob from inside the single
top-level directory of the zip so that `%(RecursiveDir)` already excludes
it - no path-rewriting required. Empty staging dirs are pruned afterwards.
into a unique sibling staging directory. When StripComponents=1, install
the single top-level directory; otherwise install the staging directory
itself. The staged directory atomically replaces the destination.
-->
<MakeDir Condition=" '$(HostOS)' != 'Windows' " Directories="$(_StagingDir)" />
<Exec
Condition=" '$(HostOS)' != 'Windows' "
Command="unzip -q -o &quot;$(_ZipPath)&quot; -d &quot;$(_StagingDir)&quot;"
Expand All @@ -362,27 +361,34 @@
<_StagedTopDir Remove="@(_StagedTopDir)" />
<_StagedTopDir Include="$([System.IO.Directory]::GetDirectories('$(_StagingDir)'))" />
</ItemGroup>
<PropertyGroup Condition=" '$(HostOS)' != 'Windows' ">
<_GlobRoot Condition=" '$(_StripComponents)' == '1' ">@(_StagedTopDir)</_GlobRoot>
<_GlobRoot Condition=" '$(_StripComponents)' == '0' ">$(_StagingDir)</_GlobRoot>
<PropertyGroup>
<_InstallSourceDir Condition=" '$(HostOS)' == 'Windows' Or '$(_StripComponents)' == '0' ">$(_StagingDir)</_InstallSourceDir>
<_InstallSourceDir Condition=" '$(HostOS)' != 'Windows' And '$(_StripComponents)' == '1' ">@(_StagedTopDir)</_InstallSourceDir>
</PropertyGroup>
Comment on lines +364 to 367
<ItemGroup Condition=" '$(HostOS)' != 'Windows' And '$(_GlobRoot)' != '' ">
<_StagedFile Remove="@(_StagedFile)" />
<_StagedFile Include="$(_GlobRoot)\**\*" />
</ItemGroup>
<Move
Condition=" '$(HostOS)' != 'Windows' "
SourceFiles="@(_StagedFile)"
DestinationFiles="@(_StagedFile->'$(_DestDir)\%(RecursiveDir)%(Filename)%(Extension)')"
/>
<RemoveDir Condition=" '$(HostOS)' != 'Windows' " Directories="$(_StagingDir)" />
<Error
Condition=" !Exists('$(_DestDir)\source.properties') "
Text="Expected Android SDK package '%(_AndroidSdkPackage.Identity)' to extract source.properties under '$(_DestDir)'."
<ReplaceDirectory
SourceDirectory="$(_InstallSourceDir)"
DestinationDirectory="$(_DestDir)"
RequiredFile="source.properties"
/>
<RemoveDir Condition=" Exists('$(_StagingDir)') " Directories="$(_StagingDir)" />
<!-- Unzip preserves archive mtimes, so refresh the sentinel used by MSBuild Outputs. -->
<Touch Files="$(_DestDir)\source.properties" />
<Touch Files="$(_DestDir)\.extracted-%(_AndroidSdkPackage.Identity)-%(_AndroidSdkPackage.Hash)" AlwaysCreate="true" />
<OnError ExecuteTargets="_CleanAndroidSdkPackageStaging" />
</Target>

<Target Name="_CleanAndroidSdkPackageStaging">
<RemoveDir
Condition=" '$(_StagingDir)' != '' And Exists('$(_StagingDir)') "
Directories="$(_StagingDir)"
ContinueOnError="WarnAndContinue"
/>
</Target>

<Target Name="_CleanAndroidSdkPackageBackups"
AfterTargets="_ExtractAndroidSdkPackages"
BeforeTargets="_CleanAndroidSdkComponents">
<RemoveDirectoryBackups Directories="@(_AndroidSdkPackage->'%(Destination)')" />
</Target>

<!--
Expand Down Expand Up @@ -411,7 +417,7 @@

<Target Name="_InstallAndroidSdkComponents"
BeforeTargets="Build"
DependsOnTargets="_ExtractAndroidSdkPackages;_GenerateAndroidPackageXmls"
DependsOnTargets="_ExtractAndroidSdkPackages;_CleanAndroidSdkPackageBackups;_GenerateAndroidPackageXmls"
/>

<Target Name="_AcceptAndroidSdkLicenses"
Expand Down
Loading