// 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. #if !NETFRAMEWORK #nullable enable 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; #endif using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Microsoft.DotNet.Build.Tasks { /// /// Replaces files that have the same content with hard links. /// public sealed class ReplaceFilesWithSymbolicLinks : Task { /// /// The path to the directory to recursively search for files to replace with symbolic links. /// [Required] public string Directory { get; set; } = ""; /// /// The path to the directory with files to link to. /// [Required] public string LinkToFilesFrom { get; set; } = ""; #if NETFRAMEWORK public override bool Execute() { Log.LogError($"{nameof(ReplaceFilesWithSymbolicLinks)} is not supported on .NET Framework."); return false; } #else public override bool Execute() { if (OperatingSystem.IsWindows()) { Log.LogError($"{nameof(ReplaceFilesWithSymbolicLinks)} is not supported on Windows."); return false; } if (!System.IO.Directory.Exists(Directory)) { Log.LogError($"'{Directory}' does not exist."); return false; } if (!System.IO.Directory.Exists(LinkToFilesFrom)) { Log.LogError($"'{LinkToFilesFrom}' does not exist."); return false; } // Find all non-empty, non-symbolic link files. string[] files = new FileSystemEnumerable( Directory, (ref FileSystemEntry entry) => entry.ToFullPath(), new EnumerationOptions() { AttributesToSkip = FileAttributes.ReparsePoint, RecurseSubdirectories = true }) { ShouldIncludePredicate = (ref FileSystemEntry entry) => !entry.IsDirectory && entry.Length > 0 }.ToArray(); foreach (var file in files) { 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)) { ReplaceByLinkTo(file, targetFile); } } 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 span1 = new Span(ptr1, checked((int)accessor1.SafeMemoryMappedViewHandle.ByteLength)); accessor2.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr2); Span span2 = new Span(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; } } } void ReplaceByLinkTo(string path, string pathToTarget) { // To link, the target mustn't exist. Make a backup, so we can restore it when linking fails. string backupFile = $"{path}.pre_link_backup"; File.Move(path, backupFile); try { string relativePath = Path.GetRelativePath(Path.GetDirectoryName(path)!, pathToTarget); File.CreateSymbolicLink(path, relativePath); File.Delete(backupFile); Log.LogMessage(MessageImportance.Normal, $"Linked '{path}' to '{relativePath}'."); } catch (Exception ex) { Log.LogError($"Unable to link '{path}' to '{pathToTarget}.': {ex}"); File.Move(backupFile, path); throw; } } #endif } }