Import element does not expand ItemGroup correctly - msbuild

I'm trying to populate an ItemGroup with directory basenames, pass it through a transform to create absolute paths from them and import the files residing there. It works flawlessly in a Message element, but fails to do anything in an Import. What am I missing?
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<MyDependencies Include="Dir1" />
<MyDependencies Include="Dir2" />
</ItemGroup>
<!-- "Expansion: C:\path\Dir1\import.targets;C:\some\path\Dir2\import.targets" -->
<Target Name="TestMessage" BeforeTargets="PrepareForBuild">
<Message
Importance="High"
Text="Expansion: #(MyDependencies -> 'C:\path\%(Identity)\import.targets')"
/>
</Target>
<!-- Uncomment to trigger error -->
<!--<Import Project="#(MyDependencies -> 'C:\path\%(Identity)\import.targets')" />-->
</Project>
The Import's path remains unexpanded and leads to:
error MSB4102: The value "#(MyDependencies -> 'C:\path\%(Identity)\import.targets')" of the "Project" attribute in element <Import> is invalid. Illegal characters in path.

This happens because the ItemGroup gets evaluated after Import only. See https://learn.microsoft.com/en-us/visualstudio/msbuild/comparing-properties-and-items?view=vs-2022#property-and-item-evaluation-order:
During the evaluation phase of a build, imported files are
incorporated into the build in the order in which they appear.
Properties and items are defined in three passes in the following
order:
In other words an Import is almost the same as copy-pasting the content of the file at the location of that import, and only after that MSBuild begins to evaluate properties and items.

Related

Get a property value out of an MSBuild Task

My scenario is as follows.
I have a .targets file that does some stuff, for example,
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Some UsingTask here -->
<PropertyGroup>
<CustomProperty1>MyFirstValue</CustomProperty1>
<CustomProperty2>MySecondValue</CustomProperty2>
<Target Name="AfterResolveReferences">
<MSBuild Projects="MyProjectLocation\MyProject.csproj" Properties="CustomProperty1=$(CustomProperty1);CustomProperty2=$(CustomProperty2)" Targets="Rebuild" />
<Message Text="CodeGenerated = $(CodeGenerated)" />
</Target>
<Target Name="CodeGeneratedSpecificStuff" Condition="$(CodeGenerated) == 'true'">
<!-- Stuff happens -->
</Target>
<!-- More targets and bits and pieces -->
</Project>
And MyProject.csproj looks like this:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<!-- Some property groups with some properties -->
<!-- Also Compile includes etc as expected in a .csproj file -->
<Target Name="BeforeBuild" DependsOnTargets="GenerateCode">
<ItemGroup Condition="$(CodeGenerated) == 'true'">
<Compile Include="MyGeneratedCode.cs" />
</ItemGroup>
</Target>
<Target Name="GenerateCode">
<MyCodeGenerationCustomTask MyOperationalProperty="$(CustomProperty1)">
<Output TaskParameter="CodeGenerated" PropertyName="CodeGenerated" />
</MyCodeGenerationCustomTask>
</Target>
</Project>
I am trying to bubble the "CodeGenerated" property that comes from my MyCodeGenerationCustomTask in the .csproj file to get out into my .targets file.
So far I've tried:
Passing in the "CodeGenerated" property as empty at the MSBuild call in the .targets file.
Specifying an "Output" element under the MSBuild task (it doesn't work as the MSBuild task doesn't have a "CodeGenerated" property...)
I have also tried setting "Outputs" on both the "BeforeBuild" target and on the "Project" element in the .csproj file to "$(CodeGenerated)" (MSBuild didn't complain about the existence of the Outputs tag on the Project element, so I thought I might as well give it a go), but the value does not bubble up to the .targets file. In the .targets file I did also change the MSBuild task to look more like this:
<MSBuild Projects="MyProjectLocation\MyProject.csproj" Properties="CustomProperty1=$(CustomProperty1);CustomProperty2=$(CustomProperty2)" Targets="Rebuild">
<Output TaskParameter="TargetOutputs" PropertyName="CodeGenerated" />
</MSBuild>
But as I would expect this just contained the list of generated files from the MSBuild task.
Important to note is the code generation is working fine, and the CodeGenerated property within the .csproj file functions correctly and conditionally includes the cs file for compilation.
Is this possible? Am I just hitting my head against a brick wall? Or am I missing some MSBuild magic?
I really want to avoid checking the specific .cs location at every level above for its existence.

using AssemblySearchPaths in csproj files

I am trying to set up my csproj files to search for dependencies in a parent directory by adding:
<PropertyGroup>
<AssemblySearchPaths>
..\Dependencies\VS2012TestAssemblies\; $(AssemblySearchPaths)
</AssemblySearchPaths>
</PropertyGroup>
I added this as the last PropertyGroup element right before the first ItemGroup which has all of the Reference declarations.
Unfortunately this is causing all of the other references to fail to resolve, for example:
ResolveAssemblyReferences:
Primary reference "Microsoft.CSharp".
9>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Microsoft.Common.targets(1578,5): warning MSB3245: Could not resolve this reference. Could not locate the assembly "Microsoft.CSharp". Check to make sure the assembly exists on disk. If this reference is required by your code, you may get compilation errors.
For SearchPath "..\Dependencies\VS2012TestAssemblies\".
Considered "..\Dependencies\VS2012TestAssemblies\Microsoft.CSharp.winmd", but it didn't exist.
Considered "..\Dependencies\VS2012TestAssemblies\Microsoft.CSharp.dll", but it didn't exist.
Considered "..\Dependencies\VS2012TestAssemblies\Microsoft.CSharp.exe", but it didn't exist.
Is there a simple way for me to tell msbuild to where to search for my project's dependencies? I realize I can use /p:ReferencePath, however I prefer to have compilation logic in the csproj files themselves rather than have TFS Team Builds dictate where to look, not to mention that I'd like this to be able to be compiled on other developers machines.
I did try moving $(AssemblySearchPaths) to be first in list, but that did not help.
Can you change the value of the "AssemblySearchPaths" property within the Target "BeforeResolveReferences" and see if that solves your issue?
<Target Name="BeforeResolveReferences">
<CreateProperty
Value="..\Dependencies\VS2012TestAssemblies;$(AssemblySearchPaths)">
<Output TaskParameter="Value"
PropertyName="AssemblySearchPaths" />
</CreateProperty>
</Target>
Seems like there was a fix recently Thus this works as well:
<PropertyGroup>
<ReferencePath>MY_PATH;$(ReferencePath)</ReferencePath>
</PropertyGroup>
This makes the assemblies in that folder to also show up in the "Add References..." window :)
And since you also might not want the assemblies to be copied into the output-folder, here an example on how to achieve this:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- ... -->
<PropertyGroup>
<!-- Add paths to ReferencePath. E.g. here it is Unity. -->
<ReferencePath>C:\Program Files\Unity\Hub\Editor\$(UNITY_VERSION)\Editor\Data\Managed\UnityEngine;$(ReferencePath)</ReferencePath>
</PropertyGroup>
<Target Name="DontCopyReferencePath" AfterTargets="ResolveAssemblyReferences">
<!-- Don't copy files indirectly referenced by ReferencePath -->
<ItemGroup>
<!-- Collect paths to allow for batching -->
<ReferencePaths_ Include="$(ReferencePath)" />
<!-- Use batching to remove all files which should not be copied. -->
<ReferenceCopyLocalPaths Remove="#(ReferencePaths_ -> '%(Identity)\*.*')" />
</ItemGroup>
</Target>
<!-- ... -->
</Project>

How do I get an msbuild task to do config transforms on a collection of files?

I am trying to transform all of the web.config files in a project I have, here's a my tree structure:
Transform.bat
Transforms
ConfigTransform.proj
Web.Transform.config
Website
web.config
Views
web.config
There's more web.config files, but the idea is that this will find all of them and apply the same config transform on them.
I've taken a few hints from a blog post I found but I get stuck in the last step, the actual transformation. Also there's a bit of a rough part in the middle that I don't really like (I don't quite understand what I'm doing and I'm obviously doing it wrong). Here's where I am so far:
<Project ToolsVersion="4.0" DefaultTargets="Transform" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="TransformXml" AssemblyFile="Tools\Microsoft.Web.Publishing.Tasks.dll"/>
<PropertyGroup>
<SitePath>..\..\Website</SitePath>
<WebConfigTransformInputFile>$(SitePath)\Web.config</WebConfigTransformInputFile>
<WebConfigTransformFile>Web.Transform.config</WebConfigTransformFile>
<OutDir>..\N\N\</OutDir>
</PropertyGroup>
<ItemGroup>
<_FilesToTransform Include="$(SitePath)\**\web.config"/>
</ItemGroup>
<Target Name="Transform">
<MakeDir Directories="#(_FilesToTransform->'$(OutDir)%(RelativeDir)')" />
<TransformXml Source="#(_FilesToTransform->'$(OutDir)%(RelativeDir)%(Filename)%(Extension)')"
Transform="$(WebConfigTransformFile)"
Destination="#(_FilesToTransform->'$(OutDir)%(RelativeDir)%(Filename)%(Extension)')" />
</Target>
</Project>
My Transform.bat looks like this:
%systemroot%\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe %CD%\Transforms\ConfigTransform.proj
So when I call the batch, the appropriate directories get created. However, as you can see I've had to be a little creative with the OutDir, making it ..\N\N. For some reason, if I don't do this the OutDir path will be exactly the same as the input directory. So I obviously need to change something in MakeDir but I'm not sure what.
The real problem comes when it starts to do the transforms. I've tried to keep the TransformXml Source parameter like this or like so:
#(_FilesToTransformNotAppConfig->'%(FullPath)')
The latter gives me an error "Could not open Source file: The given path's format is not supported." and the former gives me this output:
Build started 30-4-2012 14:02:48.
Project "D:\Dev\transform\DoTransforms\Transforms\ConfigTransform.proj" on node 1 (default targets).
Transform:
Creating directory "..\N\N\..\..\Website\Views\".
Transforming Source File: ..\N\N\..\..\Website\Views\Web.config;..\N\N\..\..\Website\Web.config
D:\Dev\transform\DoTransforms\Transforms\ConfigTransform.proj(32,2): error : Could not open Source file: Could not find a part of the path 'D:\Dev\transform\DoTransforms\Website\Views\Web.config;\Website\Web.config'.
Transformation failed
Done Building Project "D:\Dev\transform\DoTransforms\Transforms\ConfigTransform.proj" (default targets) -- FAILED.
Build FAILED.
To summarize my questions:
How do I avoid the path issue for the OutDir? I've fiddled with multiple paths but I can't get it right.
How do I get the TransformXml task to accept multiple files in the Source attribute?
I think you were pretty close. I have pasted a sample below which shows how to do this.
In my sample I discover the transform sitting next to the web.config file itself. For your scenario you can just use an MSBuild property pointing to a specific file.
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="TransformAll" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll"/>
<PropertyGroup>
<Configuration Condition=" '$(Configuration)'=='' ">Release</Configuration>
<OutputFolder Condition=" '$(OutputFolder)'=='' ">C:\temp\transformed-files\</OutputFolder>
</PropertyGroup>
<!--
This target shows how to transform web.config with a specific transform file associated to that specific web.config file.
-->
<Target Name="TransformAll">
<!-- discover the files to transform -->
<ItemGroup>
<FilesToTransofm Include="$(MSBuildProjectDirectory)\**\web.config"/>
</ItemGroup>
<!-- Ensure all target directories exist -->
<MakeDir Directories="#(FilesToTransofm->'$(OutputFolder)%(RecursiveDir)')"/>
<!-- TransformXml only supports single values for source/transform/destination so use %(FilesToTransofm.Identity)
to sned only 1 value to it -->
<TransformXml Source="%(FilesToTransofm.Identity)"
Transform="#(FilesToTransofm->'%(RecursiveDir)web.$(Configuration).config')"
Destination="#(FilesToTransofm->'$(OutputFolder)%(RecursiveDir)web.config')" />
</Target>
</Project>
FYI you can download a full sample at https://github.com/sayedihashimi/sayed-samples/tree/master/TransformMultipleWebConfigs.

Filter Global MSBuild Items

Starting with a .csproj which defines various xml Content files.
Have code generation Target which takes some xml files (Target Inputs) and generate .cs files whose names are determined by transformation from the xml files (Target Outputs).
In order for MSBuild to determine whether the code building Target needs to run, it needs to inspect the Target Inputs and Outputs. Therefore I am assuming that those Target Inputs and Outputs must be global.
If that's incorrect, there should be another question about how to create a Target who's Outputs are based on Dynamic Items; tried it but the Target keeps being called.
If it's correct, then how to filter the Content at the global level ?
Specifically, I want to filter Content Items in the project so that only the one's in a specific directory are used. The Content Items will be added by other developers via the IDE.
This can be achieved using a Target which creates Dynamic Items, doing the filtering in the Condition attribute. That requires Target Batching, which isn't available globally. Using MSBuild 3.5 and Visual Studio 2008.
<?xml version="1.0" encoding="utf-8"?>
<Project
DefaultTargets="Show" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Content Include="badxml\somebadxml1.xml" />
<!-- Note xml\somexml2.xml exists on disk, it just isn't used in this project. -->
<Content Include="xml\somexml1.xml" />
<Content Include="xml\somexml3.xml" />
</ItemGroup>
<!-- Foo should only be defined for Content Items in the "xml" directory. -->
<ItemGroup>
<Foo Include="#(Content->'%(Filename)')"/>
<!-- The line below doesn't work -->
<!-- TestFilter.proj(10,10): error MSB4090: Found an unexpected character '%' at position 3 in condition " '%(Content.RelativeDir)'=='xml' ". -->
<!-- <Foo Condition=" '%(Content.RelativeDir)'=='xml' " Include="#(Content->'%(Filename)')"/> -->
</ItemGroup>
<Target Name="ShowContent">
<Message Text="Content: %(Content.Identity)" />
<Message Text="Content RelDir: %(Content.RelativeDir)" />
</Target>
<Target Name="ShowFoo">
<Message Text="Foo: %(Foo.Identity)" />
</Target>
<Target Name="Show">
<CallTarget Targets="ShowContent;ShowFoo" />
</Target>
</Project>
Why doesn't MSBuild ItemGroup conditional work in a global scope addresses the same issue but from the perspective of asking why this doesn't work, rather than looking for alternative approaches.
Filtering Item's Metadata in msbuild uses Dynamic Items in a Target, and a dummy Output name.
My best guess is that this can't be done without using Dynamic Items in a Target, and the workaround will be rather than using Items which require a Condition, to write out a file with a predefined name and use that as Output placeholder.
So it turns out my assumption was incorrect. It's perfectly acceptable to have Target Outputs which are based on dynamic items. It helps to remember that Targets are batched according to the Outputs definition.
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="TestBatch"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Static Item declaration. -->
<ItemGroup>
<Bar Include="Static01">
<Data>Static01 Data</Data>
</Bar>
</ItemGroup>
<Target Name="PreBatchTarget">
<!-- Dynamic Item addition. -->
<ItemGroup>
<Bar Include="Dynamic01">
<Data>Dynamic01 Data</Data>
</Bar>
</ItemGroup>
</Target>
<Target Name="TestBatchTarget"
Outputs="%(Bar.Data)"
>
<Message Text="TestBatchTarget call" />
<Message Text="#(Bar)" />
</Target>
<Target Name="TestBatch"
DependsOnTargets="PreBatchTarget;TestBatchTarget"
>
</Target>
</Project>
msbuild /nologo DynamicTargetOutput.proj
Project "DynamicTargetOutput.proj" on node 0 (default targets).
TestBatchTarget call
Static01
TestBatchTarget:
TestBatchTarget call
Dynamic01
Done Building Project "DynamicTargetOutput.proj" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.09

Replace .sln with MSBuild and wrap contained projects into targets

I'd like to create a MSBuild project that reflects the project dependencies in a solution and wraps the VS projects inside reusable targets.
The problem I like solve doing this is to svn-export, build and deploy a specific assembly (and its dependencies) in an BizTalk application.
My question is: How can I make the targets for svn-exporting, building and deploying reusable and also reuse the wrapped projects when they are built for different dependencies?
I know it would be simpler to just build the solution and deploy only the assemblies needed but I'd like to reuse the targets as much as possible.
The parts
The project I like to deploy
<Project DefaultTargets="Deploy" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ExportRoot Condition="'$(Export)'==''">Export</ExportRoot>
</PropertyGroup>
<Target Name="Clean_Export">
<RemoveDir Directories="$(ExportRoot)\My.Project.Dir" />
</Target>
<Target Name="Export_MyProject">
<Exec Command="svn export svn://xxx/trunk/Biztalk2009/MyProject.btproj --force" WorkingDirectory="$(ExportRoot)" />
</Target>
<Target Name="Build_MyProject" DependsOnTargets="Export_MyProject">
<MSBuild Projects="$(ExportRoot)\My.Project.Dir\MyProject.btproj" Targets="Build" Properties="Configuration=Release"></MSBuild>
</Target>
<Target Name="Deploy_MyProject" DependsOnTargets="Build_MyProject">
<Exec Command="BTSTask AddResource -ApplicationName:CORE -Source:MyProject.dll" />
</Target>
</Project>
The projects it depends upon look almost exactly like this (other .btproj and .csproj).
Wow, this is a loaded question for a forum post. I wrote about 20 pages on creating reusable .targets files in my book, but I'll get you started here with the basics here. I believe that the key to creating reusable build scripts (i.e. .targets files) is three elements:
Place behavior (i.e. targets) into separate files
Place data (i.e. properties and items, these are called .proj files) into their own files
Extensibility
.targets files should validate assumptions
The idea is that you want to place all of your targets into separate files and then these files will be imported by the files which will be driving the build process. These are the files which contain the data. Since you import the .targets files you get all the targets as if they had been defined inline. There will be a silent contract between the .proj and .targets files. This contract is defined in properties and items which both use. This is what needs to be validated.
The idea here is not new. This pattern is followed by .csproj (and other projects generated by Visual Studio). If you take a look your .csproj file you will not find a single target, just properties and items. Then towards the bottom of the file it imports Microsoft.csharp.targets (may differ depending on project type). This project file (along with others that it imports) contains all the targets which actually perform the build.
So it's layed out like this:
SharedBuild.targets
MyProduct.proj
Where MyProdcut.proj might look like:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This uses a .targets file to off load performing the build -->
<PropertyGroup>
<Configuration Condition=" '$(Configuration)'=='' ">Release</Configuration>
<OutputPath Condition=" '$(OutputPath)'=='' ">$(MSBuildProjectDirectory)\BuildArtifacts\bin\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Projects Include="$(MSBuildProjectDirectory)\..\ClassLibrary1\ClassLibrary1.csproj"/>
<Projects Include="$(MSBuildProjectDirectory)\..\ClassLibrary2\ClassLibrary2.csproj"/>
<Projects Include="$(MSBuildProjectDirectory)\..\ClassLibrary3\ClassLibrary3.csproj"/>
<Projects Include="$(MSBuildProjectDirectory)\..\WindowsFormsApplication1\WindowsFormsApplication1.csproj"/>
</ItemGroup>
<Import Project="SharedBuild.targets"/>
</Project>
And SharedBuild.targets might look like:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This represents a re-usable build file -->
<Target Name="SharedBuild_Validate">
<!-- See http://sedodream.com/2009/06/30/ElementsOfReusableMSBuildScriptsValidation.aspx for more info
about this validation pattern
-->
<ItemGroup>
<_RequiredProperties Include ="Configuration">
<Value>$(Configuration)</Value>
</_RequiredProperties>
<_RequiredProperties Include ="OutputPath">
<Value>$(OutputPath)</Value>
</_RequiredProperties>
<_RequiredItems Include="Projects">
<RequiredValue>%(Projects.Identity)</RequiredValue>
<RequiredFilePath>%(Projects.Identity)</RequiredFilePath>
</_RequiredItems>
</ItemGroup>
<!-- Raise an error if any value in _RequiredProperties is missing -->
<Error Condition="'%(_RequiredProperties.Value)'==''"
Text="Missing required property [%(_RequiredProperties.Identity)]"/>
<!-- Raise an error if any value in _RequiredItems is empty -->
<Error Condition="'%(_RequiredItems.RequiredValue)'==''"
Text="Missing required item value [%(_RequiredItems.Identity)]" />
<!-- Validate any file/directory that should exist -->
<Error Condition="'%(_RequiredItems.RequiredFilePath)' != '' and !Exists('%(_RequiredItems.RequiredFilePath)')"
Text="Unable to find expeceted path [%(_RequiredItems.RequiredFilePath)] on item [%(_RequiredItems.Identity)]" />
</Target>
<PropertyGroup>
<BuildDependsOn>
SharedBuild_Validate;
BeforeBuild;
CoreBuild;
AfterBuild;
</BuildDependsOn>
</PropertyGroup>
<Target Name="Build" DependsOnTargets="$(BuildDependsOn)"/>
<Target Name="BeforeBuild"/>
<Target Name="AfterBuild"/>
<Target Name="CoreBuild">
<!-- Make sure output folder exists -->
<PropertyGroup>
<_FullOutputPath>$(OutputPath)$(Configuration)\</_FullOutputPath>
</PropertyGroup>
<MakeDir Directories="$(_FullOutputPath)"/>
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="OutputPath=$(_FullOutputPath)"/>
</Target>
</Project>
Don't look too much at the SharedBuild_Validate target yet. I put that there for completeness but don't focus on it. You can find more info on that at my blog at http://sedodream.com/2009/06/30/ElementsOfReusableMSBuildScriptsValidation.aspx.
The important parts to notice are the extensibility points. Even though this is a very basic file, it has all the components of a reusable .targets file. You can customize it's behavior by passing in different properties and items to build. You can extend it's behavior by overriding a target (BeforeBuild, AfterBuild or even CoreBuild) and you can inject your own targets into the build with:
<Project ...>
...
<Import Project="SharedBuild.targets"/>
<PropertyGroup>
<BuildDependsOn>
$(BuildDependsOn);
CustomAfterBuild
</BuildDependsOn>
</PropertyGroup>
<Target Name="CustomAfterBuild">
<!-- Insert stuff here -->
</Target>
</Project>
In your case I would create an SvnExport.targets file which uses the required properties:
SvnExportRoot
SvnUrl
SvnWorkingDirectory
You will use these properties to do the Export.
Then create another one for Biztalk build and deploy. You could split this up into 2 if necessary.
Then inside of your .proj file you just import both and setup the targets to build in the right order, and your off.
This is only really the beginning of creating reusable build elements, but this should get the wheels turning in your head. I am going to post all of this to my blog as well as download links for all files.
UPDATE:
Posted to blog at http://sedodream.com/2010/03/19/ReplacingSolutionFilesWithMSBuildFiles.aspx