msbuild - what are rules for scope/inheritance of properties/items? - msbuild

I have the following definitions in my working msbuild project...
<MSBuild
Projects="$(MSBuildProjectFile)"
Condition="'#(FilesToCompile)' != ''"
Targets="buildcpp"
Properties="CPPFILE=%(FilesToCompile.FullPath);OBJFILE=$(ObjectFolder)\%(FilesToCompile.Filename).doj;IncludeDirs=$(IncludeDirs)"
/>
...followed by the definition of the target.
Notice how the definition of the target contains a call to another target compilecpp...
<Target Name="buildcpp">
<PropertyGroup>
<CompileDefines Condition="'$(PreprocessorDefinitions)' != ''">-D$(PreprocessorDefinitions.Replace(";"," -D"))</CompileDefines>
</PropertyGroup>
<Exec
EchoOff="true"
StandardOutputImportance="low"
StandardErrorImportance="low"
IgnoreExitCode="true"
ConsoleToMSBuild="true"
Command='
"$(CompilerExe)" ^
$(HWProcessor) ^
$(IncludeDirs) ^
$(CompilerOptions) ^
$(CompileDefines) ^
"$(CPPFILE)" ^
-MM
'>
<Output TaskParameter="ConsoleOutput" PropertyName="output_cppdeps"/>
<Output TaskParameter="ExitCode" PropertyName="exitcode_cppdeps"/>
</Exec>
<MSBuild
Projects="$(MSBuildProjectFile)"
Condition="'$(exitcode_cppdeps)' == '0'"
Targets="compilecpp"
Properties="INPUTFILES=$(BuildCppDeps)"
/>
</Target>
...which uses the property $(OBJFILE) even though it was never passed in by the caller
<Target Name="compilecpp" Inputs="$(INPUTFILES)" Outputs="$(OBJFILE)">
<Message Importance="high" Text="$(CPPFILE): Compiling..."/>
...
QUESTION
Since this msbuild works, I can infer that $(OBJFILE) is accessible; why is it accessible? What are the scope rules for properties?

When using the <MSBuild> task, this performs a new msbuild run similar to running msbuild.exe with arguments. In particular, passing in properties is similar to passing /p:PropName=Value arguments - it defines new "global properties" for this run.
During this inner build, the property is still there and accessible by additional inner builds (buildcpp -> compilecpp) unless overwritten. So OBJFILE is only accessible in compilecpp because it was defined as global property for a parent msbuild run. If compilecpp was somehow invoked directly, the property would not be defined (assuming it not set somewhere else). When you want to stop forwarding a global property, you'd need to use the MSBuild task's RemoveProperties parameter. So if you set RemoveProperties="OBJFILE", then it won't be pased on.
Fyi, in .NET Core projects, RemoveProperties is used to not forward a RuntimeIdentifier from a self-contained apps to referenced projects, which may not be able to build with this property set (due to missing restore information).
For more information, read the properties documentation - especially the section about global properties - and the MSBuild Task documentation (important part is the description for the Properties parameter). However, the fact that global properties are passed on isn't explicitly documented (though implied by the RemoveProperties).
Update: the documentation for global properties was updated to describe this behavior:
Global properties are also forwarded to child projects unless the
RemoveProperties attribute of the MSBuild task is used to specify the
list of proerties not to forward.

Related

How does task MSBuild loop over files

<Target Name="Build">
...
<MSBuild
Projects="$(MSBuildProjectFile)"
Condition="'#(FilesToCompile)' != ''"
Targets="buildcpp"
Properties="CPPFILE=%(FilesToCompile.FullPath);OBJFILE=$(ObjectFolder)\%(FilesToCompile.Filename).doj;IncludeDirs=$(IncludeDirs)"
/>
FilesToCompile is an ItemGroup of all .cpp files.
When I look at the build log, it shows the target buildcpp being run for each of the files in CPPFILE.
I understand that is what I logically want to happen but my question is, what rule of element <MSBuild> or the MSBuild schema causes task MSBuild to be executed for each value of CPPFILE?
In short, where in the documentation does it state that is what will happen?
I want to pass in an entire ItemGroup once instead of calling the MSBuild target once for each item.
The msbuild concept this is based on is called "batching" - in your case task batching (see MSBuild's task batching documentation).
Any task that contains a %() reference to an item group will be split up into batches that share the same metadata and the task will be executed once for each batch. When using built-in metadata like Identity or FullPath, this essentially means "execute this task for ever item", though there can also be more complex use cases.

How to check if a MSBuild-Task fails if using ContinueOnError=true

I am running the MSBuild task with ContinueOnError=true:
<MSBuild Projects="#(ComponentToDeploy)"
Targets="$(DeploymentTargets)"
Properties="$(CommonProperties);%(AdditionalProperties)"
ContinueOnError="true"
Condition="%(Condition)"/>
So my build always succeeds.
Is there a way to find out if any error occurs?
I could not find any Output of the MSBuild task containing this information.
The only way I know is to parse the log file for errors but it looks like a workaround for me.
(I am using MSBuild 4.0)
This is an answer to the last feedback of #Ilya.
I'm using feedback/answer because of the length and formatting restrictions of the comments.
Log is scoped to individual targets or to be more specific tasks...
This was indeed the first question arose when I was reading your comment with the suggestion to use Log.HasLoggedErrors: "Was is the scope of the Log?".
Unfortunately I was not be able to finde a proper documentation. MSND does not help much...
Why did you know it is scoped to the task?
I'm not in doubt about your statement at all! I'm just wondering if there is a proper documentation somewhere..
(I haven't been using MSBuild for years ;-)
In any case, what are you building as project?
My test projects are very simple.
MyTest.project
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="ElenasTarget" ToolsVersion="4.0">
<UsingTask AssemblyFile="$(MSBuildProjectDirectory)\MyCompany.Tools.MSBuild.Tasks.dll" TaskName="MSBuildWithHasLoggedErrors" />
<ItemGroup>
<MyProjects Include="CopyNotExistingFile.proj" />
</ItemGroup>
<Target Name="ElenasTarget">
<MSBuildWithHasLoggedErrors Projects="#(MyProjects)" ContinueOnError="true" >
<Output TaskParameter="HasLoggedErrors" PropertyName="BuildFailed" />
</MSBuildWithHasLoggedErrors>
<Message Text="BuildFailed=$(BuildFailed)" />
</Target>
</Project>
The CopyNotExistingFile.proj just tries to copy a file that does not exist:
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Target1" ToolsVersion="4.0">
<Target Name="Target1">
<Copy SourceFiles="C:\lalala.bum" DestinationFiles="C:\tralala.bam" />
</Target>
</Project>
And this is my custom task MSBuildWithHasLoggedErrors
namespace MyCompany.Tools.MSBuild.Tasks
{
public class MSBuildWithHasLoggedErrors : Microsoft.Build.Tasks.MSBuild
{
[Output]
public bool HasLoggedErrors { get; private set; }
public override bool Execute()
{
try
{
base.Execute();
HasLoggedErrors = Log.HasLoggedErrors;
}
catch (Exception e)
{
Log.LogErrorFromException(e, true);
return false;
}
return true;
}
}
}
If I build my MyTest.proj the HasLoggedErrorswill be set to false although an error (MSB3021) was logged(?) to the console logger:
Project "C:\Users\elena\mytest.proj" on node 1 (default targets).
Project "C:\Users\elena\mytest.proj" (1) is building "C:\Users\elena\CopyNotExistingFile.proj" (2) on node 1 (default targets).
Target1:
Copying file from "C:\lalala.bum" to "C:\tralala.bam".
C:\Users\elena\CopyNotExistingFile.proj(5,4): error MSB3021: Unable to copy file "C:\lalala.bum" to "C:\tralala.bam". Could not find file 'C:\lalala.bum'.
Done Building Project "C:\Users\elena\CopyNotExistingFile.proj" (default targets) -- FAILED.
ElenasTarget:
BuildFailed=False
Done Building Project "C:\Users\elena\mytest.proj" (default targets).
Build succeeded.
My expectation was HasLoggedErrors would be set to true.
one way is to build self but with different target, for example your DefaultTargets one launches your custom MSBuildWrapper task pointing to itself (ie $(MSBuildProjectFile)) but with a different target that does other builds, copies
I've already tried it (that were my investigations I meant in my post). Unfortunately it doesn't work either :-(
(I am aware you said in theory).
My new single project looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="ElenasTarget" ToolsVersion="4.0">
<UsingTask AssemblyFile="$(MSBuildProjectDirectory)\MyCompany.Tools.MSBuild.Tasks.dll" TaskName="MSBuildWithHasLoggedErrors" />
<Target Name="ElenasTarget">
<MSBuildWithHasLoggedErrors Projects="$(MSBuildProjectFile)" Targets="CopyNotExistingFile" ContinueOnError="true" >
<Output TaskParameter="HasLoggedErrors" PropertyName="BuildFailed" />
</MSBuildWithHasLoggedErrors>
<Message Text="BuildFailed=$(BuildFailed)" />
</Target>
<Target Name="CopyNotExistingFile" >
<Copy SourceFiles="C:\lalala.bum" DestinationFiles="C:\tralala.bam" />
</Target>
</Project>
If I build this project HasLoggedErrors will still be set to false.
(Furthermore, my "real" build I'm currently maintaining is much complexer containing several project files with targets... so I can't pack them all in a single project file ).
or writing custom logger and passing it through command line
That was my last hope!
My "real" build has a custom logger passed through the command line (I didn't use it for my test project for the sake of simplicity). That is actually producing the log (a XML file) I'm going to parse to find out if any errors have been logged.
BTW, I thought the console logger is a kind of "global" logger. Am I wrong?
Anyway, the custom logger does not help neither, the Log.HasLoggedErrors is still set to false.
Is there some way I am not aware of to reference a particular logger (e.g. my custom logger) to ask if it has logged any errors?
It really looks like Log is scoped to individual targets.
Hmm... if the reflection on the buildengine instance is the last resort I would still prefer parsing the log.
(Don't blame me! :-) )
My decision
After some investigations I've decided to stick with my initial solution: parse the log to find out if the build failed.
Check my comments to see why I prefer that to the suggestions have been provided so far.
If someone has some other ideas do not hesitate to share :-)
(Otherwise this question can be closed, I suppose...)
The MSBuildLastTaskResult reserved property will be set to True if the last task succeeded and False if the last task failed:
<MSBuild Projects="#(ComponentToDeploy)"
Targets="$(DeploymentTargets)"
Properties="$(CommonProperties);%(AdditionalProperties)"
ContinueOnError="true"
Condition="%(Condition)" />
<Message Text="MSBuild failed!" Condition="'$(MSBuildLastTaskResult)' == 'False'" />
I believe this was introduced with MSBuild v4.0.
I know this thread is a bit old, but another possible solution, as I presume you needed to know that build failed in order to execute some "final task", is to use:
<OnError ExecuteTargets="FinalReportTarget;CleanupTarget" />
That would fail the build in case of error, but execute the "FinalReportTarget" and "CleanupTarget".
ContinueOnError="true" is not needed in this case.
You could capture TargetOutputs and check them for error conditions afterwards, but that's still quite hackish.
If you only want to check if MSBuild task failed, use Exec task. Set IgnoreExitCode to true and check ExitCode output value. If not zero, something is wrong.
If you need the list of build errors, use /fileloggerparameters command line switch to log errors only to some specific file:
/flp1:logfile=errors.txt;errorsonly
But if another task inside some target (e.g. Copytask) raised an error the Log.HasLoggedErrors returns false.
Didn't know comments have length limits...
Log is scoped to individual targets or to be more specific tasks, and (as far as I'm aware) there is no way to get a "global" one, may be through reflection on the buildengine instance, or writing custom logger and passing it through command line. In any case, what are you building as project? HasLoggedErrors works as expected (and has been working unchanged for years), it shows if project being built logged any errors. It doesn't, and shouldn't, have any control over logging of other tasks (that might use other types of loggers). If you want a global one, one way is to build self but with different target, for example your DefaultTargets one launches your custom MSBuildWrapper task pointing to itself (ie $(MSBuildProjectFile)) but with a different target that does other builds, copies, etc, in theory it should simulate a global HasLoggedErrors...

MSBuild 4.0 property functions cannot access properties inside of them

Is it a limitation of MSBuild 4.0 property functions that I cannot access a property from inside of one?
Here is an example that works just fine:
<PropertyGroup>
<PartialConnection>$(TargetConnectionString.Substring( 0 + 12))</PartialConnection>
</PropertyGroup>
Here is another example that doe snot work. (I replace the 0 with another property)
<PropertyGroup>
<LocationOfDataSource>$(TargetConnectionString.IndexOf("Data Source="))</LocationOfDataSource>
</PropertyGroup>
<Message Importance="high" Text="Location is = $(LocationOfDataSource)"/>
<PropertyGroup>
<PartialConnection>$(TargetConnectionString.Substring( $(LocationOfDataSource) + 12))</PartialConnection>
</PropertyGroup>
this outputs
Location is = 0
Error MSB4184: The expression ""Data Source=MySQLServer;Integrated Security=True;Pooling=False".Substring(0 + 12)" cannot be evaluated. Input string was not in a correct format.
I took the output and plugged into a console app and it works just fine. I have tried several variations and I they always fail when I put a property inside a property function. (I even tried access the same property twice in a my property function and that failed too.)
Do property functions not support accessing properties?
I think my issue was assuming that math came for free.
I needed to do this kind of thing:
<PropertyGroup>
<LocationOfDataSource>$(TargetConnectionString.IndexOf("Data Source="))</LocationOfDataSource>
<LenthOfDataSourceString>12</LenthOfDataSourceString>
<LocationOfEndOfDataSourceString>$([MSBuild]::Add($(LocationOfDataSource), $(LenthOfDataSourceString)))</LocationOfEndOfDataSourceString>
<PartialConnectionString>$(TargetConnectionString.Substring($(LocationOfEndOfDataSourceString)))</PartialConnectionString>
</PropertyGroup>
Note that I am adding using Add($(Property), $(Property)) in this version. Add is one of the built-in MSBuild Property Functions (since MSBuild 4).
It seems to be working now.

How can task parameters be defaulted in MSBuild

In mytask.targets, I have something like:
<UsingTask TaskName="DoStuff" AssemblyFile="....etc....."/>
<PropertyGroup>
<RequiredParamDefault>hello</RequiredParamDefault>
</PropertyGroup>
This task currently has a required parameter (which could be changed from required if necessary).
When the task is used:
<DoStuff RequiredParam="$(RequiredParamDefault)" OtherParam="wobble"/>
Currently, RequiredParam has to be specified everytime. Is there anyway that when UsingTask is defined, the default can be set up so it doesn't have to be specified on every use of DoStuff?
I know the default could be hardcoded in the assembly, but I'd like to be able to define different defaults with different UsingTask statements.
Thanks.
You can't do this at the UsingTask or Task but instead you can using properties that you pass into the task. For example.
<Target>
<PropertyGroup>
<ReqParam Condition=" '$(ReqParam)'=='' ">Param-Default-Value</ReqParam>
</PropertyGroup>
<DoStuff RequiredParam="$(ReqParam)" OtherParam="wobble"/>
</Target>
In this case I define the property ReqParam to be Param-Default-Value only if the property doesn't already have a value. This is not exactly what you are looking for, but it may be your best option unless you can change the task itself.

Find MSBuildProjectDirectory Parent Directory

MSBuild 3.5
I have the following project structure:
trunk/MainSolution.sln
trunk/Build/MyBuild.Proj
trunk/Library/...
trunk/etc...
So far, I've been using the following property to find out the project root folder:
<RootFolder>$(MSBuildProjectDirectory)\..\</RootFolder>
Everything was working great, until I tried using a copy task that relied on this path. It is not resolving correctly. I basically end up getting something like this which is not valid:
C:\Projects\MyProject\Trunk\Build\..\CodeAnalysis\myfile.xml
So basically, I need to get the full path for (MSBuildProjectDirectory)'s Parent.
Item metadata is your friend!
<Target Name="GetMSBuildProjectParentDirectory">
<!-- First you create the MSBuildProject Parent directory Item -->
<CreateItem Include="$(MSBuildProjectDirectory)\..\">
<Output ItemName="MSBuildProjectParentDirectory" TaskParameter="Include"/>
</CreateItem>
<!-- You can now retrieve its fullpath using Fullpath metadata -->
<Message Text="%(MSBuildProjectParentDirectory.Fullpath)"/>
<!-- Create a property based on parent fullpath-->
<CreateProperty Value="%(MSBuildProjectParentDirectory.Fullpath)">
<Output PropertyName="CodeFolder" TaskParameter="Value"/>
</CreateProperty>
</Target>
Nowadays with MSBuild 4.0 and above you don't want to use CreateItem or CreateProperty tasks anymore. What you are asking for can be solved easily with msbuild property functions:
<!-- Prints the parent directory's full path. -->
$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..'))
If you just want to read the parent directory's folder name you can combine the above statement with the GetFileName property function:
$([System.IO.Path]::GetFileName('$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..'))'))
A bit verbose but much better than the other answer as this works outside of targets and can be assigned to a property.
In case someone like me is still interested in this, here is how I did it in 2022 ^_^
<PropertyGroup>
<ParentFolderPath>$([System.IO.Directory]::GetParent($(MSBuildProjectDirectory)))</ParentFolderPath>
<ParentFolder>$([System.IO.Path]::GetFileName($(ParentFolderPath)))</ParentFolder>
...
</PropertyGroup>
I'm using this technique to auto-name the assemblies and default namespaces in the complex solutions.
<AssemblyName>$(ParentFolder).$(MSBuildProjectName)</AssemblyName>
<RootNamespace>$(ParentFolder).$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>