Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Evaluate a project from the command line #3911

Closed
ltrzesniewski opened this issue Nov 7, 2018 · 37 comments · Fixed by #8792
Closed

Evaluate a project from the command line #3911

ltrzesniewski opened this issue Nov 7, 2018 · 37 comments · Fixed by #8792
Labels
Area: Debuggability Issues impacting the diagnosability of builds, including logging and clearer error messages. help wanted Issues that the core team doesn't plan to work on, but would accept a PR for. Comment to claim. Priority:2 Work that is important, but not critical for the release triaged
Milestone

Comments

@ltrzesniewski
Copy link

ltrzesniewski commented Nov 7, 2018

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
@KirillOsenkov
Copy link
Member

@ltrzesniewski
Copy link
Author

@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.

@rainersigwald
Copy link
Member

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 rainersigwald added the Area: Debuggability Issues impacting the diagnosability of builds, including logging and clearer error messages. label Nov 19, 2018
@ltrzesniewski
Copy link
Author

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.

@rainersigwald
Copy link
Member

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

@ltrzesniewski
Copy link
Author

ltrzesniewski commented Nov 19, 2018

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.

@rainersigwald rainersigwald added this to the Backlog milestone Feb 18, 2020
@rainersigwald rainersigwald added needs-design Requires discussion with the dev team before attempting a fix. help wanted Issues that the core team doesn't plan to work on, but would accept a PR for. Comment to claim. labels Feb 18, 2020
@Scordo
Copy link

Scordo commented Sep 9, 2020

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 :-(

@baronfel
Copy link
Member

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)

@bwateratmsft
Copy link

@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

@Scordo
Copy link

Scordo commented Jan 26, 2022

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

@KirillOsenkov
Copy link
Member

@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.

@lonix1
Copy link

lonix1 commented Jul 28, 2022

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.

@dsplaisted
Copy link
Member

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.

@ltrzesniewski
Copy link
Author

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.

@KalleOlaviNiemitalo
Copy link

KalleOlaviNiemitalo commented Mar 21, 2023

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.

@dsplaisted
Copy link
Member

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
Copy link
Member

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.

@baronfel
Copy link
Member

baronfel commented Apr 11, 2023

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.

@rokonec
Copy link
Contributor

rokonec commented Apr 13, 2023

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.

@dsplaisted dsplaisted added the needs-triage Have yet to determine what bucket this goes in. label Apr 14, 2023
@dsplaisted dsplaisted modified the milestones: VS 17.6, VS 17.7 Apr 14, 2023
@AR-May AR-May added Priority:2 Work that is important, but not critical for the release and removed needs-triage Have yet to determine what bucket this goes in. labels Apr 25, 2023
@rainersigwald rainersigwald modified the milestones: VS 17.7, VS 17.8 Jun 16, 2023
@jrdodds
Copy link
Contributor

jrdodds commented Jul 5, 2023

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.

@nojaf
Copy link

nojaf commented Sep 13, 2023

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?

@baronfel
Copy link
Member

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

@slang25
Copy link

slang25 commented Sep 13, 2023

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

@baronfel
Copy link
Member

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

@jrdodds
Copy link
Contributor

jrdodds commented Sep 14, 2023

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

@jrdodds
Copy link
Contributor

jrdodds commented Sep 17, 2023

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
Copy link
Contributor

jrdodds commented Sep 17, 2023

-getTargetResult will run a target that otherwise would not run.

E.g. Given a project with targets one and two and the targets have no dependency between them:

<Project>
  <Target Name="One">
    <PropertyGroup>
      <First Condition="$(First) == ''">One</First>
    </PropertyGroup>
    <Message Text="One" />
    <Message Text="First = $(First)" />
  </Target>

  <Target Name="Two">
    <PropertyGroup>
      <First Condition="$(First) == ''">Two</First>
    </PropertyGroup>
    <Message Text="Two" />
    <Message Text="First = $(First)" />
  </Target>
</Project>

and given a command line with -target:one -getTargetResult:two, both targets will be executed.

The current behavior executes the targets provided to -getTargetResult that didn't already execute, after the 'standard' target build order. Essentially there is a secondary chain of targets.

It seems from the discussion that this is not the intended behavior.

My own quick take is that -getTargetResult should not itself cause a target to be executed and if a target provided to -getTargetResult didn't execute then there is no result to report.

@rainersigwald
Copy link
Member

What is the rationale for the MSB1063 error?

Implementation annoyances, not anything principled. Because the solution metaproject isn't actually the project that was specified, it's nontrivial to reach "through" the .sln to the metaproj to get the results.

I'd be happy to see this restriction removed, but not enough to block the feature on it.

@rainersigwald
Copy link
Member

-getTargetResult will run a target that otherwise would not run.

Excellent catch, thanks. Let's discuss on #9225.

@jrdodds
Copy link
Contributor

jrdodds commented Sep 18, 2023

What is the rationale for the MSB1063 error?

Implementation annoyances, not anything principled. Because the solution metaproject isn't actually the project that was specified, it's nontrivial to reach "through" the .sln to the metaproj to get the results.

I'd be happy to see this restriction removed, but not enough to block the feature on it.

Makes sense. Thanks

@rolfbjarne
Copy link
Member

The lack of -resultsFile:... makes this rather unreliable for scripting :/ dotnet/sdk#36694

@0xced
Copy link

0xced commented Mar 1, 2024

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

Friendly reminder that as of today there's still no trace of the --getProperty feature on https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-build. I knew I had read about it somewhere some time ago but it was difficult to find information about it.

@rainersigwald
Copy link
Member

@0xced it's in the linked MSBuild command-line docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Debuggability Issues impacting the diagnosability of builds, including logging and clearer error messages. help wanted Issues that the core team doesn't plan to work on, but would accept a PR for. Comment to claim. Priority:2 Work that is important, but not critical for the release triaged
Projects
None yet
Development

Successfully merging a pull request may close this issue.