Installing .NET 5 Runtime Automatically with Inno Setup

In this guide I will walk through how to get the .NET 5 runtime to download and install on-the-fly in an Inno Setup installer.

It works in 3 steps:

  1. Detect if the desired .NET runtime is installed
  2. Download the .NET Runtime bootstrap installer with Inno Download Plugin
  3. Run the bootstrap installer in quiet mode, which will download and install the .NET Framework. This is better than downloading the full installer since it only downloads the files it needs for your platform.

You can download the regular .NET Runtime or the .NET Desktop Runtime, which supports Windows desktop applications. If you install the Desktop Runtime you do not need to also install the normal .NET Runtime.

Here’s the full code I’m using, with the x64 .NET 5 Desktop Runtime:

#include <idp.iss>

// Other parts of installer file go here

[CustomMessages]
IDP_DownloadFailed=Download of .NET 5 failed. .NET 5 Desktop runtime is required to run VidCoder.
IDP_RetryCancel=Click 'Retry' to try downloading the files again, or click 'Cancel' to terminate setup.
InstallingDotNetRuntime=Installing .NET 5 Desktop Runtime. This might take a few minutes...
DotNetRuntimeFailedToLaunch=Failed to launch .NET Runtime Installer with error "%1". Please fix the error then run this installer again.
DotNetRuntimeFailed1602=.NET Runtime installation was cancelled. This installation can continue, but be aware that this application may not run unless the .NET Runtime installation is completed successfully.
DotNetRuntimeFailed1603=A fatal error occurred while installing the .NET Runtime. Please fix the error, then run the installer again.
DotNetRuntimeFailed5100=Your computer does not meet the requirements of the .NET Runtime. Please consult the documentation.
DotNetRuntimeFailedOther=The .NET Runtime 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.

[Code]

var
  requiresRestart: boolean;

function CompareVersion(V1, V2: string): Integer;
var
  P, N1, N2: Integer;
begin
  Result := 0;
  while (Result = 0) and ((V1 <> '') or (V2 <> '')) do
  begin
    P := Pos('.', V1);
    if P > 0 then
    begin
      N1 := StrToInt(Copy(V1, 1, P - 1));
      Delete(V1, 1, P);
    end
      else
    if V1 <> '' then
    begin
      N1 := StrToInt(V1);
      V1 := '';
    end
      else
    begin
      N1 := 0;
    end;
    P := Pos('.', V2);
    if P > 0 then
    begin
      N2 := StrToInt(Copy(V2, 1, P - 1));
      Delete(V2, 1, P);
    end
      else
    if V2 <> '' then
    begin
      N2 := StrToInt(V2);
      V2 := '';
    end
      else
    begin
      N2 := 0;
    end;
    if N1 < N2 then Result := -1
      else
    if N1 > N2 then Result := 1;
  end;
end;

function NetRuntimeIsMissing(): Boolean;
var
  runtimes: TArrayOfString;
  registryKey: string;
  I: Integer;
  meetsMinimumVersion: Boolean;
  meetsMaximumVersion: Boolean;
  minimumVersion: string;
  maximumExclusiveVersion: string;
begin
  Result := True;

  minimumVersion := '5.0.0';
  maximumExclusiveVersion := '5.1.0';
  registryKey := 'SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App';
  if RegGetValueNames(HKLM, registryKey, runtimes) then
  begin
    for I := 0 to GetArrayLength(runtimes)-1 do
    begin
      meetsMinimumVersion := not (CompareVersion(runtimes[I], minimumVersion) = -1);
      meetsMaximumVersion := CompareVersion(runtimes[I], maximumExclusiveVersion) = -1;
      if meetsMinimumVersion and meetsMaximumVersion then
      begin
        Log(Format('[.NET] Selecting %s', [runtimes[I]]));
        Result := False;
          Exit;
      end;
    end;
  end;
end;

procedure InitializeWizard;
begin
  if NetRuntimeIsMissing() then
  begin
    idpAddFile('http://go.microsoft.com/fwlink/?linkid=2155258', ExpandConstant('{tmp}\NetRuntimeInstaller.exe'));
    idpDownloadAfter(wpReady);
  end;
end;

function InstallDotNetRuntime(): String;
var
  StatusText: string;
  ResultCode: Integer;
begin
  StatusText := WizardForm.StatusLabel.Caption;
  WizardForm.StatusLabel.Caption := CustomMessage('InstallingDotNetRuntime');
  WizardForm.ProgressGauge.Style := npbstMarquee;
  try
    if not Exec(ExpandConstant('{tmp}\NetRuntimeInstaller.exe'), '/passive /norestart /showrmui /showfinalerror', '', SW_SHOW, ewWaitUntilTerminated, ResultCode) then
    begin
      Result := FmtMessage(CustomMessage('DotNetRuntimeFailedToLaunch'), [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('DotNetRuntimeFailed1602'), mbInformation, MB_OK);
        end;
        1603: begin
          Result := CustomMessage('DotNetRuntimeFailed1603');
        end;
        1641: begin
          requiresRestart := True;
        end;
        3010: begin
          requiresRestart := True;
        end;
        5100: begin
          Result := CustomMessage('DotNetRuntimeFailed5100');
        end;
        else begin
          MsgBox(FmtMessage(CustomMessage('DotNetRuntimeFailedOther'), [IntToStr(resultCode)]), mbError, MB_OK);
        end;
      end;
    end;
  finally
    WizardForm.StatusLabel.Caption := StatusText;
    WizardForm.ProgressGauge.Style := npbstNormal;
    
    DeleteFile(ExpandConstant('{tmp}\NetRuntimeInstaller.exe'));
  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 NetRuntimeIsMissing() then
  begin
    Result := InstallDotNetRuntime();
  end;
end;

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

Detecting if the desired .NET Runtime is installed

Antoine Aflalo has found the registry key location to check to see what versions of the .NET runtime are installed.

Check SOFTWARE\dotnet\Setup\InstalledVersions\x86\sharedfx on x86 or SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\x64\sharedfx on x64. Use Microsoft.WindowsDesktop.App for the Desktop Runtime and Microsoft.NETCore.App for the regular runtime.

I am checking SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App .

We are checking for a version installed >= 5.0.0 and < 5.1.0. Having .NET 6 installed would not run apps built for the .NET 5 Runtime.

Downloading the bootstrapper

First you need to locate the correct download link for the installer. This will depend on if you want the x86 or x64 version, what version you’re installing, and whether you want the normal or Desktop runtime. The Inno Dependency Installer source code has a good listing of stable links; I haven’t been able to find an official list yet.

You could also use the Inno Dependency Installer instead, though I like having slimmer installer code for just the framework I want to install, and I think checking the registry is more efficient than including another executable to run and check the version.

Now install Inno Download Plugin. This will make the idpAddFile and idpDownloadAfter calls inside InitializeWizard work. Remember that you need #include <idp.iss> at the top of your file to bring this in.

You will also need to add a line to the end of C:\Program Files (x86)\Inno Setup 6\ISPPBuiltins.iss . The Inno Download Plugin installer tries to do it but hasn’t been updated in a while and is still trying to update Inno Setup 5.

#pragma include __INCLUDE__ + ";" + "C:\Program Files (x86)\Inno Download Plugin"

This “InitializeWizard” method is a special one that InnoSetup calls when first setting up its wizard pages. We call our helper function to detect if the framework is installed, then schedule a download step after the “Ready to install” page. We include our direct download link determined earlier, and save it in a temp folder, with a file name of “NetRuntimeInstaller.exe”. This name I picked arbitrarily; we just need to refer to it later when we’re installing and cleaning up.

Installing the bootstrapper

Our code is activated on PrepareToInstall. When this function returns a string, that string is shown as an error message that stops the install from happening. We call into InstallDotNetRuntime and return its result.

Inside InstallDotNetRuntime we’re running the bootstrapper we downloaded earlier, with a flag to make the install passive (non interactive). The /showrmui option prompts the user to close applications to avoid a system restart. /showfinalerror tells the installer to show an error message if the install fails.

The .NET installer UI will show on top of the first window:

After running the installer, the bootstrapper is deleted (whether or not the install succeeded).

And now your installer is done!

Testing the installer

I would recommend setting up a Windows 10 VM in Hyper-V to test it out. You can go to “Apps and Features” and sort by install date to uninstall your app and the .NET Runtime to repeat the testing.

Thanks to Antony Male for suggesting updates to the error handling code.