2023-06-08 09:00:41 +02:00
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
2023-06-09 11:42:50 +02:00
#if ! NETFRAMEWORK
#nullable enable
2023-06-08 09:00:41 +02:00
using System ;
using System.Collections.Generic ;
using System.ComponentModel ;
using System.IO ;
using System.IO.Enumeration ;
using System.IO.MemoryMappedFiles ;
using System.Linq ;
using System.Runtime.InteropServices ;
2023-06-09 11:42:50 +02:00
#endif
2023-06-08 09:00:41 +02:00
using Microsoft.Build.Framework ;
using Microsoft.Build.Utilities ;
namespace Microsoft.DotNet.Build.Tasks
{
/// <summary>
/// Replaces files that have the same content with hard links.
/// </summary>
2023-10-17 10:21:02 +02:00
public sealed class ReplaceFilesWithSymbolicLinks : Task
2023-06-08 09:00:41 +02:00
{
/// <summary>
2023-10-17 10:21:02 +02:00
/// The path to the directory to recursively search for files to replace with symbolic links.
2023-06-08 09:00:41 +02:00
/// </summary>
[Required]
public string Directory { get ; set ; } = "" ;
2023-10-17 10:21:02 +02:00
/// <summary>
/// The path to the directory with files to link to.
/// </summary>
[Required]
public string LinkToFilesFrom { get ; set ; } = "" ;
2023-06-09 11:42:50 +02:00
#if NETFRAMEWORK
public override bool Execute ( )
{
2023-10-17 10:21:02 +02:00
Log . LogError ( $"{nameof(ReplaceFilesWithSymbolicLinks)} is not supported on .NET Framework." ) ;
2023-06-09 11:42:50 +02:00
return false ;
}
#else
2023-06-08 09:00:41 +02:00
public override bool Execute ( )
{
if ( OperatingSystem . IsWindows ( ) )
{
2023-10-17 10:21:02 +02:00
Log . LogError ( $"{nameof(ReplaceFilesWithSymbolicLinks)} is not supported on Windows." ) ;
2023-06-08 09:00:41 +02:00
return false ;
}
if ( ! System . IO . Directory . Exists ( Directory ) )
{
Log . LogError ( $"'{Directory}' does not exist." ) ;
return false ;
}
2023-10-17 10:21:02 +02:00
if ( ! System . IO . Directory . Exists ( LinkToFilesFrom ) )
{
Log . LogError ( $"'{LinkToFilesFrom}' does not exist." ) ;
return false ;
}
2023-06-08 09:00:41 +02:00
// Find all non-empty, non-symbolic link files.
2023-10-17 10:21:02 +02:00
string [ ] files = new FileSystemEnumerable < string > (
Directory ,
( ref FileSystemEntry entry ) = > entry . ToFullPath ( ) ,
new EnumerationOptions ( )
{
AttributesToSkip = FileAttributes . ReparsePoint ,
RecurseSubdirectories = true
} )
2023-06-08 09:00:41 +02:00
{
ShouldIncludePredicate = ( ref FileSystemEntry entry ) = > ! entry . IsDirectory
& & entry . Length > 0
2023-10-17 10:21:02 +02:00
} . ToArray ( ) ;
2023-06-08 09:00:41 +02:00
2023-10-17 10:21:02 +02:00
foreach ( var file in files )
2023-06-08 09:00:41 +02:00
{
2023-10-17 10:21:02 +02:00
string fileName = Path . GetFileName ( file ) ;
// Look for a file with the same name in LinkToFilesFrom
// and replace it with a symbolic link if it has the same content.
string targetFile = Path . Combine ( LinkToFilesFrom , fileName ) ;
if ( File . Exists ( targetFile ) & & FilesHaveSameContent ( file , targetFile ) )
2023-06-08 09:00:41 +02:00
{
2023-10-17 10:21:02 +02:00
ReplaceByLinkTo ( file , targetFile ) ;
2023-06-08 09:00:41 +02:00
}
}
return true ;
}
private unsafe bool FilesHaveSameContent ( string path1 , string path2 )
{
using var mappedFile1 = MemoryMappedFile . CreateFromFile ( path1 , FileMode . Open ) ;
using var accessor1 = mappedFile1 . CreateViewAccessor ( ) ;
byte * ptr1 = null ;
using var mappedFile2 = MemoryMappedFile . CreateFromFile ( path2 , FileMode . Open ) ;
using var accessor2 = mappedFile2 . CreateViewAccessor ( ) ;
byte * ptr2 = null ;
try
{
accessor1 . SafeMemoryMappedViewHandle . AcquirePointer ( ref ptr1 ) ;
Span < byte > span1 = new Span < byte > ( ptr1 , checked ( ( int ) accessor1 . SafeMemoryMappedViewHandle . ByteLength ) ) ;
accessor2 . SafeMemoryMappedViewHandle . AcquirePointer ( ref ptr2 ) ;
Span < byte > span2 = new Span < byte > ( ptr2 , checked ( ( int ) accessor2 . SafeMemoryMappedViewHandle . ByteLength ) ) ;
return span1 . SequenceEqual ( span2 ) ;
}
finally
{
if ( ptr1 ! = null )
{
accessor1 . SafeMemoryMappedViewHandle . ReleasePointer ( ) ;
ptr1 = null ;
}
if ( ptr2 ! = null )
{
accessor2 . SafeMemoryMappedViewHandle . ReleasePointer ( ) ;
ptr2 = null ;
}
}
}
2023-10-17 10:21:02 +02:00
void ReplaceByLinkTo ( string path , string pathToTarget )
2023-06-08 09:00:41 +02:00
{
// To link, the target mustn't exist. Make a backup, so we can restore it when linking fails.
2023-10-17 10:21:02 +02:00
string backupFile = $"{path}.pre_link_backup" ;
File . Move ( path , backupFile ) ;
2023-06-08 09:00:41 +02:00
2023-10-17 10:21:02 +02:00
try
2023-06-08 09:00:41 +02:00
{
2023-10-17 10:21:02 +02:00
string relativePath = Path . GetRelativePath ( Path . GetDirectoryName ( path ) ! , pathToTarget ) ;
File . CreateSymbolicLink ( path , relativePath ) ;
2023-06-08 09:00:41 +02:00
2023-10-17 10:21:02 +02:00
File . Delete ( backupFile ) ;
2023-06-08 09:00:41 +02:00
2023-10-17 10:21:02 +02:00
Log . LogMessage ( MessageImportance . Normal , $"Linked '{path}' to '{relativePath}'." ) ;
2023-06-08 09:00:41 +02:00
}
2023-10-17 10:21:02 +02:00
catch ( Exception ex )
2023-06-08 09:00:41 +02:00
{
2023-10-17 10:21:02 +02:00
Log . LogError ( $"Unable to link '{path}' to '{pathToTarget}.': {ex}" ) ;
File . Move ( backupFile , path ) ;
2023-06-08 09:00:41 +02:00
2023-10-17 10:21:02 +02:00
throw ;
2023-06-08 09:00:41 +02:00
}
}
2023-06-09 11:42:50 +02:00
#endif
2023-06-08 09:00:41 +02:00
}
}