Thursday, February 13, 2014

Using Build Events in Visual Studio to Make Life Easier

The Build Events options in Visual Studio can make your life much easier. I use them primarily to copy files around after building projects, but we can use them to run any commands that we want. Let's take a look at the options, and then I'll show how I use them.

Update May 2020: With .NET Core, I've stopped using the '$(SolutionDir)' macro (and other macros relating to the solution). Read why here: Post-Build Events and .NET Core. But don't stop reading this article; it has lots of useful information.

Build Events
Build Events are part of the Project options, and they are often overlooked. If we open up the "Properties" for a project, we get the following screen:


This lets us run command-line commands either before the project is built (pre-build events) or after (post-build events). Although, we do get a drop-down to select if the post-build event runs or not. The default value "On successful build" is usually where I keep this value. This way, the post-build event will *not* run if there is a build failure. Other options include "Always" and "When the build updates the project output."

You may be curious about this last option. Visual Studio is lazy about compiling things, so it will only build if the source files have changed. If we select "When the build updates the project output", then the post-build event will only run if the compiler updates the output files on this project.

We can basically put anything in here that we could run from the command line. And Visual Studio provides macros to make it easier. So, even though we can type straight into these text boxes, it's much better to click the "Edit Pre-build..." or "Edit Post-build..." buttons to bring up the dialog.

The Build Event Dialog
The build event dialog is where things start to get interesting. Well, they aren't actually very interesting at first (this dialog is already populated with a command which we'll talk about in just a bit):


But, when we click the "Macros", we can see how Visual Studio can really help us out:


This shows us a number of macros that we can use in our commands. We can add the macros by simply double-clicking on the item. This will add a corresponding placeholder that has the format $(xxx). These macros will be replaced with actual values when the event runs.

Here's a complete list of the available macros (this is from Visual Studio 2013, but this dialog has not changed for quite a while):


Let's take a look at the "TargetDir" macro. In the value column, we see this expanded out to the full path of the output folder.

But here's why using macros is compelling: If I move this project to a different folder, this TargetDir macro will expand to the new location. If I change the configuration from "Release" to "Debug", the macro will expand to the corresponding output folder (bin\Debug\ vs. bin\Release\).

There are a number of other interesting items. And we can get various levels of detail, depending on our needs. For example, look at the various "Project" macros in the list above.

ProjectName is just the name of the project itself
ProjectFileName includes the project name, plus the file extension
ProjectPath includes the fully-qualified path to the project, and includes the project file name/extension
ProjectDir is the fully-qualified path to the project, but does *not* include the project file name

And we can see similar naming schemes for "Target" and "Solution". Lots of cool stuff.

How I Use Post-Build Events
I primarily use post-build events to copy files around. For example, a number of my sample applications use late-binding. This means that certain dlls are not present at compile time; instead, they are referenced dynamically at runtime.

The problem with this is that I would normally need to copy the late-bound dlls into the project output folder (the same folder as the executable) in order for the application to work. Instead of manually copying the files, I use a post-build event to automatically copy them over.

So, when my examples use dynamically-loaded repositories, I can simply copy over the already-compiled assemblies from a solution folder. Here's the "LateBindingRepositories" folder from one of my solutions:


In addition to the assemblies, this also has some additional files. The "People.txt" file is the data file used by the CSV repository (PersonRepository.CSV.dll). The SQL repository (PersonRepository.SQL.dll) uses Entity Framework and also pulls its data from a local database file (People.sdf).

I need to get these files into the output folder for my main WPF application. So, I have a post-build step in that project:


So, let's parse this command.

We're using "xcopy" to copy files from one folder to another. This is how we had to copy files back before Windows (now I feel old). This has 2 main parameters: the source and the destination.

For the source, we are asking for the solution folder with the $(SolutionDir) macro. If we look at the macro list above, we can see how this expands out, and notice that it includes the trailing path delimiter. Then we go to the "LateBindingRepositories" folder and ask for all files (the *.*). This will copy the assemblies (dlls), plus the other files in that folder.

For the destination, we use the output directory with $(TargetDir). This will be the same folder as our executable (in bin\Debug or bin\Release depending on the Solution Configuration).

The "/Y" switch tells xcopy to overwrite any existing files. This is here because xcopy will normally give you an overwrite prompt if it finds an existing file. We can't get that prompt from a build event.

Important Note About Paths and File Names
One important thing to notice about the source and destination paths: they are wrapped in quotes. If there are any spaces in the path or file name, then we need quotes around it so that xcopy knows that this is a single argument (a space delimits arguments on the command line).

I've run into this issue because I don't normally have spaces in my project paths. But then someone copied the project to their machine (which did have spaces in the path) and the post-build step stopped working.

Other Uses
Another way that I've used post-build events is for dynamically loaded modules. Prism (from the Microsoft Patterns & Practices team) makes it really easy to load up modules from a particular folder. This way, we don't need them to all be present at compile time.

So, in each of my modules, I have a post-build step to copy the output to $(SolutionDir)Modules. And then in my shell application (the one that loads the modules), I have a post-build that copies the modules from $(SolutionDir)Modules into the appropriate folder that the application is looking for.

And we can get more creative as well. I'm using post-build events to copy files around. But we can also use them to run applications such as testing tools, linting tools, validation tools, IL re-writers, and obfuscators. This can help us automate these processes on our developer machines (although we would want to put these into a formal process on our build server).

Wrap Up
The Build Events can be a big time saver, especially if you find yourself copying files from one output folder to another. And the macros that are provided by Visual Studio make it really easy to use paths and file names that easily adapt when we move our projects or build with different configurations.

Happy Coding!

13 comments:

  1. We use the build events in a similar fashion but since using VS source control some build events fail because the folders are now made read-only. Any ideas on how to get around this nuisance ?

    ReplyDelete
    Replies
    1. Things do get interesting in that situation. I generally don't check in anything that's generated (.dll or .exe) or copied from another location (the original file may be under source control, but not the copy). Centralized source control doesn't generally lock folders, but you run into problems if you have a locked file and then try to copy over that file. Excluding copied items from source control can usually alleviate this issue. And these are the types of things that make distributed source control more attractive since files are not locked in that scenario.

      Delete
  2. What if you are using the pre-build event to copy a class to another folder. For example, I have to use a proxy class for a SOAP service. There is a dev/qa one and a production one. I thought I might have a folder to hold the dev one and a folder for the prod one and a third folder called proxy that would hold the one I actually needed. To keep it simple I kept all of the class names the same.

    I can get the proper class copied over, but I need to change the namespace. Is that possible in the pre-build event, or am I going down the wrong path?

    ReplyDelete
    Replies
    1. I think you may be on the wrong path here. The pre-build/post-build events are good for things that we *always* want to do, such as copying the same file as we do here or running obfuscation tools or other things from the command line. Changing a namespace in files using the command line sounds like the wrong solution.

      Instead, you probably want to look at some sort of configuration management tool. Unfortunately, I haven't had to deal with this particular problem, so I cannot make a specific recommendation. Good luck!

      Delete
  3. installed Web Analyzer for VS 2015 from https://visualstudiogallery.msdn.microsoft.com/6edc26d4-47d8-4987-82ee-7c820d79be1d. i would like to know if i can use this post build to run ESLint on entire solution

    ReplyDelete
    Replies
    1. I haven't used Web Analyzer, so I'm not familiar with the details. From the description, it looks like it automatically runs against open files, and you can also run it against an entire project by using the context menu in the Solution Explorer.

      Build Events let us make command-line calls. If there is a way to run the analyzer from the command line, then you can add it as a post-build event. If all you want is the ESLint functionality, then you may want to look at using ESLint directly. It does have a command-line interface that you could use in a post-build event: http://eslint.org/docs/user-guide/command-line-interface

      Delete
  4. Well done, Jeremy. It works !
    Thanks !

    ReplyDelete
  5. One of the best uses of this is to add "Bubbles.scr /start" in the Pre-build on your colleague's machine and watch them get confused why the screensaver starts every time they run their project.

    ReplyDelete
  6. Hi Jermy,

    Am getting the following error even though I used /Y /I commands.

    CopyFilesToOutputDirectory: Copying file from "obj\x86\Release\Entities.dll" to "bin\x86\Release\Entities.dll". Entities -> E:\src\TU2018R1.DAL2\externals\UC800\Periwinkle\Entities\bin\x86\Release\Entities.dll
    Copying file from "obj\x86\Release\Entities.pdb" to "bin\x86\Release\Entities.pdb". PostBuildEvent: xcopy "E:\src\TU2018R1.DAL2\externals\UC800\Periwinkle\Entities\bin\x86\Release\Entities.dll" "E:\src\TU2018R1.DAL2\Build\" /I /Y
    PostBuildEvent: File creation error - Cannot create a file when that file already exists. Unable to create directory - E:\src\TU2018R1.DAL2\Build

    Can you please help me...

    ReplyDelete
    Replies
    1. The best way to troubleshoot is to run the command from the command line manually. This will let you better track the errors and exactly where they occur. Then you can check that you have the right switches and files in place. I normally wouldn't copy files from the "obj" folders because those are temporary. If I need DLLs in the output folder, I'll keep them in a separate folder (often called "Additional Assemblies"). Also, if "Entities.dll" is a compile-time referenced assembly, you shouldn't need to copy it to the "Release" folder yourself; Visual Studio will do it automatically.

      Delete
  7. Is there any way to use MSBUILD macros in VS 2017 build events? In supporting multiple .NET frameworks, the $(FrameworkVersion) macro is MSBUILD is enticing but does not get populated during the VS 2017 build process. I am using version 15.4.4.

    ReplyDelete
    Replies
    1. I don't believe so. As far as I know, the only macros that you can use in the build events are the ones listed in the Macros dialog. Those are still the same in VS 2017.

      With that said, if you need a post-build event, you can probably make a command-line application that can grab the framework version from the dll/exe assembly information. The result of that could then be piped or used as an argument for another command. The pre/post-build events let you do pretty much anything you can do from a command line (assuming the process has the right permissions).

      Delete
    2. Thank you Jeremy for the reply. I have settled on the following to do what I need. Not the most elegant, but it works. It allows for multi-targeting in building .NET/.NET Core libraries.
      @ECHO OFF
      SET outDir=$(OutDir)
      ECHO %outDir%
      IF NOT "%outDir%"=="%outDir:netstandard2.0=%" (
      @ECHO Copying netstandard2.0 files...
      copy $(OutDir)$(TargetFileName) "..\..\lib\netstandard2.0"
      copy $(OutDir)$(TargetName).pdb "..\..\lib\netstandard2.0"
      )

      IF NOT "%outDir%"=="%outDir:net461=%" (
      @ECHO Copying net461 files...
      copy $(OutDir)$(TargetFileName) "..\..\lib\net461"
      copy $(OutDir)$(TargetName).pdb "..\..\lib\net461"
      )

      Delete