3

I just joined a team that has no CI process in place (not even an overnight build) and some sketchy development practices. There's desire to change that, so I've now been tasked with creating an overnight build. I've followed along with this series of articles to: create a master solution that contains all our projects (some web apps, a web service, some Windows services, and couple off tools that compile to command line executables); created an MSBuild script to automatically build, package, and deploy our products; and created a .cmd file to do it all in one click. Here's a task that I'm trying to accomplish now as part of all this:

The team currently has a practice of keeping the web.config and app.config files outside of source control, and to put into source control files called web.template.config and app.template.config. The intention is that the developer will copy the .template.config file to .config in order to get all of the standard configuration values, and then be able to edit the values in the .config file to whatever he needs for local development/testing. For obvious reasons, I would like to automate the process of renaming the .template.config file to .config. What would be the best way to do this?

Is it possible to do this in the build script itself, without having to stipulate within the script every individual file that needs to be renamed (which would require maintenance to the script any time a new project is added to the solution)? Or might I have to write some batch file that I simply run from the script?

Furthermore, is there a better development solution that I can suggest that will make this entire process unnecessary?

Marc Chu
  • 201
  • 3
  • 17
  • Why are config files not checked-in? Can you provide an example of some configuration settings that are developer-specific (and can't be shared)? – Arnold Zokas Oct 18 '12 at 16:36
  • As far as I know, there aren't settings being used that can't be shared. But a developer might want to change, say, a connection string so he can do all his testing on a local database. I can tell you that the current process sucks, because the project files are still expecting a web.config file, which increases the amount of manual work I've had to do just to get everything building. – Marc Chu Oct 18 '12 at 16:47
  • The team at large (only 3 or 4 developers, until I joined) doesn't seem to have a problem with the status quo, because it seems that developers pretty much have ownership over specific projects, and probably don't even build the others'. Hence, they've only worked on individual project files, and not in a master solution like the one I've created. – Marc Chu Oct 18 '12 at 16:56

2 Answers2

6

After a lot of reading about Item Groups, Targets, and the Copy task, I've figured out how to do what I need.

<ItemGroup>
    <FilesToCopy Include="..\**\app.template.config">
        <NewFilename>app.config</NewFilename>
    </FilesToCopy>
    <FilesToCopy Include="..\**\web.template.config">
        <NewFilename>web.config</NewFilename>
    </FilesToCopy>
    <FilesToCopy Include"..\Hibernate\hibernate.cfg.template.xml">
        <NewFilename>hibernate.cfg.xml</NewFilename>
    </FilesToCopy>
</ItemGroup>

<Target Name="CopyFiles"
        Inputs="@(FilesToCopy)"
        Outputs="@(FilesToCopy->'%(RootDir)%(Directory)%(NewFilename)')">
    <Message Text="Copying *.template.config files to *.config"/>
<Copy SourceFiles="@(FilesToCopy)"
      DestinationFiles="@(FilesToCopy->'%(RootDir)%(Directory)%(NewFilename)')"/>



I create an item group that contains the files that I want to copy. The ** operator tells it to recurse through the entire directory tree to find every file with the specified name. I then add a piece of metadata to each of those files called "NewFilename". This is what I will be renaming each file to.

This snippet adds every file in the directory structure named app.template.config and specifies that I will be naming the new file app.config:

<FilesToCopy Include="..\**\app.template.config">
    <NewFilename>app.config</NewFilename>
</FilesToCopy>

I then create a target to copy all of the files. This target was initially very simple, only calling the Copy task in order to always copy and overwrite the files. I pass the FilesToCopy item group as the source of the copy operation. I use transforms in order to specify the output filenames, as well as my NewFilename metadata and the well-known item metadata.

The following snippet will e.g. transform the file c:\Project\Subdir\app.template.config to c:\Project\Subdir\app.config and copy the former to the latter:

<Target Name="CopyFiles">
    <Copy SourceFiles="@(FilesToCopy)"
          DestinationFiles="@(FilesToCopy->'%(RootDir)%(Directory)%(NewFileName)')"/>
</Target>

But then I noticed that a developer might not appreciate having his customized web.config file being over-written every time the script is run. However, the developer probably should get his local file over-written if the repository's web.template.config has been modified, and now has new values in it that the code needs. I tried doing this a number of different ways--setting the Copy attribute "SkipUnchangedFiles" to true, using the "Exist()" function--to no avail.

The solution to this was building incrementally. This ensures that files will only be over-written if the app.template.config is newer. I pass the names of the files as the target input, and I specify the new file names as the target output:

<Target Name="CopyFiles"
        Input="@(FilesToCopy)"
        Output="@(FilesToCopy->'%(RootDir)%(Directory)%(NewFileName)')">
      ...
</Target>

This has the target check to see if the current output is up-to-date with respect to the input. If it isn't, i.e. the particular .template.config file has more recent changes than its corresponding .config file, then it will copy the web.template.config over the existing web.config. Otherwise, it will leave the developer's web.config file alone and unmodified. If none of the specified files needs to be copied, then the target is skipped altogether. Immediately after a clean repository clone, every file will be copied.

The above turned out be a satisfying solution, as I've only started using MSBuild and I'm surprised by its powerful capabilities. The only thing I don't like about it is that I had to repeat the exact same transform in two places. I hate duplicating any kind of code, but I couldn't figure out how to avoid this. If anyone has a tip, it'd be greatly appreciated. Also, while I think the development practice that necessitates this totally sucks, this does help in mitigating that suck factor.

Marc Chu
  • 201
  • 3
  • 17
  • the Target with name "CopyFiles" will be called automatically? Or I have to do something to include that target? – Vajda Endre Nov 22 '16 at 10:09
  • Assuming you are running a different target, e.g. "Build", and you want this target to run as a part of it, you need to make CopyFiles a dependency of your target, like so: – Marc Chu Nov 28 '16 at 14:38
1

Short answer:
Yes, you can (and should) automate this. You should be able to use MSBuild Move task to rename files.

Long answer:
It is great that there is a desire to change from a manual process to an automatic one. There are usually very few real reasons not to automate. Your build script will act as living documentation of how build and deployment actually works. In my humble opinion, a good build script is worth a lot more than static documentation (although I am not saying you should not have documentation - they are not mutually exclusive after all). Let's address your questions individually.

What would be the best way to do this?

I don't have a full understanding of what configuration you are storing in those files, but I suspect a lot of that configuration can be shared across the development team.

I would suggest raising the following questions:

  • Which of the settings are developer-specific?
  • Is there any way to standardise local developer machines so that settings could be shared?

Is it possible to do this in the build script itself, without having to stipulate within the script every individual file that needs to be renamed?

Yes, have a look at MSBuild Move task. You should be able to use it to rename files.

...which would require maintenance to the script any time a new project is added to the solution?

This is inevitable - your build scripts must evolve together with your solution. Accept this as a fact and include in your estimates time to make changes to your build scripts.

Furthermore, is there a better development solution that I can suggest that will make this entire process unnecessary?

I am not aware of all the requirements, so it is hard to recommend something very specific. I can say suggest this:

  • Create a shared build script for your solution
  • Automate manual tasks as much as possible (within reason)
  • If you are struggling to automate something - it could be an indicator of an area that needs to be rethought/redesigned
  • Make sure your team mates understand how the build works and are able to make changes to it themselves - don't "own" the build and become a bottleneck

Bear in mind that going from no build script to full automation is not an overnight process. Be patient and first focus on automating areas that are causing the most pain.

If I have misinterpreted any of your questions, please let me know and I will update the answer.

Arnold Zokas
  • 8,306
  • 6
  • 50
  • 76
  • Thanks, Arnold. To be clear, configuration settings _are_ being stored in source control, they're just stored with different filenames, necessitating the process above. I suppose this allows the developer to pull from the repository without having to worry that his particular configuration will get overwritten, forcing him to re-edit with his local values. It seems that there should be a better way to do this, e.g. perhaps a local file could be imported to overwrite settings in web.config, just as MSBuild can pass a /p:TargetEnvPropsFile=xxx.proj to configure for a particular environment. – Marc Chu Oct 18 '12 at 17:52
  • Also, I was hoping that, just as I can pass the solution file to MSBuild (which would not require editing of the script to build another project, but simply adding it to the solution), I would be able to write code in the script to find any file with a .template.config extension and rename the extension to .config. If I can't do that, and the script needs to be edited anytime a new project is added just so we can list another file to be renamed, then that _almost_ defeats the purpose of passing a solution at all, and might argue for listing every project within the script individually. – Marc Chu Oct 18 '12 at 17:59
  • @MarcChu Yes, you can add logic like this to a build script. I have done something similar before (for other reasons). – Arnold Zokas Oct 18 '12 at 20:38