I came across a situation a while back, where I needed to setup a build process for a Visual Studio 2008 solution that contained multiple projects:
- A bunch of Console Applications,
- A mobile app (targeted at an enterprise-class rugged Windows Mobile 6.5 device to be used on field),
- A desktop Windows Forms application &
- An ASP.NET web application.
We have 3 server environments: Dev, QA & Production on which each of these applications should be capable of running. (Except for the mobile app, which will run on its own SQLCE database file sitting on each hand-held unit). What I needed to come up with, was a way to manage the environment-specific configuration info in a manner that let me do a single build, and “apply” the right set of configuration data according to where I was doing the deployment.
The direct way to do this would be to use the VS Solution/Project Configuration functions. You would create one configuration for each environment. Then you would set up a pre-build event like <code> copy “$(ProjectDir)$(ConfigurationName).<<configfilename>>” “$(ProjectDir)$(OutDir)<<configfilename>>” /Y </code> to copy the right config file over to your build output directory. You can check out this excellent (albeit really old, considering this is VS 2008!) article from Bil Simser –
http://weblogs.asp.net/bsimser/handling-multiple-environment-configurations-with-net-2-0
or this one from Scott Hanselman –
http://www.hanselman.com/blog/ManagingMultipleConfigurationFileEnvironmentsWithPreBuildEvents.aspx
The disadvantage here is that you need to do a build with the appropriate configuration selected, in order to get the right set of files selected. I would prefer to have a single build output file, like a TAR/ZIP archive, which includes all the config files for all environments. Then when I deploy/unpack it to the right environment, I want to deploy the right config files based on an argument that I’ll pass into my deployment script. Personally, that makes it easier to control for me, rather than pick the right build configuration on the VS IDE every time. It also enables me to do everything on the command-line too if I want, which is good to have.
Being a .NET newbie, I didn’t have the time to figure out Nant (which is free & open-source btw), so I used Visual Studio’s pre-build & post-build events to make things work the way I needed them to. Here is how it went.
Project-Level Setup
- For the Console & Desktop Application Projects: Each started out with a single App.Config file in the project directory, which the build turns into an XML file name named <<ProjectName.exe.config>>. I now have multiple copies of this App.Config file, prefixed with the environment name, such as prod.app.config, qa.app.config, etc: this is where my env-specific stuff sits. Ditto with other config files that are env-specific, such as my hibernate.cfg.xml file & log4net.config files.
- For the Web Application Project: Same as # 1, except the file in question is Web.config.
- Each of these projects is then setup with pre-build and post-build commands (Project Properties > Build Events):
Pre-Build: del $(TargetDir)" /S /Q
Post-Build: "$(ProjectDir)Resources\Build.bat" "$(ProjectDir)" "$(TargetDir)" - Here TargetDir refers to the directory setup as Output Path under Project Properties > Build > Output. We do a pre-build cleanup of this directory so that any existing config files are cleaned out completely. ProjectDir refers to the project root directory. The Build.bat is a custom post-build batch script in every project with the following sample content:
SET ProjectDir=%1
SET TargetDir=%2
SET ProjectDir=%ProjectDir:"=%
SET TargetDir=%TargetDir:"=%
rmdir "%TargetDir%prod" /s /q
rmdir "%TargetDir%test" /s /q
rmdir "%TargetDir%dev" /s /q
rmdir "%TargetDir%local" /s /q
mkdir "%TargetDir%prod"
copy "%ProjectDir%Resources\prod.log4net.config" "%TargetDir%prod/log4net.config"
copy "%ProjectDir%Resources\prod.hibernate.cfg.xml" "%TargetDir%prod/hibernate.cfg.xml"
copy "%ProjectDir%Resources\prod.App.config" "%TargetDir%prod/project1.exe.config"
mkdir "%TargetDir%test"
copy "%ProjectDir%Resources\test.log4net.config" "%TargetDir%test/log4net.config"
copy "%ProjectDir%Resources\test.hibernate.cfg.xml" "%TargetDir%test/hibernate.cfg.xml"
copy "%ProjectDir%Resources\test.App.config" "%TargetDir%test/project1.exe.config"
mkdir "%TargetDir%dev"
copy "%ProjectDir%Resources\dev.log4net.config" "%TargetDir%dev/log4net.config"
copy "%ProjectDir%Resources\dev.hibernate.cfg.xml" "%TargetDir%dev/hibernate.cfg.xml"
copy "%ProjectDir%Resources\dev.App.config" "%TargetDir%dev/project1.exe.config"
mkdir "%TargetDir%local"
copy "%ProjectDir%Resources\local.log4net.config" "%TargetDir%local/log4net.config"
copy "%ProjectDir%Resources\local.hibernate.cfg.xml" "%TargetDir%local/hibernate.cfg.xml
copy "%ProjectDir%Resources\local.App.config" "%TargetDir%local/project1.exe.config"What this does is, it cleans out the env-specific directories under TargetDir, re-creates them, then renames & copies the appropriate config files into each directory. You can see that all the config files are already named as required, they’re just placed inside different directories. This makes deployment a whole lot simpler because we are just dealing with different sets of files with the exact same name.
- Here is sample build output using this build setup for one project (wherein TargetDir = \bin)
------- Build started: Project: project1, Configuration: Debug Any CPU ------
del "E:\Dev\trunk\Soln1\project1\bin\" /S /Q
----Compile Output----
Compile complete -- 0 errors, 1 warnings
projectName -> E:\Dev\trunk\Soln1\projectName\bin\project1.exe
"E:\Dev\trunk\Soln1\project1\Resources\Build.bat" "E:\Dev\trunk\Soln1\project1\" "E:\Dev\trunk\Soln1\project1\bin\"
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
1 file(s) copied.
========== Build: 2 succeeded or up-to-date, 0 failed, 0 skipped ==========
You can see both the pre-build & post-build commands’ output.
This is applied to all the individual projects, and that’s it, job done on the individual project artifacts. This has been a very long post already! Now we’ll move into the nitty-gritties of building a single deployable file for the entire solution and make it even longer 🙂
Solution-Level Setup
The solution property pages let you setup inter-project dependencies (which you can also setup using the individual project properties), which define the build order. It also lets you choose which build config you want to use for each of the projects for a particular solution-level build config. We are not using the project/solution-level build config for reasons stated earlier in this post, so that pretty much means there is no straight-forward way to setup the solution build to spit out a single deployable file at the very end. We will use a utility project to do this, a project that has no build artifacts of its own, but merely puts together the artifacts from all the other projects as needed. Here is how this can be done.
- First we create a new project, a Console Application should do.
- Right-click on Project > Project Dependencies and add all the other projects that we wish to build and put together. This will ensure that each time we build this project, all the other projects in the solution are built too.
- Then we go to Project Properties > Build events and enter the following command:
"$(ProjectDir)Resources\Build.bat" "$(SolutionDir)" "$(TargetDir)" "$(ProjectDir)" - This will trigger the script that will put all the build artifacts together into one single deployable file.
- Here is what I have in the Build.bat in my utility project:
SET SolutionDir=%1
SET TargetDir=%2
SET ProjectDir=%3
SET SolutionDir=%SolutionDir:"=%
SET TargetDir=%TargetDir:"=%
SET ProjectDir=%ProjectDir:"=%
REM This project doesn't have any targets of its own, delete the dummy build output created
REM echo "Creating target directories"
rd "%TargetDir%" /S /Q
md "%TargetDir%"
SUBST Z: "%ProjectDir:~0,-1%"
REM Create sub-folders for env-based and non-env-based files
md "%TargetDir%App"
md "%TargetDir%Env"
REM ******************************************************************************
REM ****************************** Mobile App ************************************
REM ******************************************************************************
echo "Preparing mobile app"
md "%TargetDir%App\MobileApp"
xcopy "%SolutionDir%MobileAppCab\bin" "%TargetDir%App\MobileApp" /EXCLUDE:Z:\Resources\exclude.txt
REM ******************************************************************************
REM *************************** Desktop App ***************************************
REM ******************************************************************************
echo "Preparing desktop app"
REM Non-environment-specific files
md "%TargetDir%App\DesktopSync"
xcopy "%SolutionDir%DesktopSync\bin" "%TargetDir%App\DesktopSync" /EXCLUDE:Z:\Resources\exclude.txt
REM Environment-specific files
cd "%SolutionDir%DesktopSync\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\DesktopSync"
REM ******************************************************************************
REM ****************************** Web Console ***********************************
REM ******************************************************************************
echo "Preparing web console"
REM Non-environment-specific files
md "%TargetDir%App\WebConsole"
xcopy "%SolutionDir%PrecompiledWebConsole" "%TargetDir%App\WebConsole" /S /EXCLUDE:Z:\Resources\exclude.txt
rd "%SolutionDir%PrecompiledWebConsole" /s /q
REM Environment-specific files
cd "%SolutionDir%ManagementConsole\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\WebConsole"
REM ******************************************************************************
REM ************************ Scheduled Processes *********************************
REM ******************************************************************************
md "%TargetDir%App\ScheduledProcesses"
REM Non-environment-specific files
md "%TargetDir%App\ScheduledProcesses\Project1"
xcopy "%SolutionDir%Project1\bin" "%TargetDir%App\ScheduledProcesses\Project1" /EXCLUDE:Z:\Resources\exclude.txt
md "%TargetDir%App\ScheduledProcesses\Project2"
xcopy "%SolutionDir%Project2\bin" "%TargetDir%App\ScheduledProcesses\Project2" /EXCLUDE:Z:\Resources\exclude.txt
md "%TargetDir%App\ScheduledProcesses\Project3"
xcopy "%SolutionDir%Project3\bin" "%TargetDir%App\ScheduledProcesses\Project3" /EXCLUDE:Z:\Resources\exclude.txt
md "%TargetDir%App\ScheduledProcesses\Project4"
xcopy "%SolutionDir%Project4\bin" "%TargetDir%App\ScheduledProcesses\Project4" /EXCLUDE:Z:\Resources\exclude.txt
md "%TargetDir%App\ScheduledProcesses\Project5"
xcopy "%SolutionDir%Project5\bin" "%TargetDir%App\ScheduledProcesses\Project5" /EXCLUDE:Z:\Resources\exclude.txt
md "%TargetDir%App\ScheduledProcesses\Project6"
xcopy "%SolutionDir%Project6\bin" "%TargetDir%App\ScheduledProcesses\Project6" /EXCLUDE:Z:\Resources\exclude.txt
md "%TargetDir%App\ScheduledProcesses\Project7"
xcopy "%SolutionDir%Project7\bin" "%TargetDir%App\ScheduledProcesses\Project7" /EXCLUDE:Z:\Resources\exclude.txt
md "%TargetDir%App\ScheduledProcesses\Project8"
xcopy "%SolutionDir%Project8\bin" "%TargetDir%App\ScheduledProcesses\Project8" /EXCLUDE:Z:\Resources\exclude.txt
md "%TargetDir%App\ScheduledProcesses\Project9
xcopy "%ProjectDir%Resources\Transfer*" "%TargetDir%App\ScheduledProcesses\Project9"
xcopy "%ProjectDir%Resources\Sftp*" "%TargetDir%App\ScheduledProcesses\Project9"
REM Environment-specific files
cd "%SolutionDir%Project1\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\ScheduledProcesses\Project1"
cd "%SolutionDir%Project2\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\ScheduledProcesses\Project2"
cd "%SolutionDir%Project3\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\ScheduledProcesses\Project3"
cd "%SolutionDir%Project4\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\ScheduledProcesses\Project4"
cd "%SolutionDir%Project5\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\ScheduledProcesses\Project5"
cd "%SolutionDir%Project6\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\ScheduledProcesses\Project6"
cd "%SolutionDir%Project7\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\ScheduledProcesses\Project7"
cd "%SolutionDir%Project8\bin"
for /D %%d in (*) DO xcopy /I %%d "%TargetDir%Env\%%d\ScheduledProcesses\Project8"
REM ******************************************************************************
REM ***************************** Create TARs ************************************
REM ******************************************************************************
7z a "%TargetDir%SolnDeployable.tar" "%TargetDir%App" "%TargetDir%Env"
xcopy "%ProjectDir%Resources\Deploy.bat" "%TargetDir%"
rd "%TargetDir%App" /s /q
rd "%TargetDir%Env" /s /q
SUBST /d Z: - As you can see, the script is split into different parts for the different kind of projects involved. Lets take a closer look. After first cleaning up the target directory, we start with putting everything together:
- We create 2 folders in the target dir: App & Env. As you can tell by their names, the App folder will hold non-env specific stuff and all the env-based config files will go into Env.
- For every “type” of project, we create a sub-folder within both App & Env folders: MobileApp, ScheduledProcesses, WebConsole, DesktopSync.
- We first copy everything from the individual project target directories into the utility project’s target dir. We use the XCOPY command to do a deep copy.
- The app files (files directly in the individual project target dirs) go into /App/<projecttype> directly.
- The env files (files under sub-folders in project target dirs) go into /Env/<envname>/<projecttype> folder. <envname> is simply the sub-folder name.
Note that the mobile app has no env-specific files, so all the files (actually, there are just 2 – the .cab file & .inf file) go into App/MobileApp.
- Finally we create the deployable file by zipping up both App & Env target sub-directories into a single .tar file. I use the 7-Zip open-source command-line utility for this. Once this file is created, we delete the App & Env sub-directories because they are no longer needed.
- Additionally there is a deploy.bat script in the utility project, that we will need during deploy time. We copy that out of the project directory and into the target directory.
So this leaves us with just 2 files in the target directory of the utility project – A tar file & a deploy script. And all you need to do now onward is right-click on your Solution in VS and click Build Solution! (You can use the command-line too, but I’ll cover that in a later post.)
How to deploy
The deploy script is extremely simple:
SET Env=%1
REM If Env is not one of "prod", "test", "dev" and "local", the unpacked files will not
REM include any env-specific files!
REM ******************************************************************************
REM ****************************** Unpack TAR ************************************
REM ******************************************************************************
7z x FwmDeployable.tar
xcopy "App" /S /I /Y
rd "App" /S /Q
xcopy "Env/%Env%" /S /I /Y
rd "Env" /S /Q
We just pass in the env name as an argument to the deploy.bat, and the script uses the 7Z command-line utility to unpack the tar file and copy the right set of env config files. And we’re done!
Note: I run this deploy script directly on the server where I wish to do the deployment The unpacked files sit in the same directory as my tar file, and I then just move them to the drive where they need to be. If you wish to push the deploy files from your workstation/build server to the right target server & additionally put it into the final folder location based on the env argument, that’s possible too. The script will need to be extended & customized accordingly.
I have found my build/deploy process to be extremely simple and easy to maintain with this setup. As always, there are other ways to deal with all of this, but currently this is what works, and works well for me. I’ll write up another post on how I do all of this via command-line.
Reblogged this on Dinesh Ram Kali..