Creating .NET applications that just run on any version of Windows can be tricky.
For BitFlock, the requirement was to create a Windows application that will be very easy to run. There’s no need for installing it into the user’s system and the need for dependencies has to be minimized, if not eliminated completely. The application was written in .NET, but the challenge is, how do you deploy a .NET application over the web, without an installer and in a single EXE?
One solution would be to use a virtualization technology that creates a single EXE file that effectively emulates the environment that the application needs to run in. There are a couple of solutions out there that do this but, understandably, the EXE files that they generate are too large (60+ MB). This is not an options for deploying a simple application meant to be opened directly from the web browser. At least one solution I’ve seen use a form of streaming technology to make the download process bearable, but that solution is too expensive and frankly overkill for what the BitFlock application was supposed to do.
Alternatives? Delphi? Assembly? If you’re already invested in the .NET Framework then these are not an option. Delphi is another technology to learn, and you may not like it. Assembly is a hard. So what’s the best answer if you want to do this in .NET?
.NET Ecosystem on Windows
The first question to ask is, if I compile my application targeting a specific version of the Framework, what versions of Windows will it run on out of the box?
To survey the landscape, here’s a neat little chart to show you the bigger picture.
What are all those dots?
If you look at the black solid ones, those are the important ones. Every black solid dot means that the .NET Framework comes pre-installed on that version of Windows. Knowing that, we can now answer the original question. If you target your compiler for .NET 2.0, your EXE will run on the .NET 2.0, 3.0 and 3.5 Full Frameworks. And, we can see that at least one of those come pre-installed on Windows Vista, Server 2008, Windows 7 and Server 2008 R2.
Note that .NET 2.0 assemblies files are actually installed when the user installs .NET 3.5 and .NET 3.0, so your .NET 2.0 application is not actually running over .NET 3.0 or 3.5 in the strictest sense. But the result is the same.
.NET Dependencies
We can quickly see that it’s not possible to deploy a single compiled .NET EXE that will run on everything Windows XP and later without including some sort on .NET Framework installation method. Also, more troubling, is the fact that our .NET 2.0 compiled assemblies will NOT run on a system with only .NET 4.0 installed. This is a distinct possibility since Microsoft’s still popular Windows XP SP3 platform came with no .NET pre-installed, and .NET 4 is being offered as an optional update on that platform.
So what’s the answer?
For BitFlock, our solution was twofold.
– 1 –
Have 2 separate binaries generated from the same code, one targeting .NET 2.0 and one targeting .NET 4.0. We will try to detect the .NET Framework installed from the browser’s user agent, this works for Internet Explorer and Firefox with the .NET Framework Assistant extension installed. If the user agent doesn’t contain a string identifying the version of the .NET Framework then we will check the Windows version reported in the user agent string against any known versions of Windows with the .NET Framework pre-installed. If the user is using one of those versions of Windows we send them the appropriate .NET EXE.
– 2 -
If we can’t identify the user’s .NET Framework and the user’s version of Windows is not one that comes with a pre-installed version of the .NET Framework then we rely on something called a universal binary.
The Universal Binary
The universal binary consists of a few pieces.
- A small native code bootstrapper, with a size in the tens of kilobytes, that runs on all copies of Windows starting with Windows 2000 SP4 and up.
- A .NET 4.0 Client Web Installer.
- A .NET 2.0 version of the BitFlock application as a single merged EXE.
- A .NET 4.0 version of the BitFlock application as a single merged EXE.
- Assets, such as settings and graphics.
The binary itself is a single EXE packaging all of the above as resources. The total size of the universal binary ended up being around 4 megabytes. An acceptable compromise.
How does it work?
The bootstrapper launches and reads the embedded XML settings file which defines the minimum requirements for this application. A requirement can be a particular Windows version range, existence of a particular file or registry key. Requirements can be grouped and logically combined with AND / OR.
If the system meets these requirements then the bootstrapper checks the individual requirements of each embedded application. In our case, there are 2 embedded applications, a .NET 2.0 version and a .NET 4.0 version. The first application the meets it’s requirements fully is executed and the bootstrapper’s job is complete.
If none of the embedded applications meet their requirements on the first pass, the bootstrapper then check if any application can potentially meet it’s requirements. How does it do that? Each specified requirement can optionally contain information on how that requirement can be met. In our case, it contains information saying that the .NET 4.0 requirement can be met by executing an embedded installer.
If the bootstrapper finds such an application, it displays a minimalistic UI to the user informing them that the application requires that you install certain dependencies before it can run. The user has the choice to either proceed with the installation or exit.
User Experience
At the end of the day, our primary concern is the user experience. This is the user experience running BitFlock as it’s implemented now:
- The user visits the web site, they are identified as a Windows user from the browser’s user agent string, and they click the Run link.
- The web site tries to identify the installed .NET version. If it can, then an appropriate EXE is sent.
- The web site then tries to identify the user’s version of Windows. If it can, then it checks whether it’s one of the known versions of Windows that come with .NET pre-installed. If it is, then it dispatches the appropriate EXE.
- If the two tests above fail, then the web site sends the user the Universal Binary.
- The universal binary checks the version of Windows that it’s running on and the service pack level to determine whether it is supported by BitFlock. If not, then it displays a message indicating that it can’t run and lists the supported Windows versions.
- It then checks to see if the user has .NET 2.0, 3.0, 3.5 or 4.0 installed. If they do, then the appropriate embedded EXE is launched, and the bootstrapper is done.
- At this point, the bootstrapper will check to make sure that the system meets the minimum requirements for the .NET 4.0 installer. If not, a message is displayed to the user indicating that they must install .NET 2.0 or 3.0 or 3.5, manually. This is rare.
- If .NET 4.0 can be installed, then a minimalistic message is shown to the user informing them that .NET 4.0 must be installed before the application can be run. The user is given the chance to exit and not install the .NET Framework.
- If the user chooses to proceed, .NET 4.0 is installed over the Web and the application is started.
The goal of this process is of course to make sure that as few people as possible make it all the way down to #9. Ideally, #6 is as far as most people should go.
I’ll share a bit more on the internal architecture of the bootstrapper and the settings file that makes this all happen in a later post.