Skip to content

Instantly share code, notes, and snippets.

@retorillo
Last active April 23, 2022 07:11
Show Gist options
  • Save retorillo/c55148f1f4cbc68f8572f70a9b949b62 to your computer and use it in GitHub Desktop.
Save retorillo/c55148f1f4cbc68f8572f70a9b949b62 to your computer and use it in GitHub Desktop.

MsBuild exploration for C++

Three key files

The following three XMLs are key, and I recommend to import them always to take benefit to pre-defined Visual Studio tasks.

  • $(VCTargetsPath)\Microsoft.Cpp.default.props
  • $(VCTargetsPath)\Microsoft.Cpp.props
  • $(VCTargetsPath)\Microsoft.Cpp.targets

Propery

Property (=? enviroment variable) can be set by using PropertyGroup.

Metadata

Metadata can be defined and set by using following elements:

Variable Expansion

MSBuild supports the following three expansion for variable/metadata.

$()

Use to expand property/variable. $(PATH) to expands to PATH variable.

@()

Use to expand metadata item list to comma separated path sequence.

<ItemGroup>
  <ClCompile Include="a.cpp;b.cpp" />
  <ClCompile Include="d.cpp" />
</ItemGroup>
<PropertyGroup>
  <ClCompileList>@(ClCompile)</ClCompileList>
</PropertyGroup>

On the above example, ClCompileList property will be a.cpp;b.cpp;d.cpp.

%()

The following project will print xml files on project directory.

<Project DefaultTargets="PrintXmlFiles" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <XmlFiles Include="*.xml" />
  </ItemGroup>
  <Target Name="PrintXmlFiles">
    <Exec Command="echo %(XmlFiles.Identity)" />
  </Target>
</Project>

In contrast with comma separeted @(), run command for each of files as follows:

  echo app.xml
  app.xml
  echo build.xml
  build.xml
  echo config.xml
  config.xml
  echo test.xml
  test.xml

Identity is a well-known item metadata described at https://msdn.microsoft.com/en-us/library/ms164313.aspx

Task

Reference of available metadata

Metadata of CL Task (ClCompile element) and Link Task are defined and described at:

  • $(VCTargetsPath)\1033\cl.xml
  • $(VCTargetsPath)\1033\link.xml

For example, PrecompiledHeader metadata can be used as follow:

<ItemGroup>
  <ClCompile>
    <PrecompiledHeader>Create</PrecompiledHeader>
  </ClCompile>
</ItemGroup>

UsingTask

For some tasks, must use UsingTask on your project XML.

Sequences of UsingTask for C++ are defined at $(VCTargetsPath)\Microsoft.CppCommon.targets (Import $(VCTargetsPath)\Microsoft.Cpp.targets

<!-- $(VCTargetsPath)\Microsoft.CppCommon.targets -->
<UsingTask TaskName="VCMessage"     AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="LIB"           AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="MIDL"          AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="RC"            AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="Mt"            AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="XSD"           AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="XDCMake"       AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="BscMake"       AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="CustomBuild"   AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="CL"            AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="Link"          AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="FXC"           AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>
<UsingTask TaskName="MultiToolTask" AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"/>

Using strictly defined Platform names

Platform name will be manually supplied by developer, and its identifier not unique and case-insensitive. (eg. Win32 and AnyCPU can be thought as aliases for x86), so directly using it to the conditional diverging is dangerous.

Using alternatively-defined property is a one of solution of this. PlatformTarget will be strictly defined at $(VCTargetsPath)\Platforms\*\Platform.default.props.

  • x86 (Lower-case)
  • x64 (Lower-case)
  • ARM (Upper-case)

Note that they are avaiable after <Import Project="$(VCTargetsPath)\Microsoft.Cpp.default.props" />.

<!-- $(VCTargetsPath)\Platforms\ARM\Platform.default.props -->
<PropertyGroup>
  <PlatformShortName>ARM</PlatformShortName>
  <PlatformArchitecture>ARM</PlatformArchitecture>
  <PlatformTarget>ARM</PlatformTarget>

<!-- $(VCTargetsPath)\Platforms\Win32\Platform.default.props -->
<PropertyGroup>
  <PlatformShortName>x86</PlatformShortName>
  <PlatformArchitecture>32</PlatformArchitecture>
  <PlatformTarget>x86</PlatformTarget>

<!-- $(VCTargetsPath)\Platforms\x64\Platform.default.props -->
<PropertyGroup>
  <PlatformShortName>x64</PlatformShortName>
  <PlatformArchitecture>64</PlatformArchitecture>
  <PlatformTarget>x64</PlatformTarget>

Compile DLL

By default, pre-defined Build task will try to output EXE file. If you want to change this behavior, set ConfigurationType property before importing $(VCTargetsPath)\Microsoft.Cpp.default.targets.

<PropertyGroup>
  <!-- <ConfigurationType>Application</ConfigurationType> (Build as EXE) -->
  <ConfigurationType>DynamicLibrary</ConfigurationType> <!-- (Build as DLL) -->
  <!-- <ConfigurationType>StaticLibrary</ConfigurationType> (Build as LIB) -->
</PropertyGroup

Or,

msbuild /p:ConfigurationType=DynamicLibrary

This reason can be found at $(VCTargetsPath)\Microsoft.Cpp.default.props, $(VCTargetsPath)\Microsoft.Cpp.Common.props, and the other XMLs.

<!-- $(VCTargetsPath)\Microsoft.Cpp.default.props -->

<!-- This is the Cpp defaults settings mapping file. It defines all the project properties values
     (equivalent of System Macros) and also all the ItemDefinitionGroup defaults for each known
     ItemGroup (known as the default of the defaults in the current Project System) -->

<PropertyGroup>
  <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
  <ConfigurationType Condition="'$(ConfigurationType)' == ''">Application</ConfigurationType>
</PropertyGroup>

<!-- $(VCTargetsPath)\Microsoft.Cpp.Common.props -->
<!-- Specific values -->
<PropertyGroup Condition="'$(ConfigurationType)' == 'Application'">
  <LinkCompiled>true</LinkCompiled>
  <TargetExt>.exe</TargetExt>
  <OutputType>exe</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(ConfigurationType)' == 'DynamicLibrary'">
  <LinkCompiled>true</LinkCompiled>
  <!-- $(GenerateImportLib) should be set to true when you want to generate the import library as part of the BuildCompile pass rather than wait
       until the BuildLink pass for Linker to generate it. This allows circular dependencies between dlls to be satisfied when building using passes -->
  <ImpLibCompiled Condition="'$(GenerateImportLib)'=='true'">true</ImpLibCompiled>
  <TargetExt>.dll</TargetExt>
  <OutputType>library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(ConfigurationType)' == 'StaticLibrary'">
  <LibCompiled>true</LibCompiled>
  <TargetExt>.lib</TargetExt>
  <OutputType>staticlibrary</OutputType>
</PropertyGroup>

Exec Task

Exec Task does not inherit the ending-state (variables and working directory) of prior Exec. For example, the following two cd will print the different location.

<Target Name="AfterBuild">
  <Exec Command='cd /D Z:\foobar &amp;&amp; cd' />
  <Exec Command='cd' />
</Target>

In addition, variables are reset. The next first command will print foo, but x86 or x64 will be printed on second command.

<Target Name="AfterBuild">
  <Exec Command='set Platform="foo" &amp;&amp; echo %Platform%' />
  <Exec Command='echo %Platform%' />
</Target>

All states seems to reset to the state of beginning of each Target.

NOTE: Major command will use ampersand &, so do not forget to escape it by obeying XML attribute syntax. (Use &amp;)

MSBuild properties are does not expose as environment variables, so the following command will print nothing.

<Target Name="AfterBuild">
  <Exec Command="echo_Platform.bat" />
  <!-- echo_Platform.bat (echo %Platform%) -->
</Target>

In this case, expose wanted properites as variables by using SetEnv task.

<Target Name="AfterBuild">
  <SetEnv Name="Platform" Prefix="false" Value=$(Platform) />
  <Exec Command="echo_Platform.bat" />
</Target>

Variables set by SetEnv will be inherited between target and target, beware of their side effect.

On the following example, TargetA prints nothing, but TargetB prints x86.

<Project DefaultTargets="TargetA;TargetB" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Platform>x86</Platform>
    <PlatformToolset>v140</PlatformToolset>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.default.props" />
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
  <Target Name="TargetA">
    <Exec Command="echo %Platform%" />
    <SetEnv Name="Platform" Prefix="false" Value="$(Platform)" />
  </Target>
  <Target Name="TargetB">
    <Exec Command="echo %Platform%" />
  </Target>
</Project>

If want to prevent environment from pollution, consider to place set command before batch file instead of SetEnv. Next first command will print x86 but second one nothing.

<Target Name="AfterBuild">
  <Exec Command="set Platform=$(Platform) &amp; echo_Platform.bat" />
  <Exec Command="echo_Platform.bat" />
</Target>

Note that Command="set Platform=$(Platform) & echo %Platform% print nothing because of DOS variable expansion behavior. (See EnableDelayedExpansion). Try set foo=bar & echo %foo% on your vanilla Command Prompt.

In this case, simply use MSBuild substitution echo $(Platform).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment