How to calculate MSBuild property before import - visual-studio

Consider these two MSBuild files. First build-script.xml:
<?xml version="1.0" encoding="utf-8"?>
<!-- build-script.xml -->
<Project
ToolsVersion="4.0"
DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<OutputName>OutputName</OutputName>
</PropertyGroup>
<Import Project="build-script-to-import.xml"/>
</Project>
and then build-script-to-import.xml:
<?xml version="1.0" encoding="utf-8"?>
<!-- build-script-to-import.xml -->
<Project
ToolsVersion="4.0"
DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<OutputPath>Path\$(OutputName)</OutputPath>
</PropertyGroup>
<Target Name="Build">
<Message Text="$(OutputPath)" />
</Target>
</Project>
These two files represents a very simplified C# project file (build-script.xml) referencing a common targets file (build-script-to-import.xml). That means that it is not an option to change the build-script-to-import.xml file. The problem is that the OutputName is calculated, and I need to set it to this calculated value before I import the build-script-to-import.xml file. Do you know how I can accomplish this?
NOTE 1: The calculated value is in fact an assembly version which I am able to fetch from an assembly file with the GetAssemblyIdentity MSBuild task. The problem as I see it is that I may not call a target before the import happens. I would like to set the value for property OutputName with the help of the GetAssemblyIdentity MSBuild task.
NOTE 2: The build will be triggered by Visual Studio, so I may not use a batch file and send a property to MSBuild as a command line argument.

Move the declaration of $(OutputPath) inside your "Build" target. The only way to use "calculated" properties is to have them evaluated inside a target. Globally declared "static" properties will always be evaluated before any target runs, and presumably you need to run a target to calculate the value of $(OutputName). Just be sure your name calculating target runs before the "Build" target.

If Corresponding targets from imported project allows to modify DependsOnTargets
<Target Name="Build" DependsOnTargets="#(BuildDependsOn)" />
you can modify Item and add your target wich will prepare needed properties.
If it is not possible you can write wrapper to call underlying target/targets (Build/Rebuild/Clean) by yourself (without importing build-script-to-import.xml):
build-script.xml
<Target Name="Build" DependsOn="PrepareProperties">
<MSBuild Projects="build-script-to-import.xml"
Targets="Build"
Properties="OutputName=$(OutputName);$(OtherProperties)"/>
</Target>
Suggestion
Depends on your requirement to dynamically change OutputName there may be mach simpler solution. Just not to do it. It is rather simpler to write your own custom build step to copy from fixed output directory to your destination wich is calculated using your rules.
For example, I prefer practice when I have directory called latest with fixed path and a directory for each version of product. Every one can take latest version from that folder.

Related

How do you specify common package versions in a common.props file?

We've got a simple common.props file that is basically doing what Directory.Build.props appears to do. It specifies various settings and is then imported to the csproj files that need it.
Here's the basics of the MyProject.common.props file:
<Project>
<PropertyGroup>
// some settings
</PropertyGroup>
<PropertyGroup>
<LangVersion>8.0</LangVersion>
//some more settings...
<MyNewVariable>3.1.22</MyNewVariable>
</PropertyGroup>
</Project>
Here's the basics of the csproj:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\folderA\MyProject.common.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(MyNewVariable)" />
</ItemGroup>
</Project>
When I use this approach I get the error:
Project dependency Microsoft.Extensions.Hosting.WindowsServices does
not contain an inclusive lower bound. Include a lower bound in the
dependency version to ensure consistent restore results.
However, if I just input 3.1.22 where the variable is it works fine. This suggests to me that the variable is coming through empty or not being picked up. If I remove the variable altogether and leave the Version blank I get the same error. I also double checked the path to the file by misspelling the import and I get a different error, so I know it can at least find the common.props file.

how use properties from Directory.Build.props in Import from project file of Visual Studio

I have this situation:
I have a .proj file in project directory:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Compile Include="PQExtensionTest.pq">
<SubType>Code</SubType>
</Compile>
<Content Include="PQExtensionTest.query.pq">
<SubType>Code</SubType>
</Content>
</ItemGroup>
<!-- <Import Project="..\Directory.Build.props" /> -->
<Import Project="$(aProperty)add.targets" />
</Project>
In the solution directory (..\ from project directory) I have file Directory.Build.props:
<Project DefaultTargets="BuildExtension" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<aProperty>$(MSBuildProjectDirectory)/Subdir/</aProperty>
</PropertyGroup>
</Project>
In the project directory I have subdirectory "Subdir", where there is file add.targets, which contains all the targets I need (do not show it's contains here because it is not relevant to the problem).
So all above has this folder structure:
Solution directory
Directory.Build.props
Project Directory
Project.mproj
Subdir
add.targets
Preparing all the above, I expected that aProperty will be initiated before the import and the import of add.targets will happen without problem. But I get error that imported project is not found, and I see in error message that MSBuild tries to import from project directory, and not from subdirectory Subdir.
If I uncomment this row:
<Import Project="..\Directory.Build.props" />
all works fine.
The only reasonable explanation for me of such behavior is that aProperty is empty at the moment of importing, because explicit import happens before implicit one.
Is there any way to force MSBuild to inexplicitly import Directory.Build.props before any other imports, while work in Visual Studio?
"While in Visual Studio"
For C# and VB language project, we don't need to import
Directory.Build.props manually or force it before other imports.
When creating a new project(C# or VB) in VS, open its proj file we can find the format is like this:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
...
</PropertyGroup>
<ItemGroup>
...
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Every time when creating new C# or VB project, the top line within the <Project>node is Import project="Microsoft.Common.props", and we can find the sentence from this document:
When MSBuild runs, Microsoft.Common.props searches your directory structure for the Directory.Build.props file (and Microsoft.Common.targets looks for Directory.Build.targets). If it finds one, it imports the property.
So in visual studio, we don't need to force it before other imports.Its always called after import Microsoft.Common.props, and since the import Microsoft.Common.props is always first line of project node by default, the Directory.Build.Targets is always implicitly imported right after the Microsoft.Common.props and before others.
Note: This feature only supports C# and VB, cause only these two kinds of projects will import the Microsoft.Common.Props in proj file.
And for other kinds of projects, just like your .mproj or
.vcxproj(C++), this feature(Directory.Build.props) is not supported
yet.
So the Directory.Build.Targets or .props is the same as any custom .props. It doesn't make difference between Directory.Build.Targets and anyName.props.
In this way,to read the value in it we have to use import project to call it manually. And that's why the build can't succeed until you uncomment the row:<Import Project="..\Directory.Build.props" />
The way to import properties from 'Directory.Build.props' file from nested folder structure is given below:
Refer: https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-your-build?view=vs-2019
Note that:
1. These property file definition works from MSBuild tools version 15.0
2. You need to be aware of where to place this import: At the beginning of the file or at the end of the file. Generally it is good to place at the end as nested properties will be visible to parent properties.

Check if PropertyGroup item is set to a value in .csproj

I am using .targets files to include common functionality in .csproj files.
In the target file, I want to check if a property was already set before, and if yes, do not set it again.
I need this because I am using the specific target file in many solutions, and I want to include the custum Property only if it wasn't set before.
The property I am talking about is
<PropertyGroup>
<CodeAnalysisRuleSet>$(SolutionDir)CustomizedAllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
If this ruleset was specified before Importing the target file, I don't want to include it again in the .targets file.
How do I check in .csproj if the <CodeAnalysisRuleSet>...</CodeAnalysisRuleSet> was set before?
The pattern I've seen most often is to set it conditionally based on comparing it to an empty value:
<PropertyGroup>
<CodeAnalysisRuleSet Condition="'$(CodeAnalysisRuleSet)' == ''">$(SolutionDir)CustomizedAllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
[Edit: responding to comment with code example]
Here's a longer example that works for me:
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PropertyValue Condition="$(PropertyValue) == ''">Default value</PropertyValue>
</PropertyGroup>
<Target Name="Build">
<Message Text="$(PropertyValue)" />
</Target>
</Project>
Gives the console output:
Project "D:\temp\test.proj" on node 1 (default targets).
Build:
Default value
Done Building Project "D:\temp\test.proj" (default targets).

Enforcing files in a folder have build action = Embedded Resource with target files

The Visual Studio project has a folder containing sql scripts and all files in it must have the build action set to Embedded Resource. While adding new files, developers often forget to change the build action.
I want to create a target file that throws an error a compile time if any of the files in the folder do not have the correct build action.
I have seen something similar done before.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0"
DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="MakeSureSqlFilesAreSetToCopyAlways" BeforeTargets="PrepareForBuild">
<Error Condition="!('%(Content.CopyToOutputDirectory)' == 'Always')"
Text="This Content file is not configured to Copy Always: [%(Content.FullPath)]" />
</Target>
</Project>
This block of code checks if the files are set to copy always. How do I check build action?
Would appreciate some links to further reading on this topic as well.
Thanks in advance.
Assuming that script files are in a folder called Scripts, the following target file will raise an error if there's any file with build action set to Content and if their path contains the word Scripts.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0"
DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="MakeSureSqlFilesAreSetToEmbeddedAsResource" BeforeTargets="PrepareForBuild">
<Error Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(Content.FullPath)', 'Scripts'))"
Text="This Content file is not configured as Embedded Resource: [%(Content.FullPath)]" />
</Target>
</Project>
If you're dealing with developers who forget to set their scripts as Embedded Resource, the above should be enough (though not comprehensive), mainly because Visual Studio sets the build action for new files to Content by default. If you want to make it bullet proof simply repeat the Error tag and replace Content with all possible build actions (except EmbeddedResource).

Run other than the DefaultTarget for a project configuration under Visual Studio 2010

I've a MSBuild target in my csproj to copy files and folders of my web application to a target path after build.
<Target Name="PublishToFileSystem" DependsOnTargets="PipelinePreDeployCopyAllFilesToOneFolder">
...
If I call MSBuild via command line with the target "PublishToFileSystem" everything works fine.
But now I want to "use" this target also for a special configuration in Visual Studio (like Release, Debug, ...).
How can I assign a configuration to another target than the DefaultTarget "Build" set in the project with DefaultTargets:
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Thanks, Konrad
Try to use AfterBuild target instead of PublishToFileSystem:
<Target Name="AfterBuild" DependsOnTargets="PipelinePreDeployCopyAllFilesToOneFolder">
or check Overriding Predefined Targets on MSDN
If you want to do this for a specific solution configuration and you're suffering from ciruclar dependencies as I was, the easiest thing I could come up with is writing your own Target to use as default target. That target starts other targets based on a condition on the configuration.
<Target Name="CustomBuild">
<CallTarget Targets="SignAndroidPackage" Condition="'$(Configuration)' == 'UITest'"/>
<CallTarget Targets="Build" Condition="'$(Configuration)' != 'UITest'"/>
</Target>
And then simply change the Default target at the top of the project definition to that CustomBuild target.
<Project DefaultTargets="CustomBuild" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Here's an approach that might suit your need: run a custom msbuild target from VisualStudio
(this is trick #78 in the book MSBuild Trickery)

Resources