Monday, May 19, 2008

TeamBuild ClickOnce – Versioning

It seems many people have had issues getting the version number correctly within their tfsbuild.proj files to use in their ClickOnce html files.  The solutions I've seen were very complicated, but this may be because the examples I've seen were in TFS 2005 or they were using different tasks.

My coworker added the SDC tasks to our build server because there is functionality for XML and file manipulation.  From his starting point I went on to look further into two specific tasks: Xml.GetValue and File.RegEx.

My boss directed me to where I can find the version number that I needed, which resides in the application manifest file.  The trickiest part of the Xml.GetValue task to me was understanding XPath.  The biggest point that I missed was that I needed to define the namespace within the application manifest to get to the element. 

If you look inside the application manifest of your program (not the program.exe.manifest, but program.application as I'm dealing with publishing), you'll find something similar to this:

<?xml version="1.0" encoding="utf-8"?>
    <asmv1:assembly 
        xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" 
        manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" 
        xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" 
        xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" 
        xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" 
        xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xrml="urn:mpeg:mpeg21:2003:01-REL-R-NS" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  
    <assemblyIdentity name="program.application" version="1.0" 
        publicKeyToken="abcdedf892929" language="neutral" processorArchitecture="x86" 
        xmlns="urn:schemas-microsoft-com:asm.v1" />
...
</asmv1>

I thought I could just use an XPath like this /asmv1:assembly/assemblyIdentity/@version to get the value that I wanted.  Nope, my build threw errors saying that the namespace asmv1 was not defined.  Then I defined the asmv1 namespace, but I wasn't getting a value returned.   You'll notice in the snippet above that assemblyIdentity adds its own namespace, but it's same as asmv1 defined above.  In the end it was because I didn't prefix the asmv1 namespace before assemblyIdentity as well.

I manually published my project to get the publish.htm file.  I then made it a part of my project so that I could copy it to my published application's directory and modify the version number.  I edited the html file and put a #VERSION# tag so that the File.RegEx task could find it and replace it with the $(ResultsItem) variable.  We get $(ResultsItem) from the ItemName attribute of the Output element within the Xml.GetValue task element.  The following is the snippet of my tfsbuild.proj file that does the versioning of the publish.htm file:

<Project>
...
    <Target Name="AfterCompile">
        <Copy SourceFiles="$(SolutionRoot)\SupportFiles\publish.htm" DestinationFolder="$(OutDir)" />
    </Target>

    <Target Name="AfterDropBuild">
        <Message Text="Extracting version from Application manifest..."/>

        <Xml.GetValue
            NameSpaces="asmv1=urn:schemas-microsoft-com:asm.v1"
            Path="$(DropLocation)\$(BuildNumber)\x86\Release\program.application"
            XPath="/asmv1:assembly/asmv1:assemblyIdentity/@version">
      
            <Output TaskParameter="Results" ItemName="ResultsItem"/>
        </Xml.GetValue>

        <Message Text="Updating publish.htm to version @(ResultsItem)"/>
    
        <File.RegEx
            Path="$(DropLocation)\$(BuildNumber)\x86\Release\publish.htm"
            RegularExpression="#VERSION#"
            NewValue="@(ResultsItem)" />
    </Target>
...
</Project>
 

Friday, May 16, 2008

TeamBuild ClickOnce – Automation

I've had a ton of issues trying to get ClickOnce to work with MS Team Build starting from TFS 2005; we're now using TFS 2008.  Getting a bootstrap package to work with TeamBuild took some work, but I was able to do it with success early on. 

In our 2005 environment, I had to place the signing certificate that I was using onto our build server and place it in the certificate store in order for any of my builds to work.  Without the stored certificate my builds would not complete.  Fine, my bootstrap packages work, but my ClickOnce builds didn't.  I didn't feel too bad because a ton of people out there have had issues with ClickOnce and Team Build. 

Fast forward to our 2008 environment.  I had some minor issues with my bootstrap package as I posted about when I started my blog, but nothing compared to the ClickOnce issues I've had.  I decided to create a new build for my project in VS 2008 and start fresh.  I still got the same result: my build would hang and return no warnings. 

My coworker got ClickOnce automation working on his project and I happy someone finally got it to work.  I really couldn't find any difference in our processes as far as the build automation went.  My project has a lot more dependencies, but that didn't seem to be the cause of my problems. 

Every time I published my application manually, the certificate asked for a password.  This was the same password that I used to store the certificate on our build server.  My coworker's publishing didn't require him to enter one.  So then I thought to myself, maybe a password dialog is just sitting there waiting for input. 

The reason I came to this conclusion was that I did some testing and looked at the output directory of where my files were being published.  Everything was being created and copied except my ".application" file.  That triggered the thought that my certificate wasn't signing the application manifest because of a certificate password window just sitting there waiting for input.  The build process can't handle window pop ups.

My solution was to just create a new signing certificate with no password and this time I didn't place it in our build server's certificate store.  After adding the certificate to my project I ran my ClickOnce build and it worked...FINALLY! 

Thursday, May 15, 2008

MS Team Build Bug

Today while scouring the MSDN forums I found something interesting.  If you choose "Any CPU" as your platform in your "TFSBuild.proj" file, solution files will build correctly:

...
<ItemGroup>

    <SolutionToBuild Include="$(BuildProjectFolderPath)/../../project.sln">
        <Targets>Publish</Targets>
        <Properties></Properties>
    </SolutionToBuild>

</ItemGroup>

<ItemGroup>
 
 <ConfigurationToBuild Include="Release|Any CPU">
     <FlavorToBuild>Release</FlavorToBuild>
     <PlatformToBuild>Any CPU</PlatformToBuild>
 </ConfigurationToBuild>

</ItemGroup>
...

The problem is that I tried to publish a solution and for some reason it didn't work.  Actually, the reason was that I had references in my solution to non-publishable projects.  So it made sense then to just publish the project within the solution.  When I replaced the solution file with my VB Project file in the "SolutionToBuild" element, I got errors saying that the location of my executable couldn't be found.  I followed the suggestions on MSDN and changed my platform to x86. After making the change I was able to build both solution and project files:

...
<ItemGroup>

    <SolutionToBuild Include="$(BuildProjectFolderPath)/../../project.vbproj">
        <Targets>Publish</Targets>
        <Properties></Properties>
    </SolutionToBuild>

</ItemGroup>

<ItemGroup>
 
 <ConfigurationToBuild Include="Release|x86">
     <FlavorToBuild>Release</FlavorToBuild>
     <PlatformToBuild>x86</PlatformToBuild>
 </ConfigurationToBuild>

</ItemGroup>
...

Tuesday, May 13, 2008

Visual Studio Bootstrapping (Part 3)

My project has various prerequisites for it to run and the install location is set to download from the vendor's web site.  Unfortunately, the option to specify the location of the prerequisites affects all of them and you can't choose per prerequisite:

prereqsvs

In BMG you can set the download URL of your prerequisite file by setting the "HomeSite URL" property:

homesite

I intentionally left mine blank as I wasn't sure if the download location would change frequently or not.  When I built my project, I got a build warning:

warning

Putting a "HomeSite URL" will exclude your prerequisite file from your setup package.  Instead your bootstrapper will depend on the URL to download the prerequisite.  If you put an invalid location, you'll get an error:

installerror

As I mentioned before, I wasn't sure how often the links get updated and this was why I chose to include my prerequisite with my setup package. 

If you're using a build server like our team is, make sure to copy your prerequisite package to your build server.  The default location for Visual Studio 2008's packages is "C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages"  Be sure to update your "tfsbuild.proj" file to take into account any copying of your prerequisite as well.

Overall, BMG is a great tool with some quirks and bugs.  Even with the issues I ran into, I'll still be using this utility to create any prerequisite packages that I might need.

Visual Studio Bootstrapping (Part 2)

Setting up system checks for prerequisites in BMG is pretty straight forward.  The only option I chose was the MSI Product Check:

systemchecks

You can either get your product code from the msi file that you're including or from your computer's installed applications.  They should return the same product code either way.  The "Property for Result" is just a variable name that you'll use to check against on the "Install Conditions" tab.

After looking at the help file (which was another painful and buggy experience), these were the return codes I was supposed to check against:

helpfile

However, even with my prerequisite installed, my "setup.exe" kept wanting to install my prerequisite.  I had to intentionally make my setup package fail so that I could snoop into the installation log.  What I found was that the return code that I needed to check against wasn't in the help file, the value I was supposed to check against was "5":

Installconditions

After adding the above condition, my prerequisite checks worked correctly, but my installation failed due to an exit code issue.  On the "Exit Codes" tab, I had to insert a success for exit code "0" in order for my setup package to determine that my prerequisite installed correctly:

exitcodes

Visual Studio Bootstrapping (Part 1)

We added a new feature to my current project that required a third party application as a prerequisite. Hooking this into Visual Studio seemed pretty straight forward; just use a bootstrapping program to create the necessary bootstrap package for Visual Studio. You can also create your own manually.

I used Bootstrapper Manifest Generator to generate my bootstrap package.  Although this application was quite buggy for me, in the end it got the job done.  However, it wasn't as easy as it should have been to get the whole process going.

The current version of BMG for Visual Studio 2008 is still in beta, so that may account for the numerous bugs I encountered when running this application.  I also had some issues figuring out how to get things to work the way I wanted.  For instance, if you want a progress bar for your prerequisite's installation, you'll need to put in an installation time:

BMGProperties

If you don't put a time (which is just a guess anyway), you'll end up with a screen like this:

prereqinstall

The prerequisite will install, but you'll have no progress bar.  Not a show stopper, but I found this to be unappealing.  Also, if you overshoot your installation time, the progress bar will never reach the end before exiting this screen.  If you undershoot it, the progress bar will go to the end and start over.  I chose the latter.