Thursday, January 17, 2013

Visual Studio and Custom Build Rules

Today I was interested in generating some code. I made an external tool that took an input file (.gen) and output a C++ header (.h) file.

Then, I wanted to make this as painless as possible to set up in VS, so that whenever you added a .gen file to the project, it would automatically know how to build it, track dependencies, etc.

My first attempt was of course the familiar "Custom Build Tool". If you add an unknown file type to a VS project and go to its Properties page, you can build the file with a "Custom Build Tool", then fill out a Command Line to run, etc.

Now, this would totally work. Except it's really tedious to maintain. It must be propagated to every configuration your project builds with. It's a giant painful error-prone way to maintain this sort of thing.

There's a better way. As I understand it, as of VS2010, VS is built on top of MSBuild. There's a set of files (a .props, a .targets, and an .xml file) to create your own "Item Type" that can be selected to build files with.

I'm going to lay out a simple example that worked for me. I don't end up using a .props file in my example as I didn't need it, but presumably you can use that to add your own properties that can be set per file for the build step.

Disclaimer: I don't know if this example is completely minimal - I was just happy to actually get it to work. The documentation for how to set this stuff up is really hard to find and dig through.

First, the .xml file (in my example, codegen.xml).




This is nifty and part of the magic. I don't exactly understand the difference between an ItemType and a ContentType, but here's the pragmatic explanation: given this, when you add a .gen file to your project, it will  recognize that file as a "CodegenItem" and associate the build step with it. Which is coming next.

Here's the .targets file (codegen.targets) (because of the use of $(MSBuildThisFileName) in this file, it's important that the file name matches the .xml file, so codegen.xml + codegen.targets)




So the first order of business is to 'include' the .xml file we have above, and associate the ItemType that comes from there ("CodegenItem") to the Target we have below ("GenerateCode").

The target is the actual build step. We give it a name "GenerateCode". Next, the BeforeTargets="ClCompile" attribute says that this build step should be run before the ClCompile step (which is the name of the target of the built in C++ compiler). Because I'm generating code, I want this. Next the Inputs and Outputs ("Identity" is the name of the file the build rule is applied to, and Outputs need to be specified so that VS can understand file dependencies).

I thought the Message entry would actually get displayed when the build step as invoked, however it doesn't. I don't see it output in the Build window, but I left it there because I think it should work. It's possible it's being printed to some log file somewhere. Here is the documentation for it: http://msdn.microsoft.com/en-us/library/6yy0yx8d.aspx

Finally, the Exec task (docs here: http://msdn.microsoft.com/en-us/library/x8zx72cd.aspx) - this specifies the commandline to execute. Straightforward - I have a codegen.bat file that takes the input and output file and does its thing.

Now, I would suggest putting these files alongside your .vcxproj. To actually get this to work, open VS, right-click your project and select Build Customizations... In this window, choose Find Existing... and browse to and select your .targets file. Then check the box to enable it. Now, when you add your files to a project, it will have your custom build step associated.

So, there you have it. Now here's the questions I'm left with:

- How do I refer to the Inputs and Outputs of the Target in the command line? I'd like to say %(Inputs) and %(Outputs). Needing to repeat that is redundant and tedious.
- Why doesn't the Message print in the Build window?
- Where's all the friendly introduction docs to this feature?

Thanks to John Calsbeek for a point in the right direction!

5 comments:

  1. Thanks Mike, your post has really opened my eyes and helped me bridge the gap between the simplest working case, and all the official targets, props and xml files created by Microsoft.

    I'd like to answer your remaining questions, but believe me, it would take too long to explain everything that's going on in the background without writing a comment that's longer than your original post.

    Entire books have been written on the subject of the MSBuild tool... so I'm just going to redirect you to one. It should have all the answers you need.

    http://www.amazon.com/Inside-Microsoft-Build-Engine-Foundation/dp/B00B9ZEKH4/ref=sr_1_1?ie=UTF8&qid=1367684340&sr=8-1&keywords=inside+the+microsoft+build+engine

    ReplyDelete
  2. Hi Mike,

    In case you have not resolved the issue about 'why doesn't the message appears in the output (build) window', I was looking for an answer as my custom build rule in VS2008 worked properly but the same 'rule' (using custom build steps) didn't output anything in the output window.
    The 'problem' is that if you have 'MSBuild project build output verbosity' (Tools->Options->Projects and Solutions->Build and Run) set to minimal the only text that that will appear in your output window is the one that has the format 'text : [warning | error] : text ( http://msdn.microsoft.com/en-us/library/yxkt8b26(v=vs.110).aspx ). I thought that this was just for using the mentioned benefits. But if you don't include any of the fields and the second is not 'warning' or 'error' (case sensitive) it doesn't appear anything. Formating your text in your program output it will appear properly in the output window.
    Example: In the program I built to be executed to compile the shaders with a build rule in VS2008 I had:
    printf("%02d > %s\n", iNLine+1, szLine);
    And was working properly in VS2008. But the same program executed from a Custom Build Step in VS2012 produces no output.
    Changing the program to output:
    printf("%s(%d) : warning : %02d>%s\n", g_szFullPathName, g_uNRealLine[iNLine-1], iNLine+1, szLine);
    The text appears properly in VS2012. For example, forcing a syntax error in a shader I get:
    1>D:\work2013\U1v20Lib\source\glslshaders\U1GLSL150_HDRFinalDoF2.glsl(72): warning : 16>float fDepth=texturekkk(txtrZBuffer, var_v2TexCoord0.xy).x;
    And it has the ability that as I have the filename and the correct line, double clicking on the line, opens the file and puts the cursor in the correct line.

    Hope this helps.

    --- Carlos Abril

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Weird, I tried to post this but my pasted in text didn't show.

    If you want your text (as specified in the .targets file) to show up in the Output window in Visual Studio, use the attribute Importance="High" like so:

    (NOTE: I'm removing angle brackets since BlogSpot isn't taking my XML in the comment. In the actual XML .targets file, just use the angle brackets of course.

    Message
    Text="Generating code: %(QtMOCHeader.Identity)"
    Importance="High"
    /

    Then your text will show up in Output:

    1> Generating code: ..\..\..\Tools\Launcher\Widgets\VuRuntimeLogWidget.h

    ReplyDelete
  5. This was helpful thanks. Spent last night trying to scour the MS docs on targets, etc. but they're too abstract and generic for easy comprehension.

    ReplyDelete