| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. |
| 2 | |
| 3 | namespace WixToolset.BuildTasks |
| 4 | { |
| 5 | using System; |
| 6 | using System.Collections.Generic; |
| 7 | using System.IO; |
| 8 | using System.Linq; |
| 9 | using Microsoft.Build.Framework; |
| 10 | using Microsoft.Build.Utilities; |
| 11 | |
| 12 | /// <summary> |
| 13 | /// MSBuild task to create a list of preprocessor defines and bind paths from resolved |
| 14 | /// project references. |
| 15 | /// </summary> |
| 16 | public sealed class CreateProjectReferenceDefineConstantsAndBindPaths : Task |
| 17 | { |
| 18 | private static readonly string DirectorySeparatorString = Path.DirectorySeparatorChar.ToString(); |
| 19 | |
| 20 | [Required] |
| 21 | public ITaskItem[] ResolvedProjectReferences { get; set; } |
| 22 | |
| 23 | public ITaskItem[] ProjectConfigurations { get; set; } |
| 24 | |
| 25 | [Output] |
| 26 | public ITaskItem[] BindPaths { get; private set; } |
| 27 | |
| 28 | [Output] |
| 29 | public ITaskItem[] DefineConstants { get; private set; } |
| 30 | |
| 31 | public override bool Execute() |
| 32 | { |
| 33 | var bindPaths = new Dictionary<string, List<ITaskItem>>(StringComparer.OrdinalIgnoreCase); |
| 34 | var defineConstants = new SortedDictionary<string, string>(); |
| 35 | |
| 36 | foreach (var resolvedReference in this.ResolvedProjectReferences) |
| 37 | { |
| 38 | this.AddBindPathsForResolvedReference(bindPaths, resolvedReference); |
| 39 | |
| 40 | this.AddDefineConstantsForResolvedReference(defineConstants, resolvedReference); |
| 41 | } |
| 42 | |
| 43 | this.BindPaths = bindPaths.Values.SelectMany(bp => bp).ToArray(); |
| 44 | this.DefineConstants = defineConstants.Select(define => new TaskItem(define.Key + "=" + define.Value)).ToArray(); |
| 45 | |
| 46 | return true; |
| 47 | } |
| 48 | |
| 49 | private void AddBindPathsForResolvedReference(IDictionary<string, List<ITaskItem>> bindPathByPaths, ITaskItem resolvedReference) |
| 50 | { |
| 51 | var projectPath = resolvedReference.GetMetadata("MSBuildSourceProjectFile"); |
| 52 | |
| 53 | // If the BindName was not explicitly provided, try to use the source project's filename |
| 54 | // as the bind name. |
| 55 | var name = resolvedReference.GetMetadata("BindName"); |
| 56 | if (String.IsNullOrWhiteSpace(name)) |
| 57 | { |
| 58 | name = String.IsNullOrWhiteSpace(projectPath) ? String.Empty : Path.GetFileNameWithoutExtension(projectPath); |
| 59 | } |
| 60 | |
| 61 | var path = resolvedReference.GetMetadata("BindPath"); |
| 62 | if (String.IsNullOrWhiteSpace(path)) |
| 63 | { |
| 64 | var fullpath = resolvedReference.GetMetadata("FullPath"); |
| 65 | path = Path.GetDirectoryName(fullpath); |
| 66 | |
| 67 | // If the resolved project reference (incorrectly) points at a file then use the file's |
| 68 | // file's directory instead. When this happens, it is a problem in the referenced project |
| 69 | // |
| 70 | while (File.Exists(path)) |
| 71 | { |
| 72 | this.Log.LogWarning("The project '{0}' target path '{1}' resolved to an invalid path where a filename was a child of a file. Using the parent directory '{2}' instead.", projectPath, fullpath, path); |
| 73 | |
| 74 | path = Path.GetDirectoryName(path); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | if (!bindPathByPaths.TryGetValue(path, out var bindPathsForPath) || |
| 79 | !bindPathsForPath.Any(bp => bp.GetMetadata("BindName").Equals(name, StringComparison.OrdinalIgnoreCase))) |
| 80 | { |
| 81 | if (bindPathsForPath == null) |
| 82 | { |
| 83 | bindPathsForPath = new List<ITaskItem> |
| 84 | { |
| 85 | new TaskItem(path) |
| 86 | }; |
| 87 | |
| 88 | bindPathByPaths.Add(path, bindPathsForPath); |
| 89 | } |
| 90 | |
| 91 | if (!String.IsNullOrWhiteSpace(name)) |
| 92 | { |
| 93 | var metadata = new Dictionary<string, string> { ["BindName"] = name }; |
| 94 | bindPathsForPath.Add(new TaskItem(path, metadata)); |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | private void AddDefineConstantsForResolvedReference(IDictionary<string, string> defineConstants, ITaskItem resolvedReference) |
| 100 | { |
| 101 | var configuration = resolvedReference.GetMetadata("Configuration"); |
| 102 | var fullConfiguration = resolvedReference.GetMetadata("FullConfiguration"); |
| 103 | var platform = resolvedReference.GetMetadata("Platform"); |
| 104 | |
| 105 | var projectPath = resolvedReference.GetMetadata("MSBuildSourceProjectFile"); |
| 106 | var projectDir = Path.GetDirectoryName(projectPath) + Path.DirectorySeparatorChar; |
| 107 | var projectExt = Path.GetExtension(projectPath); |
| 108 | var projectFileName = Path.GetFileName(projectPath); |
| 109 | var projectName = Path.GetFileNameWithoutExtension(projectPath); |
| 110 | |
| 111 | var referenceName = ToolsCommon.CreateIdentifierFromValue(ToolsCommon.GetMetadataOrDefault(resolvedReference, "Name", projectName)); |
| 112 | |
| 113 | var targetPath = resolvedReference.GetMetadata("FullPath"); |
| 114 | var targetDir = Path.GetDirectoryName(targetPath) + Path.DirectorySeparatorChar; |
| 115 | var targetExt = Path.GetExtension(targetPath); |
| 116 | var targetFileName = Path.GetFileName(targetPath); |
| 117 | var targetName = Path.GetFileNameWithoutExtension(targetPath); |
| 118 | |
| 119 | // If there is no configuration metadata on the project reference task item, |
| 120 | // check for any additional configuration data provided in the optional task property. |
| 121 | if (String.IsNullOrWhiteSpace(fullConfiguration)) |
| 122 | { |
| 123 | fullConfiguration = this.FindProjectConfiguration(projectName); |
| 124 | if (!String.IsNullOrWhiteSpace(fullConfiguration)) |
| 125 | { |
| 126 | var typeAndPlatform = fullConfiguration.Split('|'); |
| 127 | configuration = typeAndPlatform[0]; |
| 128 | platform = (typeAndPlatform.Length > 1 ? typeAndPlatform[1] : String.Empty); |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | // write out the platform/configuration defines |
| 133 | defineConstants[referenceName + ".Configuration"] = configuration; |
| 134 | defineConstants[referenceName + ".FullConfiguration"] = fullConfiguration; |
| 135 | defineConstants[referenceName + ".Platform"] = platform; |
| 136 | |
| 137 | // write out the ProjectX defines |
| 138 | defineConstants[referenceName + ".ProjectDir"] = projectDir; |
| 139 | defineConstants[referenceName + ".ProjectExt"] = projectExt; |
| 140 | defineConstants[referenceName + ".ProjectFileName"] = projectFileName; |
| 141 | defineConstants[referenceName + ".ProjectName"] = projectName; |
| 142 | defineConstants[referenceName + ".ProjectPath"] = projectPath; |
| 143 | |
| 144 | // write out the TargetX defines |
| 145 | var targetDirDefine = referenceName + ".TargetDir"; |
| 146 | if (defineConstants.ContainsKey(targetDirDefine)) |
| 147 | { |
| 148 | //if target dir was already defined, redefine it as the common root shared by multiple references from the same project |
| 149 | var commonDir = FindCommonRoot(targetDir, defineConstants[targetDirDefine]); |
| 150 | if (!String.IsNullOrEmpty(commonDir)) |
| 151 | { |
| 152 | targetDir = commonDir; |
| 153 | } |
| 154 | } |
| 155 | defineConstants[targetDirDefine] = CreateProjectReferenceDefineConstantsAndBindPaths.EnsureEndsWithBackslash(targetDir); |
| 156 | |
| 157 | defineConstants[referenceName + ".TargetExt"] = targetExt; |
| 158 | defineConstants[referenceName + ".TargetFileName"] = targetFileName; |
| 159 | defineConstants[referenceName + ".TargetName"] = targetName; |
| 160 | |
| 161 | // If target path was already defined, append to it creating a list of multiple references from the same project |
| 162 | var targetPathDefine = referenceName + ".TargetPath"; |
| 163 | if (defineConstants.TryGetValue(targetPathDefine, out var oldTargetPath)) |
| 164 | { |
| 165 | if (!targetPath.Equals(oldTargetPath, StringComparison.OrdinalIgnoreCase)) |
| 166 | { |
| 167 | defineConstants[targetPathDefine] += "%3B" + targetPath; |
| 168 | } |
| 169 | |
| 170 | // If there was only one targetpath we need to create its culture specific define |
| 171 | if (!oldTargetPath.Contains("%3B")) |
| 172 | { |
| 173 | var oldSubFolder = FindSubfolder(oldTargetPath, targetDir, targetFileName); |
| 174 | if (!String.IsNullOrEmpty(oldSubFolder)) |
| 175 | { |
| 176 | defineConstants[referenceName + "." + ToolsCommon.CreateIdentifierFromValue(oldSubFolder) + ".TargetPath"] = oldTargetPath; |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | // Create a culture specific define |
| 181 | var subFolder = FindSubfolder(targetPath, targetDir, targetFileName); |
| 182 | if (!String.IsNullOrEmpty(subFolder)) |
| 183 | { |
| 184 | defineConstants[referenceName + "." + ToolsCommon.CreateIdentifierFromValue(subFolder) + ".TargetPath"] = targetPath; |
| 185 | } |
| 186 | } |
| 187 | else |
| 188 | { |
| 189 | defineConstants[targetPathDefine] = targetPath; |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | /// <summary> |
| 194 | /// Look through the configuration data in the ProjectConfigurations property |
| 195 | /// to find the configuration for a project, if available. |
| 196 | /// </summary> |
| 197 | /// <param name="projectName">Name of the project that is being searched for.</param> |
| 198 | /// <returns>Full configuration spec, for example "Release|Win32".</returns> |
| 199 | private string FindProjectConfiguration(string projectName) |
| 200 | { |
| 201 | var configuration = String.Empty; |
| 202 | |
| 203 | if (this.ProjectConfigurations != null) |
| 204 | { |
| 205 | foreach (var configItem in this.ProjectConfigurations) |
| 206 | { |
| 207 | var configProject = configItem.ItemSpec; |
| 208 | if (configProject.Length > projectName.Length && |
| 209 | configProject.StartsWith(projectName) && |
| 210 | configProject[projectName.Length] == '=') |
| 211 | { |
| 212 | configuration = configProject.Substring(projectName.Length + 1); |
| 213 | break; |
| 214 | } |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | return configuration; |
| 219 | } |
| 220 | |
| 221 | /// <summary> |
| 222 | /// Finds the common root between two paths |
| 223 | /// </summary> |
| 224 | /// <param name="path1"></param> |
| 225 | /// <param name="path2"></param> |
| 226 | /// <returns>common root on success, empty string on failure</returns> |
| 227 | private static string FindCommonRoot(string path1, string path2) |
| 228 | { |
| 229 | path1 = path1.TrimEnd(Path.DirectorySeparatorChar); |
| 230 | path2 = path2.TrimEnd(Path.DirectorySeparatorChar); |
| 231 | |
| 232 | while (!String.IsNullOrEmpty(path1)) |
| 233 | { |
| 234 | for (var searchPath = path2; !String.IsNullOrEmpty(searchPath); searchPath = Path.GetDirectoryName(searchPath)) |
| 235 | { |
| 236 | if (path1.Equals(searchPath, StringComparison.OrdinalIgnoreCase)) |
| 237 | { |
| 238 | return searchPath; |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | path1 = Path.GetDirectoryName(path1); |
| 243 | } |
| 244 | |
| 245 | return path1; |
| 246 | } |
| 247 | |
| 248 | /// <summary> |
| 249 | /// Finds the subfolder of a path, excluding a root and filename. |
| 250 | /// </summary> |
| 251 | /// <param name="path">Path to examine</param> |
| 252 | /// <param name="rootPath">Root that must be present </param> |
| 253 | /// <param name="fileName"></param> |
| 254 | /// <returns></returns> |
| 255 | private static string FindSubfolder(string path, string rootPath, string fileName) |
| 256 | { |
| 257 | if (Path.GetFileName(path).Equals(fileName, StringComparison.OrdinalIgnoreCase)) |
| 258 | { |
| 259 | path = Path.GetDirectoryName(path); |
| 260 | } |
| 261 | |
| 262 | if (path.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase)) |
| 263 | { |
| 264 | // cut out the root and return the subpath |
| 265 | return path.Substring(rootPath.Length).Trim(Path.DirectorySeparatorChar); |
| 266 | } |
| 267 | |
| 268 | return String.Empty; |
| 269 | } |
| 270 | |
| 271 | private static string EnsureEndsWithBackslash(string dir) |
| 272 | { |
| 273 | if (!dir.EndsWith(DirectorySeparatorString)) |
| 274 | { |
| 275 | dir += Path.DirectorySeparatorChar; |
| 276 | } |
| 277 | |
| 278 | return dir; |
| 279 | } |
| 280 | } |
| 281 | } |