Converting .vcproj files to MSBuild projects – Brain Dump About eight months ago, I spent time working on a solution for a gap in MSBuild VC++ support. When you currently build a VC++ solution with MSBuild, MSBuild is really executing vcbuild.exe to do the actual work. There are no cl and link tasks or any other C++ MSBuild tasks, and C++ projects still use the old VS project format instead of relying on MSBuild. Thus, I prototyped an application that could convert VC++ projects to MSBuild projects and created cl and link tasks. Although this effort has been abandoned, my goal is to provide a brain dump that describes the issues I discovered while prototyping and how you might go about building this tool. My hope is that some interested people in the community will continue this project to fill this gap in MSBuild compatibility. This document is a cross between a specification and implementation; it provides insight into how I intended the solution to work and some implementation details. The analysis is broken down into three pieces: Comparing the .vcproj format to MSBuild, designs for the cl and link tasks, and discussion of the application that will produce the new project file. The workflow I envisioned for this tool was: A user creates a standard VC++ solution with VS 2005 that he wants to build with MSBuild. The user runs an application which converts the VC++ projects in the solution to MSBuild formatted .proj files. These project files import a C++ .targets file that relies on cl and link MSBuild tasks. Examining the .vcproj and MSBuild project layouts For the first part of the discussion, let’s compare the layout of .vcproj and MSBuild files. This will be useful when we consider how to rewrite the .vcproj file. Reviewing a VC++ project file, you’ll notice that it uses XML and the file is broken down within one huge element. Specifically, all of the data in a .vcproj file is contained within a VisualStudioProject element that starts the file. Within this element, there are nodes for Platforms, Configurations, References, Files, and Globals. To convert this into an MSBuild compatible file, we must decide how to map each node to an MSBuild equivalent. Let’s start by listing the attributes of the VisualStudioProject element and their counterpart MSBuild properties: VisualStudioProject Attributes ProjectType Version Name ProjectGUID Keyword MSBuild Properties N/A ProductVersion N/A ProjectGuid N/A While the ProductVersion and ProjectGuid properties are explicitly defined in MSBuild project files, the remaining VS attributes don’t appear by default. Instead, the ProjectType and Name attributes are defined through reserved MSBuild properties which can be accessed in the project file. The project type is implicitly defined by the suffix of the MSBuild project file (i.e. C# project files use the .csproj suffix) and is retrieved by accessing the MSBuildProjectExtension property. The project name is simply the name of the project file and is obtained through the MSBuildProjectName property. Note that I never found a mapping for Keyword; we’ll just ignore it for now. The Platforms node in a .vcproj lists the platforms that the VC++ project targets on a build. The listed platforms factor into the next block of XML, which is the list of Configurations. Each Configuration node has a name attribute, which specifies a Configuration name (such as Debug or Release) followed by a Platform (such as Win32). Below is a sample Debug|Win32 configuration from a VC++ project file1: <Configuration Name="Debug|Win32" OutputDirectory="Debug" IntermediateDirectory="Debug" ConfigurationType="1" UseOfMFC="2" CharacterSet="2"> <Tool Name="VCCLCompilerTool" Optimization="0" PreprocessorDefinitions="WIN32;_WINDOWS;_DEBUG" MinimalRebuild="TRUE" BasicRuntimeChecks="3" RuntimeLibrary="3" TreatWChar_tAsBuiltInType="TRUE" UsePrecompiledHeader="3" WarningLevel="3" Detect64BitPortabilityProblems="TRUE" DebugInformationFormat="4"/> <Tool Name="VCLinkerTool" LinkIncremental="2" GenerateDebugInformation="TRUE" SubSystem="2" TargetMachine="1"/> <Tool Name="VCPostBuildEventTool"/> <Tool Name="VCResourceCompilerTool" PreprocessorDefinitions="_DEBUG" Culture="1033" AdditionalIncludeDirectories="$(IntDir)"/> <Tool Name="VCWebServiceProxyGeneratorTool"/> </Configuration> The MSBuild approach is to arrange configuration properties in property groups; these properties are set when the Configuration and Platform match a PropertyGroup’s condition. Here’s an example from a C# project: 1 To shorten the example, some of the Tool elements that would normally be listed in a configuration element have been removed. <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\Debug\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> In this example, if $(Configuration) is set to Debug and $(Platform) is set to AnyCPU, the elements in the group are set with the specified values. Notice that, unlike the C++ sample, this group contains only a small list of properties. The list of possible tools is missing. Additionally, only some of the properties in the PropertyGroup map to VC++ Configuration attributes. For example, the OutputPath property does map to the OutputDirectory attribute of the Configuration element. However, the Optimize property doesn’t have an equivalent Configuration attribute. Instead, it maps to the Optimization attribute in the VCCLCompilerTool element. Reviewing the list of tools in the Configuration element, it’s clear that some map to real executables: VCCLCompilerTool maps to cl.exe and VCLinkerTool represents link.exe. Additionally, there are .vcproj tool elements for al.exe, lc.exe, midl.exe, bscmake.exe, and others. In the MSBuild world, each of these tools would be invoked by a separate MSBuild task and these collective tasks would be referenced in a .targets file. For the prototype, I simplified the situation by focusing solely on cl.exe and link.exe as the two tool elements I processed. For a complete solution, you would want to expand your scope to include tasks that map to the various C++ related tools. For “tools” that don’t have any mappings to real world applications, such as VCPostBuildEventTool, you’ll need to determine their MSBuild equivalents. For example, post build events in MSBuild are specified between <PostBuildEvent> elements that are defined within a PropertyGroup. After the Configuration section, there’s the References node. This is where all the assembly references for a C++ project are listed. Although I didn’t handle this in the prototype, it’s clear that a complete solution would need to include these in the MSBuild project file. To match MSBuild’s format, we’ll want to maintain individual elements for each Reference but list them together in an ItemGroup. We’ll need to change the tag from AssemblyReference to Reference and change some attribute names; for example, MSBuild specifies the assembly name with an Include attribute, not an AssemblyName attribute. The Files node has sub-nodes for source files, header files, resource files, and files not included in a build (like .txt files). These groupings can be mapped to ItemGroup elements that contain MSBuild Compile tags where the Include attribute would specify the relative path of the file. Preserving the FileConfiguration data might be accomplished using child elements of a Compile element or specifying the properties in the project wide property groups. <Files> <Filter Name="Source Files" Filter="cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx" <File RelativePath=".\stdafx.cpp" > <FileConfiguration Name="Debug|Win32" > <Tool Name="VCCLCompilerTool" UsePrecompiledHeader="1"/> </FileConfiguration> So, we’ve now taken a quick look at the nodes in .vcproj files and we’ve considered what their equivalent elements could be in an MSBuild project. For the initial prototype, I kept my sample .vcproj files very simple so I could focus on compiling and linking. Designing cl and link MSBuild tasks If you review the csc and vbc tasks, you’ll see that they’re invoked with a single XML element that contains a list of attributes. These attributes mostly translate to flags used by the tool. I designed my cl and link tasks similarly: <cl Sources="@(FilesToCompile)" AdditionalIncludeDirectories="$(CLAdditionalIncludeDirectories)" AdditionalOptions="$(CLAdditionalOptions)" AdditionalUsingDirectories="$(CLAdditionalUsingDirectories)" ... WarnAsError="$(CLWarnAsError)" WarningLevel="$(CLWarningLevel)" WholeProgramOptimization="$(CLWholeProgramOptimization)" IntermediateDirectory="$(IntDir)"> <Output TaskParameter="ObjectFiles" ItemName="Objects" /> </cl> <link Sources="@(Objects)" AdditionalDependencies="$(LinkAdditionalDependencies)" AdditionalOptions="$(LinkAdditionalOptions)" AssemblyDebug="$(LinkAssemblyDebug)" ... Subsystem="$(LinkSubsystem)" TargetMachine="$(LinkTargetMachine)" Version="$(LinkVersion)" /> Here, each attribute corresponds to a cl.exe or link.exe flag. A list of C++ compiler flags is at http://msdn2.microsoft.com/en-us/library/19z1t1wy(VS.80).aspx and a list of link.exe flags is available at http://msdn2.microsoft.com/enus/library/y0zzbyt4(VS.80).aspx. Some things to note are that the cl and link properties are all prepended with CL and Link respectively. This was done to resolve situations where properties would have the same name for multiple tasks. For example, both tasks have an AdditionalOptions attribute and, subsequently, could have an AdditionalOptions property. Prepending CL and Link distinguishes which AdditionalOptions property applies to which task. The Sources attributes for the cl and link tasks are assigned item groups that contain the list of source files and the list of produced .obj files, respectively. The list of object files is obtained by using the Output element in the cl task. This element ties the cl task’s ObjectFiles output property to an MSBuild item named Objects. The ObjectFiles parameter provides the list of objects produced by cl.exe. By assigning the list to Objects, the link task can reference Objects as an input. Since a tool’s flags will be specified through task properties, we need to determine the granularity of those properties. Consider the many optimization flags: /O1, /O2, /Ob, /Od, /Og, /Oi, /Os, /Ot, /Ox, and /Oy. We want to represent these 10 flags with some cl properties. A gut reaction may be to have individual properties for each of them but that’s not the most efficient or natural. For example, the /Os (favors small code) and /Ot (favors fast code) compiler flags both affect optimization. However, you wouldn’t set both independently since you can either favor small code or fast code, but not both. One option is to use a property like FavorSizeOverSpeed that adds /Os to the compiler line if set to true or adds /Ot otherwise. Later, I’ll discuss a more systematic way to determine which properties these tasks should have. To write a task for cl or link, we’ll need to create classes that represent each tool. MSBuild provides a very convenient interface for this, known as a ToolTask. The purpose of a ToolTask is to provide functionality for a task that wraps a command line tool. As demonstrated above, the properties of a tool task can be used to set flags for the wrapped tools. MSBuild also provides helper classes for building a command line to invoke a tool and logging output from the tool. More information on MSBuild ToolTasks is available here. For the sample cl and link tasks I wrote, I found most of the work involved storing data regarding the flags to set since both cl and link have tons of flags. One approach is to use a table where you relate the task’s property name to a corresponding flag. If the property is set, you parse its value and add the appropriate flag to the table. When you’re ready to build the command line, you can simply run through the table values. However, you’ll want to maintain the order of flags on the command line. This makes it easier to verify that an upgraded version of the task hasn’t altered the way the underlying tool is executed since the flag order would be consistent. After building the command line, you’ll want to pass the string of flags to the responseFileCommands argument of the base ExecuteTool method. This allows you to avoid issues with the command line being too long. Converting the .vcproj file We need a way to actually read the .vcproj file and produce a new project file that MSBuild can understand. The new project file can then import our .targets files which use our VC++ tasks. For the prototype, I created a console application that could perform the conversion. Reading a project file is pretty easy to do using the interfaces in the VCProject and VCProjectEngine namespaces; these interfaces expose the contents of .vcproj files. To load a project: VCProjectEngine vcprojEngine = new VCProjectEngineObject(); VCProject vcproj = (VCProject)vcprojEngine.LoadProject(projectFileName); Here, we’re creating a VCProjectEngine and then loading a .vcproj file. In the prototype, projectFileName was supplied by the user on the command line. With the project loaded, we can use the properties of the VCProject object to obtain the various elements in the .vcproj file. For example, to obtain a list of files under the Source Files filter, you can do: List<VCFile> files = new List<VCFile>(); IVCCollection fileCollection = (IVCCollection)vcproj.Files; foreach (VCFile vcFile in fileCollection) { if (vcFile.Parent is VCFilter) { if (((VCFilter)(vcFile.Parent)).Name.Equals(“Source Files”)) { files.Add(vcFile); } } } Here, we’ve used the project’s Files field to obtain an IVCCollection. An IVCCollection resides in the VCProjectEngine namespace; it’s different from other collections since it contains a reference to the VCProjectEngine. A VCFile object describes the operations that can take place on a file in the .vcproj. Here, we iterate over the collection of VCFile objects and for those that have a parent that’s a VCFilter, we check if the filter’s name is Source Files. In that case, we add the vcFile object to our collection of files. With a VCProject object, you can obtain collections for all the data in a VC++ project: Configurations, Files, Filters, Platforms, and References. Additionally, a VCProject object has properties that specify the project’s name, its GUID, the project directory’s name, the root namespace, and more. These are some of the properties we’re interested in for our new project’s property group. To pick up the tools, we need to start by processing each configuration since the same tool can have different attributes in each configuration. Similar to above, you can iterate over a list of configurations using: foreach (VCConfiguraton vcConfig in ((IVCCollection)vcproj.Configurations)) VCConfiguration objects have fields that specify the configuration name (vcConfig.Name) and access the associated VCPlatform object (vcConfig.Platform). Using the platform object, you can obtain the platform name (((VCPlatform)vcConfig.Platform).Name). To get the lists of tools associated with a configuration, obtain the collection from the Tools field (vcConfig.Tools). Since there’s a defined list of tools, it’s easiest to just specify the tool you want from the collection. For cl.exe: vcCompilerTool = (VCCLCompilerTool)(vcTools.Item(“VCCLCompilerTool”)); Similarly for link.exe: vcLinkerTool = (VCLinkerTool)(vcTools.Item(“VCLinkerTool”)); In both examples, vcTools is an IVCCollection from vcConfig.Tools. With the VCCLCompilerTool and VCLinkerTool objects, it’s now possible to access the related attributes in the .vcproj file. This enables us to take the data from the .vcproj file and write it out to a new project file. Once our converter application has stored all the .vcproj data, it’s time to produce an MSBuild project file that also contains this data. This is where we’ll tie the attributes of the cl and link tasks to the fields in the corresponding tool objects. Before that, however, let’s begin by producing the initial properties for the project file. Since I didn’t find any classes like VCProjectEngine for MSBuild, I chose to generate the project file with an XmlTextWriter object. To make the XML easier to read, I set the writer’s formatting field to Formatting.Indented. Note that I’m writing the project properties and the actual tasks in the project files. Typically, the tasks would be in their own .targets file(s) but my approach was more convenient for testing since I could programmatically reflect changes to task attributes instead of needing to manually edit the .targets files. As I describe the elements I wrote to the project file, I’ll provide sample code so you can see which fields I used to obtain the data. The first step is to write out the required MSBuild header, which is present in any MSBuild project file: xmlw.WriteStartElement("Project"); xmlw.WriteAttributeString("xmlns", "http://schemas.microsoft.com/developer/msbuild/2003"); The next step is to add UsingTask elements to specify the tasks being used and the assembly file they’re in. xmlw.WriteStartElement("UsingTask"); xmlw.WriteAttributeString("TaskName", "cl"); xmlw.WriteAttributeString("AssemblyFile", "VCProjTasks.dll"); xmlw.WriteEndElement(); xmlw.WriteStartElement("UsingTask"); xmlw.WriteAttributeString("TaskName", "link"); xmlw.WriteAttributeString("AssemblyFile", "VCProjTasks.dll"); xmlw.WriteEndElement(); Following this is a PropertyGroup for the properties common to the entire project. This is where we’ll list the Product GUID, the project name, the root namespace, and the schema version: xmlw.WriteStartElement("PropertyGroup"); xmlw.WriteElementString("ProjectGuid", vcproj.ProjectGUID); xmlw.WriteElementString("ProjectName", vcproj.Name); xmlw.WriteElementString("RootNamespace", vcproj.RootNamespace); xmlw.WriteElementString("SchemaVersion", Environment.Version.ToString(2)); xmlw.WriteEndElement(); Next, add an MSBuild ItemGroup that specifies the files to be compiled: xmlw.WriteStartElement("ItemGroup"); foreach (VCFile vcf in files) { xmlw.WriteStartElement("FilesToCompile"); xmlw.WriteAttributeString("Include", vcf.Name); xmlw.WriteEndElement(); } xmlw.WriteEndElement(); At this point, we’re ready to write the build configuration properties. To start, we need to print the PropertyGroup element, but this time with an attribute that tests a condition for the configuration’s name and platform. Next, we’ll write configuration specific data that’s not tied to a tool. Using the Configurations field from our VCProject object, we’ll examine each VCConfiguration. The VCConfiguration object has fields for the IntermediateDirectory, the OutputDirectory, and the output path using PrimaryOutput. We can use the configuration’s Tools collection to obtain references to VCCLCompilerTool and VCLinkerTool. With these references, we’ll begin writing cl and link specific properties to the file. Additionally, we can use the fields of these objects to drive the attributes we need for the cl and link tasks. Specifically, we’ll have an attribute for each property we write in the project file. For writing the properties, I created helper methods that invoked WriteElementString. For the cl task, the method looked like: private void WriteCLProperty(string property, string value) { // Don't print properties with no values. if (!String.IsNullOrEmpty(value)) { xmlw.WriteElementString("CL" + property, value); } } For each cl property I wanted to track, I’d invoke this method specifying the property name and the field of the VCCLCompilerTool that contained its value. For example: WriteCLProperty("AdditionalIncludeDirectories", vcCompilerTool.AdditionalIncludeDirectories); AdditionalIncludeDirectories is a simple example since I’m literally printing the value of a string. Similarly, for boolean fields, I invoked ToString() after the field to obtain a true or false string. There are some more complicated situations which require additional work. One example is with the ExceptionHandling field, which provides a value from an enumeration that contains the possible values. To handle this scenario, I cast the value of ExceptionHandling to an Int32. With this integer, I indexed a string array, excepHandlingParamMap, of easily readable2 string representations of the different options. The cl task can then convert the string into the appropriate flag. For example: static readonly string[] excepHandlingParamMap = { "Disabled", "CPP", "ASync" }; WriteCLProperty("ExceptionHandling", GetValueString((Int32)vcclTool.ExceptionHandling, excepHandlingParamMap)); GetValueString returns the appropriate value from the array given the array and the index. I used a helper method to ensure the index is never out of bounds for the array. The Linker flags followed a similar pattern and once all the properties had been written, I had a property group which looked like: <PropertyGroup Condition="'$(ConfigurationName)|$(PlatformName)' == 'Debug|Win32'"> <ProjectDir>c:\Documents and Settings\clichten\My Documents\Visual Studio 2005\CPXSolutions\Investigations\VCP2MSB\bin\Debug\ </ProjectDir> <IntDir>$(ConfigurationName)</IntDir> <OutDir>$(ProjectDir)$(ConfigurationName)</OutDir> <TargetDir>c:\documents and settings\clichten\my documents\visual studio 2005\cpxsolutions\investigations\vcp2msb\bin\debug\ </TargetDir> <TargetFileName>TestVCProj.exe</TargetFileName> <TargetName>TestVCProj</TargetName> <CLAnalyze>False</CLAnalyze> <CLBufferSecurityCheck>True</CLBufferSecurityCheck> <CLCallingConvention>CDecl</CLCallingConvention> <CLCompileAsManaged>true</CLCompileAsManaged> ... <LinkSetChecksum>False</LinkSetChecksum> <LinkShowProgress>Disabled</LinkShowProgress> <LinkSubsystem>NotSet</LinkSubsystem> 2 Readability is important since MSBuild project files should be human readable. <LinkTargetMachine>X86</LinkTargetMachine> </PropertyGroup> These property groups should be created for all relevant build configurations. Finally, we need to write out a target element which will invoke our tasks. For the prototype, I created a target named BuildVCProj that invoked the cl and link tasks: xmlw.WriteStartElement("Target"); xmlw.WriteAttributeString("Name", "BuildVCProj"); { WriteCLTask(); WriteLinkTask(); } xmlw.WriteEndElement(); WriteCLTask and WriteLinkTask print the actual cl and link tasks, along with assigning attributes to the matching property. The task output matches the cl and link tasks that I presented above. With all these pieces, you can convert a .vcproj file to a MSBuild project file and build that project using your cl and link tasks. The next step would be to add additional tasks for other VC++ tools to cover additional build scenarios.