Reputation: 305
I am trying to set up an application I'm working on in Visual Studio to include a set of standard assembly-level attributes in the projects by importing a couple of scripts into the project files as they are compiled by MSBuild. The attributes in question are generated by an MSBuild task that is called from one of the scripts, while the other script includes the generated code file as a Compile item.
So far, my custom task is generating the include file OK and passing back to MSBuild a path to the file it has created, but for some reason MSBuild is stubbornly refusing to include the generated file in the build of the project. This is confirmed by verifying that the compiled .dll file does not have any of the values from the generated assembly attributes in its version information resource.
My custom task script (Build.targets):
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask AssemblyFile="..\Build.Tasks\$(OutDir)Build.Tasks.dll" TaskName="GenerateAssemblyInfo" />
<Target Name="GenerateAssemblyAttributes" Condition="'$(SkipGenerateAssemblyAttributes)' == 'false'">
<GenerateAssemblyInfo AssemblyName="$(AssemblyName)" CompilationSymbols="$(DefineConstants)" IncludeFileFolder="$(AssemblyInfoFileFolder)">
<Output TaskParameter="IncludeFilePath" PropertyName="AssemblyInfoFilePath"/>
</GenerateAssemblyInfo>
<Message Text="Generated AssemblyInfo include file $(AssemblyInfoFilePath)."/>
</Target>
</Project>
The conditional compilation script (Build.props):
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="Exists('$(AssemblyInfoFilePath)')">
<Compile Include="$(AssemblyInfoFilePath)"/>
</ItemGroup>
</Project>
A sample project I created to test/demonstrate the problem:
<Project DefaultTargets="GenerateAssemblyAttributes;Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AssemblyInfoFileFolder>Properties</AssemblyInfoFileFolder>
<SkipGenerateAssemblyAttributes>false</SkipGenerateAssemblyAttributes>
</PropertyGroup>
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
...
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>BuildTest</RootNamespace>
<AssemblyName>BuildTest</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
...
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
...
</PropertyGroup>
<Import Project="..\Build.Tasks\Build.targets" />
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<Import Project="..\Build.Tasks\Build.props" />
<ItemGroup>
<Compile Include="Class1.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
If, in the Build.props script, I replace the $(AssemblyInfoFilePath)
property with the literal path to the generated file (e.g. Properties\_AssemblyInfo.cs
), or I replace the import of the Build.props script with a Compile item to reference the generated file directly (e.g. <Compile Include="Properties\_AssemblyInfo.cs" Condition="Exists('Properties\_AssemblyInfo.cs')" />
), then the file gets included in the build as I would expect. As soon as I put the $(AssemblyInfoFilePath)
property back into the <Compile>
item, MSBuild starts ignoring the file again, with or without the "Exists" condition.
I have proved that the $(AssemblyInfoFilePath)
property is being created and set from the output of the custom task, both from within the target that calls the task (diagnostic build output excerpt follows):
Target "GenerateAssemblyAttributes" in file "... Build.Tasks\Build.targets":
Using "GenerateAssemblyInfo" task from assembly "... Build.Tasks\..\Build.Tasks\bin\Debug\Build.Tasks.dll".
Task "GenerateAssemblyInfo"
Build 2.0.5394.41529
Done executing task "GenerateAssemblyInfo".
Task "Message"
Generated AssemblyInfo include file Properties\_AssemblyInfo.cs.
Done executing task "Message".
Done building target "GenerateAssemblyAttributes" in project "BuildTest.csproj".
Target "BeforeBuild" in file "C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets":
...
and also by adding another target to the application project file, dependent on the GenerateAssemblyAttributes target, that also uses a <Message>
element to output the value of the property.
I just can't work out why, if the property containing the path to the generated source code file is correctly set from the output of the custom task, MSBuild is acting like it doesn't even exist? Using Visual Studio 2005 and MSBuild 2.0.
edit: So, if I modify my Build.targets file like so:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask AssemblyFile="..\Build.Tasks\$(OutDir)Build.Tasks.dll" TaskName="GenerateAssemblyInfo" />
<Target Name="GenerateAssemblyAttributes" Condition="'$(SkipGenerateAssemblyAttributes)' == 'false'">
<GenerateAssemblyInfo AssemblyName="$(AssemblyName)" CompilationSymbols="$(DefineConstants)" IncludeFileFolder="$(AssemblyInfoFileFolder)">
<Output TaskParameter="IncludeFilePath" PropertyName="AssemblyInfoFilePath"/>
</GenerateAssemblyInfo>
<Message Text="Generated AssemblyInfo include file $(AssemblyInfoFilePath)."/>
<CreateItem Include="$(AssemblyInfoFilePath)" Condition ="Exists('$(AssemblyInfoFilePath)')">
<Output TaskParameter="Include" ItemName="Compile"/>
</CreateItem>
</Target>
</Project>
and remove the Build.props file from the application project file then the build ends up being "half right" in that the source code file generated by the custom task gets included in the build. Unfortunately, MSBuild then proceeds to produce the following errors for the first source code file in each of the application projects included (referenced) in the project that is being built:
Error 147 The item "source_code_file.cs" was specified more than once in the "Sources" parameter. Duplicate items are not supported by the "Sources" parameter. C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.CSharp.targets
If the target for executing the custom task and creating (adding) the "Compile" item for the generated source code file refers only to the path of that source code file, how is it that unrelated source code files get duplicated in the source code file list sent to the compiler as MSBuild claims?
Upvotes: 1
Views: 3779
Reputation: 305
I now have MSBuild including the generated source code file, the path of which is specified in a property output from my custom build task, in the compilation of each of my application assemblies by adding an item to the "Compile" items group in the target that calls the custom task (as detailed above).
The problem I was having with "duplicate sources" being reported by the compiler has been remedied by adding another target to the application project files to remove duplicates from the "Compile" item group before the assembly is compiled, e.g.
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="RemoveSourceCodeDuplicates" >
<RemoveDuplicates Inputs="@(Compile)">
<Output TaskParameter="Filtered" ItemName="Compile"/>
</RemoveDuplicates>
</Target>
</Project>
and in the application project file:
<Project DefaultTargets="GenerateAssemblyAttributes;Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
...
<Import Project="..\Build.Tasks\RemoveDuplicates.targets"/>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
...
</Project>
Upvotes: 1
Reputation: 5652
It's the evaluation order. The <Itemgroup>
is evaluated before any tasks are performed.
Instead, add the items as part of the task that generates them, instead of at global level. That's not pretty with old versions of MSBuild: I don't know exactly when the ability to simply put itemgroups inside a task element was added.
Also, the Include for a wildcard will only add things that exist that the time it's performed, so you'll get the problem where it doesn't work on a clean build but seems to work ok after a 2nd attempt, if it were resolving the path (like you had it hard-coded).
Upvotes: 1