24

Is there any way to ask msbuild what build targets provided msbuild file support? If there is no way to do it in command prompt? May be it could be done programmatically?

Are there no way to do it besides parsing msbuild XML?

Artem Tikhomirov
  • 21,497
  • 10
  • 48
  • 68
  • I recently used XML parsing for examining/modifying MSBuild project files. Sorry, no answer for you... – mmmmmmmm Jan 27 '09 at 15:48
  • See https://stackoverflow.com/questions/2618201/is-there-a-way-to-list-all-the-build-targets-available-in-a-build-file/60811784#60811784 for the built-in way to do it. – riQQ Jan 11 '22 at 20:07

6 Answers6

19

Certainly MS provides the api to do this without parsing the xml yourself. Look up microsoft.build.buildengine

Adapted from some C# code found on msdn ... it's usually worth exploring. Need to reference the microsoft.build.engine dll for this to compile. Replace framework version and path below with your values. This worked on a sample project file, although the list may be longer than you expect.

using System;
using Microsoft.Build.BuildEngine;
class MyTargets
{        
  static void Main(string[] args)
  {
    Engine.GlobalEngine.BinPath = @"C:\Windows\Microsoft.NET\Framework\v2.0.NNNNN";
    Project project = new Project();
    project.Load(@"c:\path\to\my\project.proj");
    foreach (Target target in project.Targets)
    {
      Console.WriteLine("{0}", target.Name);
    }
  }
}
Zac Thompson
  • 12,401
  • 45
  • 57
  • 22
    My next question would be, *Why in the hell do we have to write a program to do this???* – jww Jul 25 '15 at 12:23
17

I loved the idea of using PowerShell, but the pure XML solution doesn't work because it only outputs targets defined in that project file, and not imports. Of course, the C# code everyone keeps referring to is dead simple, and with .Net 4.5 it's two lines (the first of which you should consider just adding to your profile):

Add-Type -As Microsoft.Build
New-Object Microsoft.Build.Evaluation.Project $Project | Select -Expand Targets 

Yeah. Really. That's the whole thing.

Since the output is very verbose, you might want to restrict what you're looking at:

New-Object Microsoft.Build.Evaluation.Project $Project | 
    Select -Expand Targets |
    Format-Table Name, DependsOnTargets -Wrap

There is a catch, however.

When you load builds like that, they stick in the GlobalProjectCollection as long as you leave that PowerShell window open and you can't re-open them until you unload them. To unload them:

[Microsoft.Build.Evaluation.ProjectCollection]::GlobalProjectCollection.UnloadAllProjects()

Considering that, it might be worth wrapping that in a function that can accept partial and relative paths or even piped project files as input:

Add-Type -As Microsoft.Build
Update-TypeData -DefaultDisplayPropertySet Name, DependsOnTargets -TypeName Microsoft.Build.Execution.ProjectTargetInstance

function Get-Target {
    param(
        # Path to project file (supports pipeline input and wildcards)
        [Parameter(ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=1)]
        [Alias("PSPath")]
        [String]$Project,

        # Filter targets by name. Supports wildcards
        [Parameter(Position=2)]
        [String]$Name = "*"

    )
    begin {
        # People do funny things with parameters
        # Lets make sure they didn't pass a Project file as the name ;)
        if(-not $Project -and $Name -ne "*") {
            $Project = Resolve-Path $Name
            if($Project) { $Name = "*" }
        }
        if(-not $Project) {
            $Project = Get-Item *.*proj
        }
    }
    process {
        Write-Host "Project: $_ Target: $Name"
        Resolve-Path $Project | % {
            # Unroll the ReadOnlyDictionary to get the values so we can filter ...
            (New-Object Microsoft.Build.Evaluation.Project "$_").Targets.Values.GetEnumerator()
        } | Where { $_.Name -like $Name }
    }
    end {
        [microsoft.build.evaluation.projectcollection]::globalprojectcollection.UnloadAllProjects()
    }
}

And now you don't even need to manually Format-Table...

Addendum:

Obviously you can add anything you want to the output with that Update-TypeData, for instance, if you wanted to see Conditions, or maybe BeforeTargets or AfterTargets...

You could even pull nested information. For instance, you could replace the Update-TypeData call above with these two:

Update-TypeData -MemberName CallTargets -MemberType ScriptProperty -Value {
    $this.Children | ? Name -eq "CallTarget" | %{ $_.Parameters["Targets"] } 
} -TypeName Microsoft.Build.Execution.ProjectTargetInstance

Update-TypeData -DefaultDisplayPropertySet Name, DependsOnTargets, CallTargets -TypeName Microsoft.Build.Execution.ProjectTargetInstance

You see the first one adds a calculated CallTargets property that enumerates the direct children and looks for CallTarget tasks to print their Targets, and then we just include that ind the DefaultDisplayPropertySet.

NOTE: It would take a lot of logic on top of this to see every target that's going to be executed when you build any particular target (for that, we'd have to recursively process the DependsOnTargets and we'd also need to look for any targets that have this target in their BeforeTargets or AfterTargets (also recursively), and that's before we get to tasks that can actually just call targets, like CallTargets and MSBuild ... and all of these things can depend on conditions so complex that it's impossible to tell what's going to happen without actually executing it ;)

Jaykul
  • 15,370
  • 8
  • 61
  • 70
  • Your solution does indeed, provide a more complete reporting... but: You show `DependsOnTargets` but you should also include `CallTargets` to get a full accounting. (Some time ago I adapted @knut's answer and added `CallTargets` to it.) Is it possible to report `CallTargets` with your solution? – Michael Sorens Aug 07 '14 at 01:09
  • For myself, I don't think I care about CallTargets -- I'm not sure what you're trying to accomplish, but I just want to list the targets available to call on the command-line. Having said that, let me append something to the answer for you, before I run out of space in this box. – Jaykul Aug 08 '14 at 18:18
  • 1
    Does that help? I'm pretty sure that as a general rule, you should not care about CallTargets though -- it is part of the nitty-gritty of how a target does whatever it's supposed to do. Why do you care about this particular task? I mean, you're not going to look at all the tasks from the command-line, and (just for instance), MSBuild is required *instead of* CallTargets if you just want to call "the default targets" in your build. – Jaykul Aug 08 '14 at 18:28
  • To my mind, DependsOnTargets tell me that when I call target A, before it processes anything it will call target B and target C. Whereas CallTargets tell me that when I call target A, sometime during its processing it will call target B and target C. They seem to be equivalently-useful pieces of information. Or is my view of this skewed in some fashion...? – Michael Sorens Aug 08 '14 at 23:05
  • The thing is that there are lots of ways to do the same thing differently ;) A can `DependsOnTargets` B or A can `After` B or B can `Before` A ... or B can `CallTarget` A or it can `MSBuild Project="$(MSBuildProjectFile)" Targets="A"` ... or it could use `Exec` or there could be third-party tasks ... – Jaykul Aug 10 '14 at 19:51
  • More to the point, the "DependsOnTargets" relationship is special (and preferable) because it allows the build engine the most flexibility in ordering. Unlike the other methods it doesn't explicitly dictate order or make one task "part of" another task, simply expresses dependencies. Therefore: if you `msbuild /t:A,B` and B `DependsOnTarget` A, they will each run only once. But if B `CallTarget` A, then A will run twice. But my point is that CallTarget is an implementation detail: it's just one of many hundreds of pieces of information you could have about what B does ;) – Jaykul Aug 10 '14 at 20:03
14

Updated for .NET Framework 4, since the above has been deprecated. Import microsoft.build.dll and code is as follows:

using System;
using Microsoft.Build.Evaluation;
class MyTargets
{
  static void Main(string[] args)
  {
    Project project = new Project(args[0]);
    foreach (string target in project.Targets.Keys)
    {
      Console.WriteLine("{0}", target);
    }
  }
}
woods
  • 156
  • 1
  • 2
  • 25
    My next question would be, *Why in the hell do we have to write a program to do this???* – jww Jul 25 '15 at 12:22
  • For Visual Studio 2013 add this reference Microsoft.Build.dll v12 (C:\Program Files (x86)\MSBuild\12.0\Bin\Microsoft.Build.dll), not the v4 one from GAC. – Marcos Feb 23 '16 at 20:19
  • 2
    @jww dynamic extensibility. curse and gift at the same time. it causes more tears of pain than joy though. – Dbl May 18 '16 at 16:14
2

I suggest you use PowerShell:

Select-Xml `
    -XPath //b:Target `
    -Path path-to-build-file `
    -Namespace @{ b = 'http://schemas.microsoft.com/developer/msbuild/2003' } |
    Select-Object -ExpandProperty Node |
    Format-Table -Property Name, DependsOnTargets -AutoSize

The XPath query will find all Target elements and display the target name and dependencies in table format. Here is a sample that selects the 10 first targets from Microsoft.Web.Publishing.targets:

PS C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v11.0\Web> Select-Xml `
    -XPath //b:Target `
    -Path Microsoft.Web.Publishing.targets `
    -Namespace @{ b = 'http://schemas.microsoft.com/developer/msbuild/2003' } |
    Select-Object -ExpandProperty Node |
    Sort-Object -Property Name |
    Select-Object -First 10 |
    Format-Table -Property Name, DependsOnTargets -AutoSize

Name                                    DependsOnTargets                       
----                                    ----------------                       
_CheckPublishToolsUpToDate                                                     
_CheckRemoteFx45                                                               
_CleanWPPIfNeedTo                                                              
_DetectDbDacFxProvider                                                         
_WPPCopyWebApplication                  $(_WPPCopyWebApplicationDependsOn)     
AddContentPathToSourceManifest          $(AddContentPathToSourceManifestDepe...
AddDeclareParametersItems               $(AddDeclareParametersItemsDependsOn)  
AddDeclareParametersItemsForContentPath $(AddDeclareParametersItemsForConten...
AddDeclareParametersItemsForIis6        $(AddDeclareParametersItemsForIis6De...
AddDeclareParametersItemsForIis7        $(AddDeclareParametersItemsForIis7De...
knut
  • 4,699
  • 4
  • 33
  • 43
  • Very nice! How difficult is it to add a column for the list of values in the immediate child node `CallTarget`? Some dependencies lie there as well. – Michael Sorens Apr 17 '13 at 21:32
  • 1
    The problem with that is that it's file based, and won't import the includes, so for an actual project build file, you'll get only a tiny subset of the targets. – Jaykul Aug 06 '14 at 22:04
0

Here is the code snippet to get all targets in the order their execution.

    static void Main(string[] args)
    {
        Project project = new Project(@"build.core.xml");
        var orderedTargets = GetAllTargetsInOrderOfExecution(project.Targets, project.Targets["FinalTargetInTheDependencyChain"]).ToList();
        File.WriteAllText(@"orderedTargets.txt", orderedTargets.Select(x => x.Name).Aggregate((a, b) => a + "\r\n" + b));
    }

    /// <summary>
    /// Gets all targets in the order of their execution by traversing through the dependent targets recursively
    /// </summary>
    /// <param name="allTargetsInfo"></param>
    /// <param name="target"></param>
    /// <returns></returns>
    public static List<ProjectTargetInstance> GetAllTargetsInOrderOfExecution(IDictionary<string, ProjectTargetInstance> allTargetsInfo, ProjectTargetInstance target)
    {
        var orderedTargets = new List<ProjectTargetInstance>();

        var dependentTargets =
            target
            .DependsOnTargets
            .Split(';')
            .Where(allTargetsInfo.ContainsKey)
            .Select(x => allTargetsInfo[x])
            .ToList();

        foreach (var dependentTarget in dependentTargets)
        {
            orderedTargets = orderedTargets.Union(GetAllTargetsInOrderOfExecution(allTargetsInfo, dependentTarget)).ToList();
        }

        orderedTargets.Add(target);

        return orderedTargets;
    }
0

If you build from command-line you can generate a metaproject file by setting an environment variable before building. This file will detail everything that a solution-file will target as well as outlining their target order.

It's not really a read-friendly diagnostic file but does have all the information you likely need in XML format.

E.g. from CMD and a typical VS2017 installation; replace line 1 with whatever build-tool-loader you use

call "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\VsDevCmd.bat"
set MSBuildEmitSolution=1
call MSBuild SolutionFile.sln /t:rebuild   

For a real example, I've made an SLN file starting with a C# project called mixed_proj, so I've got mixed_proj.sln, mixed_proj.csproj, then the two C++ projects I added, ConsoleApplication1 and DLL1.

I've set the build dependency order to where ConsoleApplication1 only builds after DLL1

Here is the command-line for building it:

call "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\VsDevCmd.bat"
set MSBuildEmitSolution=1
call MSBuild miced_proj.sln /t:rebuild 

Here is the generated metaproject file content (too big to paste here)

kayleeFrye_onDeck
  • 6,648
  • 5
  • 69
  • 80