main
cs 281 lines 12.2 KB
Raw
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 }