msbuild icon indicating copy to clipboard operation
msbuild copied to clipboard

Evaluate a project from the command line

Open ltrzesniewski opened this issue 5 years ago • 12 comments

This is a feature request: Add a command line switch which would make MSBuild evaluate a project and output the value of a property or a list of items. No target would be called. This would be useful for scripting purposes.

Some examples of what I mean:

C:\Project> msbuild SomeProject.csproj -evaluate:'$(TargetPath)'
C:\Project\SomeProject\bin\Debug\SomeProject.exe
C:\Project> msbuild SomeProject.csproj -evaluate:'@(Compile)'
Program.cs
Foo.cs
Bar.cs
C:\Project> msbuild SomeProject.csproj -evaluate:'@(Compile->%(FileName))'
Program
Foo
Bar

Or a simpler version:

C:\Project> msbuild SomeProject.csproj -evaluateProperty:TargetPath
C:\Project\SomeProject\bin\Debug\SomeProject.exe
C:\Project> msbuild SomeProject.csproj -evaluateItems:Compile
Program.cs
Foo.cs
Bar.cs

ltrzesniewski avatar Nov 07 '18 11:11 ltrzesniewski

Take a look at MSBuildDumper and see if it fits your needs: https://github.com/KirillOsenkov/MSBuildTools

https://github.com/KirillOsenkov/MSBuildTools/blob/master/src/MSBuildDumper/MSBuildDumper.cs

KirillOsenkov avatar Nov 17 '18 19:11 KirillOsenkov

@KirillOsenkov thanks, but I know that getting the evaluated values is pretty easy with the MSBuild NuGet package.

To clarify, I simply thought that having this possibility from MSBuild itself (without requiring additional tools) would be a useful feature, and I think it would make sense to support that. I wished several times that MSBuild offered that.

I could contribute that feature if the MSBuild team thinks it would be valuable.

ltrzesniewski avatar Nov 17 '18 23:11 ltrzesniewski

Can you elaborate on "This would be useful for scripting purposes."? What specifically?

I would have sworn that we already had a work item for this but I don't see one. I think it's a fine idea and would help with various debugging scenarios. I would lean toward the simpler only-dump-values interface; I'm not sure how difficult plumbing the transform mechanism in would be and I think you could get most of the value from just items/properties. I'd suggest that the output should match the text logger, so combining requests would be more clear:

$ msbuild SomeProject.csproj -evaluateItems:Compile -evaluateProperty:TargetPath
TargetPath = s:\msbuild\artifacts\Debug\bin\Microsoft.Build.Framework\net472\Microsoft.Build.Framework.dll
Compile
    ..\Shared\BinaryWriterExtensions.cs
        Link = Shared\BinaryWriterExtensions.cs
    ..\Shared\Constants.cs
        Link = Shared\Constants.cs
    BuildEngineResult.cs
    BuildErrorEventArgs.cs
    BuildEventArgs.cs
    BuildEventContext.cs

rainersigwald avatar Nov 19 '18 15:11 rainersigwald

Can you elaborate on "This would be useful for scripting purposes."? What specifically?

I originally needed to get the OutDir property value in a script in order to zip the build output.

I couldn't just set my own OutDir because some referenced projects use multi-targeting and all targets would end up in the same output directory, thus overwriting each other. I ended up solving this issue differently, but I wished I had that feature nevertheless.

I'd suggest that the output should match the text logger, so combining requests would be more clear

The problem I see with this approach is that it's well suited for human inspection, but less useful for scripting. My idea was to make MSBuild behave like a Unix utility in this case: only output the desired value so it can be easily consumed by a script or by another tool which could be piped to the MSBuild output.

With this approach, a script would additionally have to strip the TargetPath = prefix, and there would probably be issues if the value is multi-line.

Also, given that the debugging experience is very good using the binary log viewer, I don't think a text output aimed at debuggability would provide a compelling benefit over that.

ltrzesniewski avatar Nov 19 '18 17:11 ltrzesniewski

That's interesting. What would you do about item metadata?

rainersigwald avatar Nov 19 '18 17:11 rainersigwald

Well, actually item metadata is the reason I suggested the "full" syntax (-evaluate) in the first place. I supposed it shouldn't be too hard to evaluate MSBuild expressions since they're also evaluated in the execution phase, and that this mode could be an alternate "execution phase" where you basically evaluate a single expression.

So suppose I want a list of linked files from your example, here's how I could get it:

$ msbuild SomeProject.csproj -evaluate:"@(Compile->'%(Identity):%(Link)')"
..\Shared\BinaryWriterExtensions.cs:Shared\BinaryWriterExtensions.cs
..\Shared\Constants.cs:Shared\Constants.cs
BuildEngineResult.cs:
BuildErrorEventArgs.cs:
BuildEventArgs.cs:
BuildEventContext.cs:

Line breaks could still be an issue though, but a unique delimiter could be inserted at the end of each line to handle these if needed.

ltrzesniewski avatar Nov 19 '18 19:11 ltrzesniewski

This would be super useful for us. We worked around that currently but our workaround broke and we now have lots of work backporting script-changes :-(

Scordo avatar Sep 09 '20 13:09 Scordo

There's a related consideration here - properties and items are not static, they can be modified during the evaluation of a particular target. That implies to me that the request might look something like to specify a target to run (and maybe just evaluate the project if none is specified):

dotnet msbuild -t:TARGET_TO_RUN --evaluate "EVAL_EXPRESSION"

or

dotnet msbuild -t:TARGET_TO_RUN --evaluateProperty "PROP_NAME"

or

dotnet msbuild -t:TARGET_TO_RUN --evaluateItems "ITEMS_NAME"

(though labeling of the returned values might be interesting)

baronfel avatar Jan 25 '22 19:01 baronfel

@baronfel Great point. We do this in the Docker extension for VSCode to determine the path of the Blazor static web assets manifest, in order to "containerize" it by adjusting the paths. The target "ResolveStaticWebAssetsConfiguration" has to be run before the necessary information is available.

More on that here: https://github.com/microsoft/vscode-docker/blob/main/resources/netCore/GetBlazorManifestLocations.targets

bwateratmsft avatar Jan 25 '22 20:01 bwateratmsft

I just want to show our usecase. We do have a msbuild file thats integrated in most of our projects. It contains version info which is used to set assembly metadata like FileVersion, AssemblyVersion and so on. There are properties which are static and calculated using values of other properties. We do have Powershell scripts that have to read this values to do deployments and calculate paths and so on. We've used the msbuild api in the past to evaluate the properties in powershell but this broke with a new version of msbuild/powershell. So we now have a proxy msbuild-script which gets a path to another msbuild-script and a list of properties to evaluate and then does the evaluation and pretty prints the evaluated properties and so on. So we now use msbuild to run the proxy-msbuild script which then prints everything to console and we parse it in powershell.

Here is the script we want to have values of:

<Project xmlns="http://XXXs.microsoft.com/developer/msbuild/2003">
  <Import Project="$(USERPROFILE)\Pre-XXX-VersionInfo.msb.xml" Condition="exists('$(USERPROFILE)\Pre-XXX-VersionInfo.msb.xml')" />
  <PropertyGroup>
    <!-- The XXX Release-Year -->
     <XXXProductYear Condition="'$(XXXProductYear)' == ''">2022</XXXProductYear>
    <!-- The XXX Service-Pack or empty for a major release -->
     <XXXProductServicePack Condition="'$(XXXProductServicePack)' == ''">0</XXXProductServicePack>
    <!-- The suffix appended to the product name, for example "SP1" -->
    <XXXProductSuffix Condition="'$(XXXProductSuffix)' == '' and '$(XXXProductServicePack)' != '0'"> SP$(XXXProductServicePack)</XXXProductSuffix>
    <!-- The full product name used in AssemblyInformationalVersion -->
    <XXXProductName Condition="'$(XXXProductName)' == ''">XXX $(XXXProductYear)$(XXXProductSuffix)</XXXProductName>
    <XXXCompanyName>XXX Company</XXXCompanyName>
    <XXXCopyright>Copyright © XXX</XXXCopyright>
    <!-- START: Release Version -->
    <!-- This group of versions will be used as the release version and for help documentation -->
    <!-- $(XXXVersionMajor).$(XXXVersionMinor).$(XXXVersionBuild).$(XXXVersionRevision) -->
    <!-- Major version position -->
     <XXXVersionMajor Condition="'$(XXXVersionMajor)' == ''">12</XXXVersionMajor>
    <!-- Is never changed currently. Defaults to zero -->
    <XXXVersionMinor Condition="'$(XXXVersionMinor)' == ''">0</XXXVersionMinor>
    <!-- Service-Pack position -->
    <XXXVersionBuild Condition="'$(XXXVersionBuild)' == ''">$(XXXProductServicePack)</XXXVersionBuild>
    <!-- Is never changed currently. Defaults to zero -->
    <XXXVersionRevision Condition="'$(XXXVersionRevision)' == ''">0</XXXVersionRevision>
    <!-- END: Release Version -->
    <!-- START: Assembly Version -->
    <!-- This group of versions will be used in future to automatically manipulate assembly version of assemblyinfo.cs files -->
    <!-- $(XXXVersionAssemblyMajor).$(XXXVersionAssemblyMinor).$(XXXVersionAssemblyBuild).$(XXXVersionAssemblyRevision) -->
    <!-- Major verison used for references between assemblies -->
    <XXXVersionAssemblyMajor Condition="'$(XXXVersionAssemblyMajor)' == ''">$(XXXVersionMajor)</XXXVersionAssemblyMajor>
    <!-- Is never changed currently. Defaults to zero -->
    <XXXVersionAssemblyMinor Condition="'$(XXXVersionAssemblyMinor)' == ''">0</XXXVersionAssemblyMinor>
    <!-- Is never changed currently. Defaults to zero -->
    <XXXVersionAssemblyBuild Condition="'$(XXXVersionAssemblyBuild)' == ''">$(XXXProductServicePack)</XXXVersionAssemblyBuild>
    <!-- Is never changed currently. Defaults to zero -->
    <XXXVersionAssemblyRevision Condition="'$(XXXVersionAssemblyRevision)' == ''">0</XXXVersionAssemblyRevision>
    <!-- END: Assembly Version -->
    <!-- START: File Version -->
    <!-- This group of versions will be used in future to automatically manipulate file version of assemblyinfo.cs files -->
    <!-- $(XXXVersionFileMajor).$(XXXVersionFileMinor).$(XXXVersionFileBuild).$(XXXVersionFileRevision) -->
    <!-- Major version used for file details in windows explorer -->
    <XXXVersionFileMajor Condition="'$(XXXVersionFileMajor)' == ''">$(XXXVersionMajor)</XXXVersionFileMajor>
    <!-- Is never changed currently. Defaults to zero -->
    <XXXVersionFileMinor Condition="'$(XXXVersionFileMinor)' == ''">0</XXXVersionFileMinor>
    <!-- Is never changed currently. Defaults to zero -->
    <XXXVersionFileBuild Condition="'$(XXXVersionFileBuild)' == ''">$(XXXProductServicePack)</XXXVersionFileBuild>
    <!-- can be changed for each modified assembly manually. -->
    <XXXVersionFileRevision Condition="'$(XXXVersionFileRevision)' == ''">0</XXXVersionFileRevision>
    <!-- END: File Version -->
    <!-- Make sure our target file changes will take effect when a new build is done -->
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
    <GenerateXXXAssemblyInfo Condition="'$(GenerateXXXAssemblyInfo)' == ''">true</GenerateXXXAssemblyInfo>
  </PropertyGroup>
  <PropertyGroup Condition="'$(GenerateXXXAssemblyInfo)' == 'true'">
    <GenerateXXXAssemblyVersionAttribute Condition="'$(GenerateXXXAssemblyVersionAttribute)' == ''">true</GenerateXXXAssemblyVersionAttribute>
    <GenerateXXXAssemblyFileVersionAttribute Condition="'$(GenerateXXXAssemblyFileVersionAttribute)' == ''">true</GenerateXXXAssemblyFileVersionAttribute>
    <GenerateXXXAssemblyInformationalVersionAttribute Condition="'$(GenerateXXXAssemblyInformationalVersionAttribute)' == ''">true</GenerateXXXAssemblyInformationalVersionAttribute>
    <GenerateXXXAssemblyCompanyAttribute Condition="'$(GenerateXXXAssemblyCompanyAttribute)' == ''">true</GenerateXXXAssemblyCompanyAttribute>
    <GenerateXXXAssemblyCopyrightAttribute Condition="'$(GenerateXXXAssemblyCopyrightAttribute)' == ''">true</GenerateXXXAssemblyCopyrightAttribute>
  </PropertyGroup>
  <Target Name="GenerateXXXAssemblyInfoFile" BeforeTargets="CoreCompile" DependsOnTargets="PrepareForBuild;BeforeCoreGenerateXXXAssemblyInfoFile;CoreGenerateXXXAssemblyInfoFile" Condition="'$(GenerateXXXAssemblyInfo)' == 'true'" />
  <!-- We have to create the property here, otherwise $(IntermediateOutputPath) would be empty -->
  <Target Name="BeforeCoreGenerateXXXAssemblyInfoFile">
    <PropertyGroup>
      <XXXAssemblyInfoFilePath Condition="'$(XXXAssemblyVersionInfoFilePath)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).XXXAssemblyInfo$(DefaultLanguageSourceExtension)</XXXAssemblyInfoFilePath>
    </PropertyGroup>
  </Target>
  <Target Name="CoreGenerateXXXAssemblyInfoFile" Condition="'$(Language)'=='VB' or '$(Language)'=='C#'" Inputs="$(MSBuildAllProjects)" Outputs="$(XXXAssemblyInfoFilePath)">
    <PropertyGroup>
      <GitShortSHA Condition="'$(CI_COMMIT_SHORT_SHA)' != ''">$(CI_COMMIT_SHORT_SHA)</GitShortSHA>
    </PropertyGroup>
    <Exec Command="git rev-parse --short=8 HEAD" ConsoleToMSBuild="true" EchoOff="true" WorkingDirectory="$(SourceCodePath)" ContinueOnError="true" Condition="'$(GitShortSHA)' == ''">
      <Output TaskParameter="ConsoleOutput" PropertyName="GitShortSHA" />
    </Exec>
    <ItemGroup>
      <XXXAssemblyAttribute Include="System.Reflection.AssemblyVersionAttribute" Condition="'$(GenerateXXXAssemblyVersionAttribute)' == 'true'">
        <_Parameter1>$(XXXVersionAssemblyMajor).$(XXXVersionAssemblyMinor).$(XXXVersionAssemblyBuild).$(XXXVersionAssemblyRevision)</_Parameter1>
      </XXXAssemblyAttribute>
      <XXXAssemblyAttribute Include="System.Reflection.AssemblyFileVersionAttribute" Condition="'$(GenerateXXXAssemblyFileVersionAttribute)' == 'true'">
        <_Parameter1>$(XXXVersionFileMajor).$(XXXVersionFileMinor).$(XXXVersionFileBuild).$(XXXVersionFileRevision)</_Parameter1>
      </XXXAssemblyAttribute>
      <XXXAssemblyAttribute Include="System.Reflection.AssemblyInformationalVersionAttribute" Condition="'$(GenerateXXXAssemblyInformationalVersionAttribute)' == 'true'">
        <_Parameter1>$(XXXVersionAssemblyMajor).$(XXXVersionAssemblyMinor).$(XXXVersionAssemblyBuild).$(XXXVersionAssemblyRevision) $(XXXProductName) ($(GitShortSHA))</_Parameter1>
      </XXXAssemblyAttribute>
      <XXXAssemblyAttribute Include="System.Reflection.AssemblyCompanyAttribute" Condition="'$(GenerateXXXAssemblyCompanyAttribute)' == 'true'">
        <_Parameter1>$(XXXCompanyName)</_Parameter1>
      </XXXAssemblyAttribute>
      <XXXAssemblyAttribute Include="System.Reflection.AssemblyCopyrightAttribute" Condition="'$(GenerateXXXAssemblyCopyrightAttribute)' == 'true'">
        <_Parameter1>$(XXXCopyright)</_Parameter1>
      </XXXAssemblyAttribute>
    </ItemGroup>
    <ItemGroup>
      <!-- Ensure the generated assemblyinfo file is not already part of the Compile sources -->
      <Compile Remove="$(XXXAssemblyInfoFilePath)" />
    </ItemGroup>
    <Error Text="Variable XXXVersionAssemblyMajor with value '$(XXXVersionAssemblyMajor)' is not a number.'" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch($(XXXVersionAssemblyMajor), '^\d+$'))" />
    <Error Text="Variable XXXVersionAssemblyMinor with value '$(XXXVersionAssemblyMinor)' is not a number.'" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch($(XXXVersionAssemblyMinor), '^\d+$'))" />
    <Error Text="Variable XXXVersionAssemblyBuild with value '$(XXXVersionAssemblyBuild)' is not a number.'" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch($(XXXVersionAssemblyBuild), '^\d+$'))" />
    <Error Text="Variable XXXVersionAssemblyRevision with value '$(XXXVersionAssemblyRevision)' is not a number.'" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch($(XXXVersionAssemblyRevision), '^\d+$'))" />
    <Error Text="Variable XXXVersionFileMajor with value '$(XXXVersionFileMajor)' is not a number.'" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch($(XXXVersionFileMajor), '^\d+$'))" />
    <Error Text="Variable XXXVersionFileMinor with value '$(XXXVersionFileMinor)' is not a number.'" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch($(XXXVersionFileMinor), '^\d+$'))" />
    <Error Text="Variable XXXVersionFileBuild with value '$(XXXVersionFileBuild)' is not a number.'" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch($(XXXVersionFileBuild), '^\d+$'))" />
    <Error Text="Variable XXXVersionFileRevision with value '$(XXXVersionFileRevision)' is not a number.'" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch($(XXXVersionFileRevision), '^\d+$'))" />
    <WriteCodeFragment AssemblyAttributes="@(XXXAssemblyAttribute)" Language="$(Language)" OutputFile="$(XXXAssemblyInfoFilePath)">
      <Output TaskParameter="OutputFile" ItemName="Compile" />
      <Output TaskParameter="OutputFile" ItemName="FileWrites" />
    </WriteCodeFragment>
  </Target>
  <Import Project="$(USERPROFILE)\Post-XXX-VersionInfo.msb.xml" Condition="exists('$(USERPROFILE)\Post-XXX-VersionInfo.msb.xml')" />
</Project>

And thats the proxy-script:

<Project DefaultTargets="PrintPropertiesToEvaluate">
	<PropertyGroup>
		<ProjectFilePath></ProjectFilePath>
		<PropertiesToEvaluate></PropertiesToEvaluate>
		<PropertiesToInitialize></PropertiesToInitialize>
		<IncludePropertyNames>false</IncludePropertyNames>
		<PropertyValueSeparator>=</PropertyValueSeparator>
	</PropertyGroup>

	<Target Name="PrintPropertiesToEvaluate">
		<Error Text="The property 'ProjectFilePath' is empty, but is required." Condition="'$(ProjectFilePath)' == ''" />
		<Error Text="Project '$(ProjectFilePath)' does not exist." Condition="!Exists('$(ProjectFilePath)')" />
		<Error Text="The property 'PropertiesToEvaluate' is empty, but is required." Condition="'$(PropertiesToEvaluate)' == ''" />

		<EvaluateMSBuildProperties ProjectFilePath="$(ProjectFilePath)" PropertiesToEvaluate="$(PropertiesToEvaluate)" PropertiesToInitialize="$(PropertiesToInitialize)">
			<Output TaskParameter="Result" ItemName="EvaluatedProperties"/>
		</EvaluateMSBuildProperties>

		<ConsoleWriteLine Text="%(EvaluatedProperties.Identity)$(PropertyValueSeparator)%(EvaluatedProperties.Value)" Condition="$(IncludePropertyNames)"/>
		<ConsoleWriteLine Text="%(EvaluatedProperties.Value)" Condition="!$(IncludePropertyNames)"/>
	</Target>

	<UsingTask TaskName="EvaluateMSBuildProperties" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
		<ParameterGroup>
			<ProjectFilePath ParameterType="System.String" Required="true" />
			<PropertiesToEvaluate ParameterType="System.String" Required="true" />
			<PropertiesToInitialize ParameterType="System.String" Required="false" />
			<Result ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
		</ParameterGroup>
		<Task>
			<Reference Include="System.Xml" />
			<Reference Include="Microsoft.Build" />
			<Using Namespace="System.Collections.Generic" />
			<Using Namespace="Microsoft.Build.Evaluation" />

			<Code Type="Fragment" Language="cs"><![CDATA[
				Dictionary<string, string> initProperties = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);

				if (PropertiesToInitialize != null)
				{
					foreach (string initProperty in PropertiesToInitialize.Split(';'))
					{
						int equalSignIndex = initProperty.IndexOf('=');
						if (equalSignIndex == -1)
							throw new InvalidDataException(string.Format("Property definition '{0}' is invalid. It's missing a value (no equal sign).", initProperty));

						initProperties[initProperty.Substring(0, equalSignIndex)] = initProperty.Substring(equalSignIndex + 1);
					}
				}


				ProjectCollection projectCollection = new ProjectCollection(initProperties);
				Project project = projectCollection.LoadProject(ProjectFilePath);

				string[] propertyNamesToValuate = PropertiesToEvaluate.Split(';');
				Result = new ITaskItem[propertyNamesToValuate.Length];

				for (int i = 0; i < propertyNamesToValuate.Length; i++)
				{
					string propertyName = propertyNamesToValuate[i];
					string propertyValue = project.GetPropertyValue(propertyName);
					Result[i] = new TaskItem(propertyName, new Dictionary<string, string> { { "Value", propertyValue } });
				}
			]]>
			</Code>
		</Task>
	</UsingTask>

	<UsingTask TaskName="ConsoleWriteLine" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
		<ParameterGroup>
			<Text Required="false" ParameterType="System.String"/>
		</ParameterGroup>
		<Task>
			<Code Type="Fragment" Language="cs"><![CDATA[ Console.WriteLine(Text); ]]></Code>
		</Task>
	</UsingTask>
</Project>

It would be nice to be able to skip this proxy script and just use msbuild with parameters. We dont use the msbuild api in powershell anymore because it broke suddenly and we had to do lots of backporting. Hope this helps in evaluating usecases.

Regards, Scordo

Scordo avatar Jan 26 '22 08:01 Scordo

@Scordo for advanced scenarios like this I think a C# console app that uses MSBuild APIs would be a good approach.

I doubt we can ever put enough flexibility into MSBuild.exe to get what you want, and arguably, at this point just doing what you’re already doing with a proxy project is that kind of extensibility already.

KirillOsenkov avatar Jan 26 '22 17:01 KirillOsenkov

I like the idea of rendering just one interesting property (as mentioned in the OP), but I also like the idea of rendering the entire xml (as in my closed dupe issue). The fact that some variables cannot be evaluated is true, but many can be, so it's still useful.

Quick elaboration:

When everything is done by msbuild, this feature request adds no value and seems unimportant.

But not everyone uses msbuild as the primary build mechanism. We use bash for everything, and call the msbuild/dotnet CLI to perform work for c# projects. But that means we are outside msbuild's "domain", and so lack data that msbuild doesn't naturally provide - so it would be helpful to have a feature where we can get msbuild to rendering the csproj.

In a multi-platform build environment, with many containers, different technologies (not just c#), etc., shell scripting is the lowest common denominator, but acquiring the info required to perform a build is quite hard.

lonix1 avatar Jul 28 '22 00:07 lonix1

Here's a proposal for this to see if we can get it moving forward. What do people think?

Command line evaluation of MSBuild properties

We will add command-line options to MSBuild to support getting the value of properties, items, or target return values.

  • -getProperty:<propertyName> - Get the value of the specified property
  • -getItem:<itemName> - Get the value(s) of the specified item
  • -getTargetResult - Get the return values of the targets that were specified via the -target option.

If no targets are specified on the command line via the -target option, then the -getProperty and -getItem options will get the values from MSBuild evaluation, and no targets will be built. If the -target option is specified, then the property or item values returned will be the values after the build is finished (all targets have run).

By default, the requested values will be printed to the console output in text format, and any other MSBuild output will be suppressed, unless there is an error. The text format for the values will simply put each value on a separate line, and won't include any item metadata. It will be possible to get the values for multiple properties, for example -getProperty:OutputPath;TargetPath. In that case each property would be on a separate line in the text format output. It will also be possible to get values for multiple items this way, however this won't be very useful with the text format as there won't be a way to know when the values for one item stop and the next one begin.

The format for the values can be switched to json by specifying -resultsFormat:json. This format will include item metadata values, as well as supporting multiple properties, items, and target results.

The values can be saved to a file instead of printed to the console with the -resultsFile:<fileName> option. In this case the normal console log output will not be suppressed. If saving the results to a file, and the results format is not specified, it will default to using json unless the file extension of the results file is .txt.

A possible format for the json output could be as follows:

{
  "properties":
  {
    "PropertyName": "PropertyValue",
    "PropertyName2": "PropertyValue2"
  },
  "items":
  {
    "Sources":
    [
        {
            "ItemSpec": "Program.cs"
        },
        {
            "ItemSpec": "obj\\Debug\\net6.0-windows10.0.19041.0\\ConsoleTest.AssemblyInfo.cs"
        }
    ],
    "References":
    [
        {
            "ItemSpec": "C:\\Program Files\\dotnet\\packs\\Microsoft.WindowsDesktop.App.Ref\\6.0.14\\ref\\net6.0\\Accessibility.dll",
            "FileVersion": "6.0.1423.7402",
            "ReferenceSourceTarget": "ResolveAssemblyReference"
        }
    ]
  },
  "targets":
  {
    "GetTargetPath":
    {
      {
        "ItemSpec": "c:\\git\\repro\\ConsoleTest\\bin\\Debug\\net6.0-windows10.0.19041.0\\ConsoleTest.dll",
        "TargetFrameworkIdentifier": ".NETCoreApp",
        "ReferenceAssembly": "c:\\git\\repro\\ConsoleTest\\obj\\Debug\\net6.0-windows10.0.19041.0\\ref\\ConsoleTest.dll"
      }
    }
  }
}

Comments

This doesn't support evaluating arbitrary expressions from the command line, so something like -evaluate:'@(Compile->%(FileName))' isn't possible. However, the command line syntax is simpler, and you can still get more complex values by using the json format or calling a target.

The -getTargetResult option may not be necessary as it is a bit redundant with just getting property or item values after running targets. However, it would allow for command-line builds to do the same thing as design-time builds do in Visual Studio, where a bunch of targets are run and the return values of each target are returned.

dsplaisted avatar Mar 21 '23 19:03 dsplaisted

Thanks, I like it! 🙂

A few comments:

any other MSBuild output will be suppressed, unless there is an error

Just to clarify: will the error message go to stderr? I think it would be better for stdout to remain empty in that case.

The format for the values can be switched to json

Excellent idea. It becomes necessary when dealing with multi-line values.

The values can be saved to a file instead of printed to the console

Is there a real use case for this? When used in a script, the command output can be redirected to a file.

"ItemSpec": "Program.cs"
  • Shouldn't it be Identity instead of ItemSpec?
  • Should other well-known item metadata be included? Stuff like FullPath could be useful.
  • Since everything in MSBuild uses PascalCase, I'd also name the top-level JSON items that way: Properties, Items, Targets.

ltrzesniewski avatar Mar 21 '23 19:03 ltrzesniewski

How would these options interact with the InitialTargets and DefaultTargets attributes of the Project element? I'd expect:

  • -getProperty or -getItem, without -target, ignores both InitialTargets and DefaultTargets.
  • -getProperty or -getItem, with -target, runs both InitialTargets and the -target targets, but not DefaultTargets.
  • -getTargetResult, without -target, is an error and ignores both InitialTargets and DefaultTargets.
  • -getTargetResult, with -target, runs both InitialTargets and the -target targets, but not DefaultTargets. It outputs only the results of the -target targets, not the results of InitialTargets.

KalleOlaviNiemitalo avatar Mar 21 '23 20:03 KalleOlaviNiemitalo

Just to clarify: will the error message go to stderr? I think it would be better for stdout to remain empty in that case.

I'm not sure. I don't know if the error messages currently go to stdout or stderr, and if they don't go to stderr it might be tricky to change that.

The values can be saved to a file instead of printed to the console

Is there a real use case for this? When used in a script, the command output can be redirected to a file.

I think the normal MSBuild output can be useful to have in logs in case something goes wrong. This is more likely to matter if targets are being run instead of just evaluating the project.

  • Shouldn't it be Identity instead of ItemSpec?

We use both. ItemSpec is what we use in the MSBuild .NET APIs, while Identity is what we use as the metadata name. Probably Identity would be better here.

  • Should other well-known item metadata be included? Stuff like FullPath could be useful.

Yes, this is probably a good idea.

  • Since everything in MSBuild uses PascalCase, I'd also name the top-level JSON items that way: Properties, Items, Targets.

I'm not sure about this, JSON typically uses camelCase.

dsplaisted avatar Mar 22 '23 20:03 dsplaisted

How would these options interact with the InitialTargets and DefaultTargets attributes of the Project element? I'd expect:

  • -getProperty or -getItem, without -target, ignores both InitialTargets and DefaultTargets.
  • -getProperty or -getItem, with -target, runs both InitialTargets and the -target targets, but not DefaultTargets.
  • -getTargetResult, without -target, is an error and ignores both InitialTargets and DefaultTargets.
  • -getTargetResult, with -target, runs both InitialTargets and the -target targets, but not DefaultTargets. It outputs only the results of the -target targets, not the results of InitialTargets.

Yes, all of this matches what I was thinking.

dsplaisted avatar Mar 22 '23 20:03 dsplaisted

An explicit use case for this comes from our friends at the VSCode Docker tooling - they currently do evaluations looking for specific properties and items. They'd love to have a way to get that same information that doesn't require building and shipping an entire .NET application.

baronfel avatar Apr 11 '23 21:04 baronfel

Using stdout in MSBuild for structured data output is something we have not done in past, all structured build artifacts are in form of files. Although convenient, there is lot of corner cases like errors during MSBuild execution which might make it less useful. I recommend to support just json file output. That should be sufficient for almost everybody.

rokonec avatar Apr 13 '23 13:04 rokonec

I recommend to support just json file output. That should be sufficient for almost everybody.

That would be very inconvenient to use in scripts (you'd need to install and use something like jq, then get rid of the output json file).

Retrieving info in scripts in the main goal behind this feature request.

ltrzesniewski avatar Apr 13 '23 20:04 ltrzesniewski

That would be very inconvenient to use in scripts (you'd need to install and use something like jq, then get rid of the output json file).

Not an expert here, but I believed that it is quite simple for javascript to read json files. Anyway, I am just expecting whole kind of issues with it, and would rather not to implement it unless it is necessary. I would rather propose stable solution which is harder to use than easy but buggy solution.

Here are incomplete list of possible problems I fear (please take it with grain of salt):

  • custom tasks using Console.WriteLine as oppose to ILogger
  • error in execution will cause partial output while failure exit code will be not be handled
  • cancellation will cause partial output while failure exit code will be not be handled
  • stderr vs stdout confusion
  • console out encoding mismatch (ansi vs utf-8 for example)
  • console buffer width truncating lines

Especially the 1st point is very concerning as I am sure quite a few people do it. I even know few internal tasks which do so.

rokonec avatar Apr 14 '23 00:04 rokonec

Please dont discuss if the output should be json or stdout. Just let the user decide when invoking msbuild. Just provide parameters like --std-out or --json-out xxx.json or --xml-out. The user knows his scenario and knows what can go wrong. For me using a json-file would be bad. Our scenario is to evaluate properties. We know that nothing additionally would be written to stdout in this case. Creating and deleting a json file is a pain in the ass for certain scenarios when you can just use stdout. So just implement both and use parameters. :-)

Scordo avatar Apr 14 '23 07:04 Scordo

it is quite simple for javascript to read json files

It's easy in JavaScript, but not so much in shell scripts. Does anyone even call MSBuild from JS? 😅

  • custom tasks using Console.WriteLine as oppose to ILogger

This can be mitigated with:

Console.Out = TextWriter.Null;
Console.Error = TextWriter.Null;

Custom tasks would then need to use Console.OpenStandardOutput to write to stdout, and I wouldn't expect any task to do so.

Redirecting Console.Out/Error to an ILogger instead of TextWriter.Null by writing an adapter would be nice to have, but potentially unreliable due to missing line feeds for instance.

Also, as @Scordo stated above, this is not a problem if all you need is the evaluation phase.

  • failure exit code will be not be handled

That would be a bug in the script which calls MSBuild, not in MSBuild itself.

  • stderr vs stdout confusion
  • console out encoding mismatch (ansi vs utf-8 for example)

Those points are valid for any command-line executable.

  • console buffer width truncating lines

This shouldn't be an issue when redirecting MSBuild output.

ltrzesniewski avatar Apr 14 '23 08:04 ltrzesniewski

There is already an open PR and there has been lots of discussion both here in the issue and in the PR.

Every target available on an MSBuild project is a potential 'endpoint' or 'sub-command'. You don't need to use the build target for everything.

I have routinely implemented 'diagnostic' targets whose purpose is to report specific information (from properties and/or items). These targets can require other targets to execute, or not. A common set of 'diagnostic' targets can be applied to a set of projects with a Directory.Build.targets file. For each project in the set the shared target can then be called. Creating a shared PrintTargetPath target, as an example, is straightforward.

For a one-off ad-hoc circumstance, simple evaluations at the command line have value.

For some of the scripting use cases described, expanding the API surface (or the command surface if you prefer) by adding specialized targets is a better approach because it can be tailored to the specific requirements (including the returned data format) and can use the full capabilities of MSBuild.

There is a way in which the more complete and the more sophisticated the command line evaluation support is, the more redundant it will be with just executing a project.

jrdodds avatar Jul 05 '23 00:07 jrdodds

Hi there, is this available in RC1? If so, are there any more examples of how to use this? I would be very interested to get the parameters of the FscTask in CoreCompile for F# projects; image

Would this be possible to extract with these new flags?

nojaf avatar Sep 13 '23 08:09 nojaf

This will be in RC 2, and we'll have proper documentation on learn.microsoft.com at that time.

baronfel avatar Sep 13 '23 11:09 baronfel

I've been playing with it in the nightlys, it's very cool 😎

slang25 avatar Sep 13 '23 13:09 slang25

@slang25 got anything you want to share with the class? 🥹

baronfel avatar Sep 13 '23 13:09 baronfel

-help doesn't provide usage information for the new command line switches.

jrdodds avatar Sep 14 '23 20:09 jrdodds

The meta project created from a solution file has properties, items, and targets. What is the rationale for the MSB1063 error?

MSBUILD : error MSB1063: Cannot access properties or items when building solution files or solution filter files. This feature is only available when building individual projects.

I didn't find anything in the discussion here or in the PR.

jrdodds avatar Sep 17 '23 02:09 jrdodds