It's no good down here, I can't maneuver!
I'm going to stop writing new tasks for a couple of entries while I take some time to clean things up. My intention has always been to build up this workspace to make it easy for someone to use these postbuild steps. Ideally, this would include:
- A means to easily install the files necessary to get up and running
- A project file template
- Clear steps for how to run the project file
- Documentation describing the tasks
Notice: there is absolutely no mention of source code up above. Ideally, I would like to keep source out of this, unless someone was really curious about it. Right now, source is basically all I've got. Let's fix that by creating something someone can download and install all on their own.
Why do I want to do this now? Well, I was poking around the blogs here on MSDN, and came across an entry from the MSBuild team about how to redistribute your custom task. That entry points out 2 things I've been wanting to do for a while: build an installer for my task, and show how to use a .targets file. Let's start with the targets file.
What could possibly be the advantage of a targets file? Easy: transparency. I always have problems remembering to write my "UsingTask" entry in my postbuild project file. Having them all declared in a targets file eliminates this need. It will also make my template easier to write: I won't need to modify the template every time I add a new task. So, let's add a new XML file to our SetupProjects.Tasks library: SetupProjects.targets.
Next, I need this thing to be available for my use as a developer. Therefore, I need to copy it to the same location as my other dlls. Should be easy: I'll just update my PostBuild step in the C# class library.
Hold it right there, Buck-o! As long as we're making changes here, let's make a slightly better change to our plan: it seems to me its a bit unstructured to just dump all of our files into the MSBuild Extensions path directory. Let's be good citizens and copy them into a sub-folder of this directory: SetupProjects. Now, our post-build looks like this:
copy "$(TargetPath)" "$(MSBuildExtensionsPath)\SetupProjects\$(TargetFileName)"
copy "$(TargetDir)Interop.WindowsInstaller.dll" "$(MSBuildExtensionsPath)\SetupProjects\Interop.WindowsInstaller.dll"
copy "$(ProjectDir)SetupProjects.targets" "$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Targets"
Unfortunately, this fails unless you have already created the SetupProjects folder underneath the MSBuildExtensionsPath directory, so let's create it as part of our post-build:
mkdir "$(MSBuildExtensionsPath)\SetupProjects"
copy "$(TargetPath)" "$(MSBuildExtensionsPath)\SetupProjects\$(TargetFileName)"
copy "$(TargetDir)Interop.WindowsInstaller.dll" "$(MSBuildExtensionsPath)\SetupProjects\Interop.WindowsInstaller.dll"
copy "$(ProjectDir)SetupProjects.targets" "$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Targets"
Now we need some content for our targets file. A targets file should still have Project as the root node, and our familiar msbuild namespace:
<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003">
</Project>
Intelli-sense warns us that we have incomplete content in there. So let's fix that by using UsingTask. We currently have 6 tasks defined, so add those to the targets file:
<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="EnableLaunchApplication" AssemblyFile="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Tasks.dll"/>
<UsingTask TaskName="ExecuteSql" sAssemblyFile="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Tasks.dll"/>
<UsingTask TaskName="FindExternalCabs" AssemblyFile="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Tasks.dll"/>
<UsingTask TaskName="ModifyTableData" AssemblyFile="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Tasks.dll"/>
<UsingTask TaskName="PopulateDigitalSignature" AssemblyFile="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Tasks.dll"/>
<UsingTask TaskName="Select" AssemblyFile="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Tasks.dll"/>
</Project>
Now it should be safe to have our setup projects use this targets file. We use it by importing it, something like this:
<Import Project="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Targets" />
So we can replace all of our UsingTask statements with the one above (which I won't bother to show, but there are a total of 10 instance that got removed, leaving a total of 6 projects with the Imports statement (SetupSignCabs, SetupLaunchApplication, SetupModifyStringTableDataTest, SetupModifyIntTableDataTest, SetupPasswordInput, and SetupSetARPINSTALLLOCATION. Note I also modified the structure of these projects).
Building our projects shows that this new system seems to be working.
I won't go into to many details about the installer, but I will mention that I am using some of these very tasks in the build of the project. Here is a little snippet from my project file:
<ModifyTableData MsiFileName="$(InstallerDir)\Output\SetupProjects.Tasks.msi" TableName="Property" ColumnName="Value" Value="$(InstallerVersion)" Where="`Property`='ProductVersion'" />
<ModifyTableData MsiFileName="$(InstallerDir)\Output\SetupProjects.Tasks.msi" TableName="Property" ColumnName="Value" Value="{$(ProductCode)}" Where="`Property`='ProductCode'" />
<ModifyTableData MsiFileName="$(InstallerDir)\Output\SetupProjects.Tasks.msi" TableName="Upgrade" ColumnName="VersionMax" Value="$(InstallerVersion)" Where="`ActionProperty`='PREVIOUSVERSIONSINSTALLED'" />
<ModifyTableData MsiFileName="$(InstallerDir)\Output\SetupProjects.Tasks.msi" TableName="Upgrade" ColumnName="VersionMin" Value="$(InstallerVersion)" Where="`ActionProperty`='NEWERPRODUCTFOUND'" />
Part of my build is to generate a build number specific to the day of the build (there is info on the MSBuild blog about how to do this for VB, C#, and J# projects; I used something similar). This value is stored in the InstallerVersion
property in my MSBuild project. So I replace the ProductVersion
property in the Property table. Of course, this property is used in other places as well: in the Upgrade table for the DetectNewerInstalledVersion and RemovePreviousVersions of the setup project. Lastly, when you update the Version of the installer, it is also necessary to update the ProductCode as well. The new ProductCode is stored in the ProductCode
MSBuild property and is transferred into the MSI via a simple update of the ProductCode property in the Property table.
I have attached the installer and the sources as part of this blog post. Build number is the same as the numbering system we now use in Visual Studio. In Visual Studio 8, we used the ymmdd format. This means that the June 12, 2004 build was 40612. There is a slight problem with that system, though: when you get into 2007, it is not possible to simply roll over to 70101: the maximum allowed build number by Windows is roughly 65000. So, VS (in late 2006) changed its system from a leading "6" to a "leading "1" (presumably because a leading "0" would have been dropped when displayed as a number). With the move to 2007, the "1" was rolled over to a "2".
I apologize for the size of the installer. One source of the bloat is the custom action setup projects use to check if the .NET framework is installed. I think I'd be okay with not including this in the package, but of course there is no way to do this through the IDE. I even tried WiStream.vbs to take it out, but that didn't reduce the size of the file at all.