Using MSBuild to Build Multiple Configurations
Asked Answered
G

3

26

I'm trying to edit my project file to enable me to have a project that builds multiple build configs at once. I've done this using a batching approach and using the MSBuild task (see below).

If I run the script, I get an this error:

Error 103 The OutputPath property is not set for project "ThisMSBuildProjectFile.csproj". Please check to make sure that you have specified a valid combination of Configuration and Platform for this project. Configuration='Debug' Platform='AnyCPU'.

I get this if I add or omit the OutputPath from the MSBuild task. If used the VS2010 debugger to step through the script and the MSBuild Task is called - the debugger steps into the file again and then steps into OutputPath, so afaik, it should pick that value up, no?

Any help for this would be greatly appreciated - it's driving me crazy. Thanks, Paul.

ThisMSBuildProjectFile.csproj (surplus stuff taken out):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">

  <!-- Only Import normal targets if not building multiple projects -->
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" Condition="'$(Configuration)|$(Platform)' != 'AllBuild|AnyCPU' "/>

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == '' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>C:\Folder\Etc\Output\$(Configuration)\</OutputPath>
    <OutDir>C:\Folder\Etc\Output\$(Configuration)\</OutDir>
    <BaseOutputPath>C:\Folder\Etc\Output\$(Configuration)\</BaseOutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>

  <!-- Common -->
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <Platform>AnyCPU</Platform>
    <!-- Repeated properties from above here (including, of course, OutputPath) -->  
   </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <!-- Repeated properties from above here (including, of course, OutputPath) --> 
  </PropertyGroup>

  <ItemGroup>
    <Projects Include="C:\Folder\Etc\ThisMSBuildProjectFile.csproj" />
  </ItemGroup>

   <!-- Call this project file again, but with a different configuration - if this was working, this would call multiple  build configs -->
  <Target Name="Build" Condition="'$(Configuration)|$(Platform)' == 'AllBuild|AnyCPU' ">
    <Message Text="hm!"/>
    <!-- Tried thiswith and without the OutputPath property - makes no difference. -->
   <MSBuild  Projects="@(Projects)" Properties="Configuration=Debug;OutputPath=C:\Folder\Etc\Output\" ToolsVersion="4.0" Condition="'$(Configuration)|$(Platform)' == 'AllBuild|AnyCPU' "/>
 </Target>

   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'AllBuild|AnyCPU' ">
    <!-- Repeated properties from above here (including, of course, OutputPath) --> 
  </PropertyGroup>

  <!-- Project files -->
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="Blah\Blah.cs" />
  </ItemGroup>

Gangling answered 1/4, 2011 at 9:6 Comment(0)
P
25

It is important to realize that when you use a "MSBuild" task, a new child MSBuild process will be started. The implication of this is that any items and properties you define in the parent MSBuild process will not be automatically passed to/visible from the child MSBuild process unless you explicitely pass them via Properties attribute on MSBuild element (as in <MSbuild Properties="..." />).

To answer your question, I wrote the following self-contained example that runs a child MSBuild project for all the specified configurations:

  1. First, create a directory for your MSBuild experiment (for example I used C:\temp\msbuildtest)

  2. In this directory, create the first file, main.proj:

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build" ToolsVersion="4.0">
        <ItemGroup>
            <ConfigList Condition=" '@(ConfigList)' == '' and $(Config) != '' " Include="$(Config.Split('+'))" /><!-- parse all requested configurations into a list -->
            <ConfigList Condition=" '@(ConfigList)' == '' " Include="Debug" /><!-- if no configurations were specified, default to Debug -->
        </ItemGroup>
        <!--
    
        Build the child project for each requested configuration. -->
        <Target Name="Build">
            <MSBuild Projects="$(MSBuildProjectDirectory)\child.proj" Properties="Configuration=%(ConfigList.Identity);OutputPath=$(MSBuildProjectDirectory)\bin\%(ConfigList.Identity)" Targets="Build" />
        </Target>
    </Project>
    
  3. In the same directory, create the second file, child.proj (in your case this would be the actual C# project you're trying to build, but because I'm trying to illustrate my point, I am using a simple child project that instead of running C# compiler just prints values of properties :-) )

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build" ToolsVersion="4.0">
        <Target Name="Build">
            <Message Text="Building configuration $(Configuration) with output path $(OutputPath)" Importance="High" />
        </Target>
    </Project>
    
  4. Now you can run the example. First the default, if you don't explicitly specify configurations to build:

    C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\msbuild main.proj
    > (cut the noise)
    > Build:
    >   Building configuration Debug with output path C:\temp_c\d\bin\Debug
    

    And then explicitly specified multiple configurations:

    C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\msbuild main.proj /property:Config=Debug+Release+Staging+Production
    > (cut the noise)
    > Build:
    >   Building configuration Debug with output path C:\temp_c\d\bin\Debug
    > Build:
    >   Building configuration Release with output path C:\temp_c\d\bin\Release
    > Build:
    >   Building configuration Staging with output path C:\temp_c\d\bin\Staging
    > Build:
    >   Building configuration Production with output path C:\temp_c\d\bin\Production
    

You should be able to adapt this technique to your situation.

Prospect answered 1/4, 2011 at 14:32 Comment(5)
Excellent, thank you for this - it's not exactly what I was looking for (as I was looking to minimise the changes to the standard CSProj files), but it's looking like I was over-thinking things. I'll go with this approach for now. ThanksGangling
Fyi, this technique is called "Task Batching". There are many ways to do batching and Sayed Ibrahim Hashimi covers them in his book "Inside the Microsoft Build Engine: Using MSBuild and Team Foundation Build"Naucratis
"It is important to realize that when you use an "MSBuild" task, a new child MSBuild process will be started." This is not true. According to msdn.microsoft.com/en-us/library/z7f65y0d.aspx: "Unlike using the Exec Task to start MSBuild.exe, this task uses the same MSBuild process to build the child projects." This matches my own experience as well.Deflation
@MilianGardian is there a difference between "And" and "and"?Mariko
@Mariko I think logical operators are case-insensitive, but I've never tried uppercase variants :-).Prospect
D
5

Something is amiss in your project file. Consider this XML:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == '' ">
  <DebugType>pdbonly</DebugType>
  <Optimize>true</Optimize>
  <OutputPath>C:\Folder\Etc\Output\$(Configuration)\</OutputPath> 
  ...
</PropertyGroup>

Those properties can never be set, since even if $(Configuration) and $(Platform) are empty, they can never match the empty string when concatenated with the bar character; the minimal value for that condition is '|' and not ''. Even if corrected by making the condition compare with '|', you then go on to try to use $(Configuration) in the OutputPath in that PropertyGroup, but $(Configuration) will never have a value at the point it is used. Likewise, where you try to set $(Platform) to 'AnyCPU' it must already have that value. You probably meant to omit the condition on the first PropertyGroup altogether, and you may need to supply default values for $(Configuration) and $(Platform) in an early PropertyGroup with no conditions as well. Diff your whole project against a new project and see if there are any other oddities like this present.

Also notice that on your override of the "Build" target, you have a redundant Condition on the MSBuild task; with the same condition is on the <Target> you don't need it on any of the tasks.

Dian answered 1/4, 2011 at 13:59 Comment(2)
Sure, the reason that is there is that it's from the standard CSProj file - and the reason it's there is as a fallback if no config is specified. I realise that it wouldn't be hit in the example I gave, but when using the MSBuild debugger, the other conditional groups were hit as expected. Thanks for pointing out the duplication on the child nodes!Gangling
Perhaps you're missing my point, it isn't a fallback at all, the way it is coded the condition will never evaluate to true, because the left hand side will always contain at least a '|' and the right hand side will always be empty they can never be equal.Dian
H
4

I am not quite sure if I'd wanna go through such a convoluted configuration of the project's csproj file itself. I'd rather setup a separate MSBuild "BuildBoth.proj" file that has a specific target called "Both" that builds the solution in both configurations.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Both">

    <!-- Calls twice for both configs -->
    <Target Name="Both">
        <MSBuild Projects="buildboth.sln" Targets="Rebuild" Properties="Configuration=Debug"
                         StopOnFirstFailure="true">
        </MSBuild>

        <MSBuild Projects="buildboth.sln" Targets="Rebuild" Properties="Configuration=Release"
                         StopOnFirstFailure="true">
        </MSBuild>
    </Target>

    <!-- single config targets

    <Target Name="Debug">
        <MSBuild Projects="buildboth.sln" Targets="Rebuild" Properties="Configuration=Debug"
                         StopOnFirstFailure="true">
        </MSBuild>
    </Target>

    <Target Name="Release">
        <MSBuild Projects="buildboth.sln" Targets="Rebuild" Properties="Configuration=Release"
                         StopOnFirstFailure="true">
        </MSBuild>
    </Target>
    -->

</Project>

Then I'd run the command (verbosity set Minimal) to target Both

C:\Projects\experiments\BuildBoth>msbuild /v:m /target:Both BuildBoth.proj
Microsoft (R) Build Engine Version 4.0.30319.1
[Microsoft .NET Framework, Version 4.0.30319.225]
Copyright (C) Microsoft Corporation 2007. All rights reserved.

  BothWpf -> C:\Projects\experiments\BuildBoth\BothWpf\bin\Debug\BothWpf.exe
  BothWpf -> C:\Projects\experiments\BuildBoth\BothWpf\bin\Release\BothWpf.exe
Harrar answered 1/4, 2011 at 15:2 Comment(1)
Hi - thanks, I realise I can have a standalone project, but the idea was for members of the team to all be able to push builds from VS rather than shelling out - I was looking to minimally alter the standard CSProj file to enable the rest of the team to do this. Thaks for replying though.Gangling

© 2022 - 2024 — McMap. All rights reserved.