Installing .NET using Inno Setup

There are many blog posts online telling you how to install the .NET Framework using Inno Setup. Most of them work in most cases, but they fail to properly handle restarting the machine if the .NET Framework installer needs to restart, or handle errors encountered by the .NET Framework installer. If you put a bit more effort in (and learn a bit of Pascal!), you can write something more robust.

There are many people advocating many different approaches. Many of them are a variation on this:

[Files]
Source: "dotNet452Setup.exe"; DestDir: {tmp}; Flags: deleteafterinstall; Check: FrameworkIsNotInstalled

[Run]
Filename: "{tmp}\dotNetFx40_Full_setup.exe"; Check: FrameworkIsNotInstalled

[code]
function FrameworkIsNotInstalled: Boolean;
begin
  Result := not RegKeyExists(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\.NETFramework\policy\v4.0');
end;

This simple example has number of problems:

  1. It doesn’t correctly detect the installed framework version.
  2. It doesn’t start the .NET Framework installer with the correct flags, this means that it might restart the PC (potenailly confusing Inno Setup if you have other items under [Run]), and will require interaction from the user.
  3. It doesn’t attempt to detect the return code from the .NET Framework installer, and so won’t notice any errors.

I happened to stumble on this approach which correctly detects the installed framework version, and also runs the .NET Framework installer from Pascal, meaning that we can detect the return code. It just shows a dialog box and carries on with the installation if it fails to launch the installer, but it provides a good starting point.

I developed this into a more comprehensive solution. This will:

  • Detect the correct framework version, and only run the installer if necessary
  • Detect if the .NET installer framework failed, and abort the installation
  • Detect if the .NET installer requires a machine restart, and prompt the user at the end of installation
  • Display a progress bar and message while installation is happening
  • Run the .NET installer with the correct flags to prevent it from restarting the user’s PC (before your installation has finished), and avoid the need for input from the user
  • Run the .NET installer before your application is installed, rather than after
#define DotNetInstallerExe "dotNet452Setup.exe"

[CustomMessages]
InstallingDotNetFramework=Installing .NET Framework. This might take a few minutes...
DotNetFrameworkFailedToLaunch=Failed to launch .NET Framework Installer with error "%1". Please fix the error then run this installer again.
DotNetFrameworkFailed1602=.NET Framework installation was cancelled. This installation can continue, but be aware that this application may not run unless the .NET Framework installation is completed successfully.
DotNetFrameworkFailed1603=A fatal error occurred while installing the .NET Framework. Please fix the error, then run the installer again.
DotNetFrameworkFailed5100=Your computer does not meet the requirements of the .NET Framework. Please consult the documentation.
DotNetFrameworkFailedOther=The .NET Framework installer exited with an unexpected status code "%1". Please review any other messages shown by the installer to determine whether the installation completed successfully, and abort this installation and fix the problem if it did not.

[Files]
; This should be near the beginning, as it's extracted individually first
Source: "{#DotNetInstallerExe}"; DestDir: "{tmp}"; Flags: dontcopy nocompression noencryption

[Code]
var
  GlobalRestartRequired: boolean;

function DotNetIsMissing(): Boolean;
var 
  Exists: Boolean;
  Release: Cardinal;
begin
  Exists := RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', Release);
  ; This magic number is for .NET 4.5.2. Pick the correct number for your .NET version from
  ; https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx
  Result := not Exists or (Release < 379893);
end;

// Adapted from https://blogs.msdn.microsoft.com/davidrickard/2015/07/17/installing-net-framework-4-5-automatically-with-inno-setup/
function InstallDotNet(): String;
var
  StatusText: string;
  ResultCode: Integer;
begin
  StatusText := WizardForm.StatusLabel.Caption;
  WizardForm.StatusLabel.Caption := CustomMessage('InstallingDotNetFramework');
  WizardForm.ProgressGauge.Style := npbstMarquee;
  try
    ExtractTemporaryFile('{#DotNetInstallerExe}');
    if not Exec(ExpandConstant('{tmp}\{#DotNetInstallerExe}'), '/passive /norestart /showrmui /showfinalerror', '', SW_SHOW, ewWaitUntilTerminated, ResultCode) then
    begin
      Result := FmtMessage(CustomMessage('DotNetFrameworkFailedToLaunch'), [SysErrorMessage(ResultCode)]);
    end
    else
    begin
      // See https://msdn.microsoft.com/en-us/library/ee942965(v=vs.110).aspx#return_codes
      case resultCode of
        0: begin
          // Successful
        end;
        1602 : begin
          MsgBox(CustomMessage('DotNetFrameworkFailed1602'), mbInformation, MB_OK);
        end;
        1603: begin
          Result := CustomMessage('DotNetFrameworkFailed1603');
        end;
        1641: begin
          GlobalRestartRequired := True;
        end;
        3010: begin
          GlobalRestartRequired := True;
        end;
        5100: begin
          Result := CustomMessage('DotNetFrameworkFailed5100');
        end;
        else begin
          MsgBox(FmtMessage(CustomMessage('DotNetFrameworkFailedOther'), [IntToStr(ResultCode)]), mbError, MB_OK);
        end;
      end;
    end;
  finally
    WizardForm.StatusLabel.Caption := StatusText;
    WizardForm.ProgressGauge.Style := npbstNormal;
  end;
end;

function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
  // 'NeedsRestart' only has an effect if we return a non-empty string, thus aborting the installation.
  // If the installers indicate that they want a restart, this should be done at the end of installation.
  // Therefore we set the global 'restartRequired' if a restart is needed, and return this from NeedRestart()

  if DotNetIsMissing() then
  begin
    Result := InstallDotNet();
  end;
end;

function NeedRestart(): Boolean;
begin
  Result := GlobalRestartRequired;
end;