Building a polymorphic installer with NSIS

I want to share something I find really cool (if you’re not into software, it might seem boring.) It’s an installer system for Windows called NSIS. Why is it cool? As a long-time software engineer, one big challenge I’ve seen is distributing software that actually runs reliably on clients computers.

Consider the hurdles:

  • 32-bit vs. 64-bit
  • OS versions
  • Dependencies
  • Runtime libraries

If a program runs in an incompatible environment, it simply won’t work. While the web bypasses many of these issues, it’s not always an option. Web apps are limited: they can’t do things like handle raw sockets. For those cases, you need a native app, and you’re back to handling the same challenges.

NSIS is extremely under-rated

The Nullsoft Install System (NSIS) lets you script software delivery for any Windows version. Its extensible plugin system provides high-level functions for nearly any need. Want a downloader? Just use the download plugin, pass your parameters, and execute. Plugins cover everything from cryptography to metadata access. See why I’m excited about this tool?

You can grab NSIS here: https://nsis.sourceforge.io/Download.

Let me show you some features I’ve used. Open NSIS, ignore the clutter, and click “Compile NSI Script.” This lets you write an NSI script that compiles into an EXE. The result? An installer that works from 95 to 11, and server versions!

Intro to NSI script

This is what a “hello, world” looks like in NSI. You define a name for the output binary. The main section is like the C main function. The language has special keywords for some functions. MessageBox shows an alert window when you run the installer.

; Use the ANSI compiler
Outfile "output.exe"

; Main installer program.
Section "MainSection"
    MessageBox MB_OK "Hello, world!"
SectionEnd

Variables

NSI scripts have global variables (declared at the top), in-built numbered variables (similar to the registers in assembly), and a stack (stacks are useful for algorithms.)

; Use the ANSI compiler
Outfile "output.exe"

Var /GLOBAL SysDrive

; Main installer program.
Section "MainSection"
    ; Init global var.
    StrCpy $SysDrive "C:"

    ; Param no variables.
    StrCpy $0 "test"
    StrCpy $1 "something"

    ; Basic stack manipulation.
    Push "Current top of stack"
    Push "New top of the stack"
    Pop $2 ; Take ^ this off the stack.
    MessageBox MB_OK $2
SectionEnd

Logic

The way logic is done in NSI uses function calls that do comparisons and then executes certain named branches of code. This is convoluted, and a much nicer way to do this is to use a special logic module that provides high-level macros. This allows for more familiar boolean comparisons.

!include "LogicLib.nsh"

; Use the ANSI compiler
Outfile "output.exe"

; Main installer program.
Section "MainSection"
    ; Show if.
    StrCpy $0 = "test"
    ${If} $0 == "test"
        MessageBox MB_OK "0 is test"
    ${EndIf}

    ; Show if else and basic numerical logic.
    StrCpy $0 = 10
    ${If} $0 > 10
        MessageBox MB_OK "0 > 10"
    ${Else}
        MessageBox MB_OK "0 is <= 10"
    ${EndIf}

    ; Show what a loop looks like.
    StrCpy $0 0
    loop:
        ${If} $0 >= 10
            Goto end_loop
        ${EndIf}
        IntOp $0 $0 + 1
        Goto loop
    end_loop:
        MessageBox MB_OK "left loop"
SectionEnd

Functions

In NSI you don’t declare parameters for functions. Since all variables are global you simply set them before the function call and the function takes what it needs directly. Rust users will be shaking! Don’t die though because it simplifies a lot of things. Look how clean the function looks without a prototype. Strange.

; Use the ANSI compiler
Outfile "output.exe"

Function Hello
    MessageBox MB_OK $0
FunctionEnd

; Main installer program.
Section "MainSection"
    StrCpy $0 "Hello, world!"
    Call Hello
SectionEnd

Plugins

To use plugins in NSI you need to open the install location for NSI and find the plugins folder (x86 program files/NSIS/Plugins.) There is a sub-folder for ANSI and unicode. Usually plugins provide a specific build for ASCI and unicode — though when they don’t I copy the plugin to both folders.

The code needs the inetc plugin. It can be downloaded from here: https://nsis.sourceforge.io/Inetc_plug-in

; Use the ANSI compiler
Outfile "output.exe"

; Main installer program.
Section "MainSection"
    StrCpy $0 "https://www.python.org/ftp/python/3.13.1/python-3.13.1-amd64.exe"
    StrCpy $1 "python.exe"
    inetc::get $0 $1 /END
    ExecWait '"$1"'
SectionEnd

Look at how succinct and powerful this code is. In 4 lines of code we have downloaded a file and executed it. In a way that will run on every version of Windows. That’s impressive.

The big idea

I love writing software in Python: it’s a beautiful, simple language. But I don’t want my users dealing with Python installs, especially on older Windows versions where specific Python releases, runtime libraries, or custom builds are often required.

I began by creating a conditional downloader to install Python 3 based on the Windows version. Then, I added features like SHA256 hash verification, pre-existing Python detection, and basic start menu launchers. And then it hit me:

What if I were to make the installer reusable?

A polymorphic installer

Instead of a fixed module installer I could simply get the module name from the file name. I developed the idea even further.

The installer would now use its own icon as the launcher icon for the module. And it was possible to change the URL mirror path for the binary files by editing the file description. What resulted was a reusable, Python 3 module installer that could be customized simply by editing a file name.

Here’s a link to the Github for that: https://github.com/robertsdotpm/win-auto-py3

And my NSI script code: https://github.com/robertsdotpm/win-auto-py3/blob/main/installer.nsi

Here’s the EXE (or build it yourself from the repo): https://github.com/robertsdotpm/win-auto-py3/raw/refs/heads/main/install_1NSERT-PYPI-PKG-HERE.exe

Closing thoughts

IMO, this demonstrates the potential of NSI for new distribution and packaging systems. I hope you found this interesting. The crazy thing is this only took a couple of days to build.

Also, somewhat humorously: theoretically my installer would solve the issue for that guy screaming for an EXE on Github. But in practice sherlock project is a bad example to test the tool with because it uses many modules that use C-extensions. When you mix native code with Python it almost always breaks installations (which kind of defeats the point of Python.)

Leave a Reply

Your email address will not be published. Required fields are marked *