| File: AssignTargetPath.cs | Web Access |
| Project: src\msbuild\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Utilities; #nullable disable namespace Microsoft.Build.Tasks { /// <summary> /// Create a new list of items that have <TargetPath> attributes if none was present in /// the input. /// </summary> [MSBuildMultiThreadableTask] public class AssignTargetPath : TaskExtension, IMultiThreadableTask { #region Properties /// <summary> /// Gets or sets the task execution environment for thread-safe path resolution. /// </summary> public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; /// <summary> /// The folder to make the links relative to. /// </summary> [Required] public string RootFolder { get; set; } /// <summary> /// The incoming list of files. /// </summary> public ITaskItem[] Files { get; set; } = Array.Empty<ITaskItem>(); /// <summary> /// The resulting list of files. /// </summary> /// <value></value> [Output] public ITaskItem[] AssignedFiles { get; private set; } #endregion /// <summary> /// Execute the task. /// </summary> /// <returns></returns> public override bool Execute() { AssignedFiles = new ITaskItem[Files.Length]; if (Files.Length > 0) { AbsolutePath fullRootPath = default; string fullRootPathString = null; bool isRootFolderSameAsCurrentDirectory; if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_5)) { // Compose a file in the root folder. // NOTE: at this point fullRootPath may or may not have a trailing slash // Ensure trailing slash otherwise c:\bin appears to match part of c:\bin2\foo // Also ensure that relative segments in the path are resolved. fullRootPath = TaskEnvironment.GetAbsolutePath(FileUtilities.EnsureTrailingSlash(RootFolder)).GetCanonicalForm(); // Ensure trailing slash for comparison. Current directory is already canonical, so we don't need to call GetCanonicalForm on it. AbsolutePath currentDirectory = FileUtilities.EnsureTrailingSlash(TaskEnvironment.ProjectDirectory); // Check if the root folder is the same as the current directory - AbsolutePath handles OS-aware case sensitivity. isRootFolderSameAsCurrentDirectory = fullRootPath == currentDirectory; } else { // Compose a file in the root folder. // NOTE: at this point fullRootPath may or may not have a trailing slash // Ensure trailing slash otherwise c:\bin appears to match part of c:\bin2\foo // Also ensure that relative segments in the path are resolved and throw on illegal characters in Path.GetFullPath to preserve pre-existing behavior. fullRootPathString = #pragma warning disable MSBuildTask0002 // Path is already absolute from TaskEnvironment.GetAbsolutePath; GetFullPath only canonicalizes. Guarded by ChangeWave. Path.GetFullPath(TaskEnvironment.GetAbsolutePath(FileUtilities.EnsureTrailingSlash(RootFolder))); #pragma warning restore MSBuildTask0002 // Ensure trailing slash for comparison. Current directory is already canonical, so we don't need to call GetCanonicalForm on it. AbsolutePath currentDirectory = FileUtilities.EnsureTrailingSlash(TaskEnvironment.ProjectDirectory); // Check if the root folder is the same as the current directory. // Perform a case-insensitive comparison to match Path.GetFullPath behavior on Windows, even on case-sensitive file systems, // since that's what we're using to determine whether to preserve relative paths or not. isRootFolderSameAsCurrentDirectory = String.Compare(fullRootPathString, currentDirectory, StringComparison.OrdinalIgnoreCase) == 0; } for (int i = 0; i < Files.Length; ++i) { AssignedFiles[i] = new TaskItem(Files[i]); // If TargetPath is already set, it takes priority. // https://github.com/dotnet/msbuild/issues/2795 string targetPath = Files[i].GetMetadata(ItemMetadataNames.targetPath); // If TargetPath not already set, fall back to default behavior. if (string.IsNullOrEmpty(targetPath)) { targetPath = Files[i].GetMetadata(ItemMetadataNames.link); } if (string.IsNullOrEmpty(targetPath)) { if (// if the file path is relative !Path.IsPathRooted(Files[i].ItemSpec) && // if the file path doesn't contain any relative specifiers !Files[i].ItemSpec.Contains("." + Path.DirectorySeparatorChar) && // if the file path is already relative to the root folder isRootFolderSameAsCurrentDirectory) { // then just use the file path as-is // PERF NOTE: we do this to avoid calling Path.GetFullPath() below (which is called by GetCanonicalForm method), // because that method consumes a lot of memory, esp. when we have a lot of items coming through this task targetPath = Files[i].ItemSpec; } else { if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_5)) { AbsolutePath itemSpecFullFileNamePath = TaskEnvironment.GetAbsolutePath(Files[i].ItemSpec).GetCanonicalForm(); if (itemSpecFullFileNamePath.Value.StartsWith(fullRootPath.Value, FileUtilities.PathComparison)) { // The item spec file is in the "cone" of the RootFolder. Return the relative path from the cone root. targetPath = itemSpecFullFileNamePath.Value.Substring(fullRootPath.Value.Length); } else { // The item spec file is not in the "cone" of the RootFolder. Return the filename only. targetPath = Path.GetFileName(Files[i].ItemSpec); } } else { string itemSpecFullFileNamePath = #pragma warning disable MSBuildTask0002 // Path is already absolute from TaskEnvironment.GetAbsolutePath; GetFullPath only canonicalizes. Guarded by ChangeWave. Path.GetFullPath(TaskEnvironment.GetAbsolutePath(Files[i].ItemSpec)); #pragma warning restore MSBuildTask0002 if (String.Compare(fullRootPathString, 0, itemSpecFullFileNamePath, 0, fullRootPathString.Length, StringComparison.CurrentCultureIgnoreCase) == 0) { // The item spec file is in the "cone" of the RootFolder. Return the relative path from the cone root. targetPath = itemSpecFullFileNamePath.Substring(fullRootPathString.Length); } else { // The item spec file is not in the "cone" of the RootFolder. Return the filename only. targetPath = Path.GetFileName(Files[i].ItemSpec); } } } } AssignedFiles[i].SetMetadata(ItemMetadataNames.targetPath, EscapingUtilities.Escape(targetPath)); } } return true; } } }