1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 
4 using System;
5 using System.Collections.Generic;
6 using System.IO;
7 using System.Text;
8 using System.Xml;
9 using Microsoft.Build.BuildEngine.Shared;
10 using Microsoft.Win32;
11 
12 namespace Microsoft.Build.BuildEngine
13 {
14     internal class VCWrapperProject
15     {
16         /// <summary>
17         /// Add a target for a VC project file into the XML doc that's being generated.
18         /// This is used only when building standalone VC projects
19         /// </summary>
20         /// <param name="msbuildProject"></param>
21         /// <param name="projectPath"></param>
22         /// <param name="targetName"></param>
23         /// <param name="subTargetName"></param>
24         /// <owner>RGoel</owner>
AddVCBuildTarget( Project msbuildProject, string projectPath, string targetName, string subTargetName )25         static private void AddVCBuildTarget
26         (
27             Project msbuildProject,
28             string projectPath,
29             string targetName,
30             string subTargetName
31         )
32         {
33             Target newTarget = msbuildProject.Targets.AddNewTarget(targetName);
34             if (subTargetName == "Publish")
35             {
36                 // Well, hmmm.  The VCBuild doesn't support any kind of
37                 // a "Publish" operation.  The best we can really do is offer up a
38                 // message saying so.
39                 SolutionWrapperProject.AddErrorWarningMessageElement(newTarget, XMakeElements.error, true, "SolutionVCProjectNoPublish");
40             }
41             else
42             {
43                 SolutionWrapperProject.AddErrorWarningMessageElement(newTarget, XMakeElements.warning, true, "StandaloneVCProjectP2PRefsBroken");
44 
45                 string projectFullPath = Path.GetFullPath(projectPath);
46                 AddVCBuildTaskElement(msbuildProject, newTarget, "$(VCBuildSolutionFile)", projectFullPath, subTargetName, "$(PlatformName)", "$(ConfigurationName)");
47             }
48         }
49 
50         /// <summary>
51         /// Adds a new VCBuild task element to the specified target
52         /// </summary>
53         /// <param name="target">The target to add the VCBuild task to</param>
54         /// <param name="solutionPath">Path to the solution if any</param>
55         /// <param name="projectPath">Path to the solution if any</param>
56         /// <param name="vcbuildTargetName">The VCBuild target name</param>
57         /// <param name="platformName">The platform parameter to VCBuild</param>
58         /// <param name="fullConfigurationName">Configuration property value</param>
59         /// <returns></returns>
AddVCBuildTaskElement( Project msbuildProject, Target target, string solutionPath, string projectPath, string vcbuildTargetName, string platformName, string fullConfigurationName )60         static internal BuildTask AddVCBuildTaskElement
61         (
62             Project msbuildProject,
63             Target target,
64             string solutionPath,
65             string projectPath,
66             string vcbuildTargetName,
67             string platformName,
68             string fullConfigurationName
69         )
70         {
71             // The VCBuild task (which we already shipped) has a bug - it cannot
72             // find vcbuild.exe when running in MSBuild 64 bit unless it's on the path.
73             // So, pass it here, unless some explicit path was passed.
74             // Note, we have to do this even if we're in a 32 bit MSBuild, because we save the .sln.cache
75             // file, and the next build of the solution could be a 64 bit MSBuild.
76 
77             if (VCBuildLocationHint != null) // Should only be null if vcbuild truly isn't installed; in that case, let the task log its error
78             {
79                 BuildTask createProperty = target.AddNewTask("CreateProperty");
80 
81                 createProperty.SetParameterValue("Value", VCBuildLocationHint);
82                 createProperty.Condition = "'$(VCBuildToolPath)' == ''";
83                 createProperty.AddOutputProperty("Value", "VCBuildToolPath");
84             }
85 
86             BuildTask newTask = target.AddNewTask("VCBuild");
87 
88             newTask.SetParameterValue("Projects", projectPath, true /* treat as literal */);
89 
90             // Add the toolpath so that the user can override if necessary
91             newTask.SetParameterValue("ToolPath", "$(VCBuildToolPath)");
92 
93             newTask.SetParameterValue("Configuration", fullConfigurationName);
94 
95             if (!string.IsNullOrEmpty(platformName))
96             {
97                 newTask.SetParameterValue("Platform", platformName);
98             }
99 
100             newTask.SetParameterValue("SolutionFile", solutionPath);
101 
102             if ((vcbuildTargetName != null) && (vcbuildTargetName.Length > 0))
103             {
104                 newTask.SetParameterValue(vcbuildTargetName, "true");
105             }
106 
107             // Add the override switch so that the user can supply one if necessary
108             newTask.SetParameterValue("Override", "$(VCBuildOverride)");
109 
110             // Add any additional lib paths
111             newTask.SetParameterValue("AdditionalLibPaths", "$(VCBuildAdditionalLibPaths)");
112 
113             // Only use new properties if we're not emitting a 2.0 project
114             if (!String.Equals(msbuildProject.ToolsVersion, "2.0", StringComparison.OrdinalIgnoreCase))
115             {
116                 // Add any additional link library paths
117                 newTask.SetParameterValue("AdditionalLinkLibraryPaths", "$(VCBuildAdditionalLinkLibraryPaths)");
118 
119                 // Add the useenv switch so that the user can supply one if necessary
120                 // Note: "VCBuildUserEnvironment" is included for backwards-compatibility; the correct
121                 // property name is "VCBuildUseEnvironment" to match the task parameter. When the old name is
122                 // used the task will emit a warning.
123                 newTask.SetParameterValue("UseEnvironment", "$(VCBuildUseEnvironment)");
124             }
125 
126             newTask.SetParameterValue("UserEnvironment", "$(VCBuildUserEnvironment)");
127 
128             // Add the additional options switches
129             newTask.SetParameterValue("AdditionalOptions", "$(VCBuildAdditionalOptions)");
130 
131             return newTask;
132         }
133 
134         /// <summary>
135         /// This method generates an XmlDocument representing an MSBuild project wrapper for a VC project
136         /// </summary>
137         /// <owner>LukaszG</owner>
GenerateVCWrapperProject(Engine parentEngine, string vcProjectFilename, string toolsVersion)138         static internal XmlDocument GenerateVCWrapperProject(Engine parentEngine, string vcProjectFilename, string toolsVersion)
139         {
140             string projectPath = Path.GetFullPath(vcProjectFilename);
141             Project msbuildProject = null;
142 
143             try
144             {
145                 msbuildProject = new Project(parentEngine, toolsVersion);
146             }
147             catch (InvalidOperationException)
148             {
149                 BuildEventFileInfo fileInfo = new BuildEventFileInfo(projectPath);
150                 string errorCode;
151                 string helpKeyword;
152                 string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "UnrecognizedToolsVersion", toolsVersion);
153                 throw new InvalidProjectFileException(projectPath, fileInfo.Line, fileInfo.Column, fileInfo.EndLine, fileInfo.EndColumn, message, null, errorCode, helpKeyword);
154             }
155 
156             msbuildProject.IsLoadedByHost = false;
157             msbuildProject.DefaultTargets = "Build";
158 
159             string wrapperProjectToolsVersion = SolutionWrapperProject.DetermineWrapperProjectToolsVersion(toolsVersion);
160             msbuildProject.DefaultToolsVersion = wrapperProjectToolsVersion;
161 
162             BuildPropertyGroup propertyGroup = msbuildProject.AddNewPropertyGroup(true /* insertAtEndOfProject = true */);
163             propertyGroup.Condition = " ('$(Configuration)' != '') and ('$(Platform)' == '') ";
164             propertyGroup.AddNewProperty("ConfigurationName", "$(Configuration)");
165 
166             propertyGroup = msbuildProject.AddNewPropertyGroup(true /* insertAtEndOfProject = true */);
167             propertyGroup.Condition = " ('$(Configuration)' != '') and ('$(Platform)' != '') ";
168             propertyGroup.AddNewProperty("ConfigurationName", "$(Configuration)|$(Platform)");
169 
170             // only use PlatformName if we only have the platform part
171             propertyGroup = msbuildProject.AddNewPropertyGroup(true /* insertAtEndOfProject = true */);
172             propertyGroup.Condition = " ('$(Configuration)' == '') and ('$(Platform)' != '') ";
173             propertyGroup.AddNewProperty("PlatformName", "$(Platform)");
174 
175             AddVCBuildTarget(msbuildProject, projectPath, "Build", null);
176             AddVCBuildTarget(msbuildProject, projectPath, "Clean", "Clean");
177             AddVCBuildTarget(msbuildProject, projectPath, "Rebuild", "Rebuild");
178             AddVCBuildTarget(msbuildProject, projectPath, "Publish", "Publish");
179 
180             // Special environment variable to allow people to see the in-memory MSBuild project generated
181             // to represent the VC project.
182             if (Environment.GetEnvironmentVariable("MSBuildEmitSolution") != null)
183             {
184                 msbuildProject.Save(vcProjectFilename + ".proj");
185             }
186 
187             return msbuildProject.XmlDocument;
188         }
189 
190         /// <summary>
191         /// Hint to give the VCBuild task to help it find vcbuild.exe.
192         /// </summary>
193         static private string path;
194 
195         /// <summary>
196         /// Hint to give the VCBuild task to help it find vcbuild.exe.
197         /// Directory in which vcbuild.exe is found.
198         /// </summary>
199         static internal string VCBuildLocationHint
200         {
201             get
202             {
203                 if (path == null)
204                 {
205                     path = GenerateFullPathToTool(RegistryView.Default);
206 
207                     if (path == null && Environment.Is64BitProcess)
208                     {
209                         path = GenerateFullPathToTool(RegistryView.Registry32);
210                     }
211 
212                     if (path != null)
213                     {
214                         path = Path.GetDirectoryName(path);
215                     }
216                 }
217 
218                 return path;
219             }
220         }
221 
222         // The code below is mostly copied from the VCBuild task that we shipped in 3.5.
223         // It is the logic it uses to find vcbuild.exe. That logic had a flaw -
224         // in 64 bit MSBuild, in a vanilla command window (like in Team Build) it would not
225         // find vcbuild.exe. We use the logic below to predict whether VCBuild will find it,
226         // and if it won't, we will pass the "hint" to use the 64 bit program files location.
227 
228         /// <summary>
229         /// constants for VS9 Pro and above SKUs
230         /// </summary>
231 
232         // root registry key for VS9
233         private const string vs9RegKey = @"SOFTWARE\Microsoft\VisualStudio\9.0";
234         // the name of the value containing disk install directory for the IDE components
235         // ("...\common7\ide" for layouts)
236         private const string vs9InstallDirValueName = "InstallDir";
237         // relative path from the above directory to vcbuild.exe on layouts
238         private const string vs9RelativePathToVCBuildLayouts = @"..\..\vc\vcpackages\vcbuild.exe";
239         // relative path from the above directory to vcbuild.exe on batch
240         private const string vs9RelativePathToVCBuildBatch = @"vcbuild.exe";
241 
242         /// <summary>
243         /// constants for the VC9 Express SKU
244         /// </summary>
245 
246         // root registry key for VC9
247         private const string vc9RegKey = @"SOFTWARE\Microsoft\VCExpress\9.0";
248         // the name of the value containing disk install directory for the IDE components
249         // ("...\common7\ide" for layouts)
250         private const string vc9InstallDirValueName = "InstallDir";
251         // relative path from the above directory to vcbuild.exe on layouts
252         private const string vc9RelativePathToVCBuildLayouts = @"..\..\vc\vcpackages\vcbuild.exe";
253         // relative path from the above directory to vcbuild.exe on batch
254         private const string vc9RelativePathToVCBuildBatch = @"vcbuild.exe";
255 
256         // name of the tool
257         private const string vcbuildName = "vcbuild.exe";
258 
259         /// <summary>
260         /// Determing the path to vcbuild.exe
261         /// </summary>
262         /// <returns>path to vcbuild.exe, or null if it's not found</returns>
GenerateFullPathToTool(RegistryView registryView)263         private static string GenerateFullPathToTool(RegistryView registryView)
264         {
265             using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, registryView))
266             {
267                 // try VS9 professional and above SKUs first
268                 string location = TryLocationFromRegistry(baseKey, vs9RegKey, vs9InstallDirValueName,
269                     vs9RelativePathToVCBuildLayouts, vs9RelativePathToVCBuildBatch);
270 
271                 if (null != location)
272                 {
273                     return location;
274                 }
275 
276                 // fall back to the VC Express SKU
277                 location = TryLocationFromRegistry(baseKey, vc9RegKey, vc9InstallDirValueName,
278                     vc9RelativePathToVCBuildLayouts, vc9RelativePathToVCBuildBatch);
279 
280                 if (null != location)
281                 {
282                     return location;
283                 }
284 
285                 // finally, try looking in the system path
286                 if (Microsoft.Build.BuildEngine.Shared.NativeMethods.FindOnPath(vcbuildName) == null)
287                 {
288                     // If SearchPath didn't find the file, it's not on the system path and we have no chance of finding it.
289                     return null;
290                 }
291 
292                 return null;
293             }
294         }
295 
296         /// <summary>
297         /// Looks up a path from the registry if present, and checks whether VCBuild.exe is there.
298         /// </summary>
299         /// <param name="subKey">Registry key to open</param>
300         /// <param name="valueName">Value under that key to read</param>
301         /// <param name="messageToLogIfNotFound">Low-pri message to log if registry key isn't found</param>
302         /// <param name="relativePathFromValueOnLayout">Relative path from the key value to vcbuild.exe for layout installs</param>
303         /// <param name="relativePathFromValueOnBatch">Relative path from the key value to vcbuild.exe for batch installs</param>
304         /// <returns>Path to vcbuild.exe, or null if it's not found</returns>
305         /// <owner>danmose</owner>
TryLocationFromRegistry(RegistryKey root, string subKeyName, string valueName, string relativePathFromValueOnLayout, string relativePathFromValueOnBatch)306         private static string TryLocationFromRegistry(RegistryKey root, string subKeyName, string valueName,
307             string relativePathFromValueOnLayout, string relativePathFromValueOnBatch)
308         {
309             using (RegistryKey subKey = root.OpenSubKey(subKeyName))
310             {
311                 if (subKey == null)
312                 {
313                     // We couldn't find an installation of the product we were looking for.
314                     return null;
315                 }
316                 else
317                 {
318                     string rootDir = (string)subKey.GetValue(valueName);
319 
320                     if (rootDir != null)
321                     {
322                         // first try the location for layouts
323                         string vcBuildPath = Path.Combine(rootDir, relativePathFromValueOnLayout);
324                         if (File.Exists(vcBuildPath))
325                         {
326                             return vcBuildPath;
327                         }
328 
329                         // if not found in layouts location, try the alternate dir if any,
330                         // which contains vcbuild for batch installs
331                         if (null != relativePathFromValueOnBatch)
332                         {
333                             vcBuildPath = Path.Combine(rootDir, relativePathFromValueOnBatch);
334                             if (File.Exists(vcBuildPath))
335                             {
336                                 return vcBuildPath;
337                             }
338                         }
339                     }
340                 }
341 
342                 // Didn't find it
343                 return null;
344             }
345         }
346     }
347 }
348