Illustration about building from a Linux CI pipeline

Building a Windows Installer from a Linux CI Pipeline

With the rise of Go, cross-compiling platform agnostic code has become more accessible than ever. Building a Windows binary of your Go application on Linux is now as simple as setting an environment variable. To make Windows binaries easier for users to install, they’re packaged into a standalone executable referred to as an installer.

Typically, Windows installers are built on the Windows platform, but solutions do exist to build a Windows installer from Linux, which makes creating such installers possible in a Linux-based CI pipeline ,Wine not necessary. CyberArk has several pieces of software written in Go that are all built on a Linux-based CI pipeline. To build installers for these tools, we turned to the Nullsoft Scriptable Install System (or NSIS).

About NSIS

NSIS was originally written by the creators of Winamp, the music player for Windows, and was released as open source software in 2001. It steadily gained popularity as an alternative to less flexible, proprietary alternatives, and is used as the installer generator of choice for well known products such as VLC and VirtualBox.

Thanks to its script-based approach, NSIS has an extensive feature set. Some highlights include localization & Unicode support, support for Windows XP & older, a powerful API with a considerable number of freely available plugins, silent mode, and support for modification of almost any Windows subsystem (registry, filesystem, windowing system etc). There are also a wealth of existing scripts available. Best of all, the compiler can built on POSIX-compliant platforms thanks to the heavily modular nature of the system.

Creating An Installer

Here we’ll walk through the process of preparing and building a basic installer.

Gathering Artifacts

The first step is to assemble all the artifacts for NSIS to compile into the installer. If an artifact repository isn’t available and Jenkins is being used, the copy artifact plugin might come in handy. The demo installer below expects a directory named `install_dir` to be present beside the script, and that it contain what the installer will place in the installation directory.

Additionally, there are at least two components (see below) of the UI that ought to be customized, and you can reference the MUI documentation for a complete list of options. NSIS is very picky about having visual assets in the right format to avoid bundling image decoding libraries, but the free image editing software GIMP supports all of the necessary formats and options.

  1. Windows applications are typically displayed with an icon chosen by the author, and the same goes for the installer executable. The icon should be a 48×48 square with a transparent background (see Contrib\Graphics\Icons\modern-install.ico for the default). The icon must be saved as .ico, which does allow for multiple resolutions and bits per pixel, but for modern Windows systems this one configuration will suffice.
  2. There is a banner down the left of the UI during the install process, which can be customized. The banner must be a 164×314 image with no transparency (match background with MUI_BGCOLOR which is white, see Contrib\Graphics\Wizard\win.bmp for default). It must be saved as a bitmap (.bmp) with a 32 bitdepth and no alpha channel.

An Example Script

The script below will generate an installer similar to what the Windows-only Quick Setup Script Generator produces, but has been rewritten to allow easy modification without the generator. It provides for a welcome page, an optional license agreement, install-directory selection, and an optional start menu folder. Each field containing `CHANGEME` should be edited to reflect the application being installed.

!define APP_NAME "[CHANGEME Application Name]"
!define COMP_NAME "[CHANGEME Company Name]"
!define WEB_SITE "[CHANGEME Application/Company Website]"
!define VERSION "[CHANGEME Application Version (X.X.X.X)]"
!define COPYRIGHT "[CHANGEME Company Name, Year]"
!define DESCRIPTION "[CHANGEME Short Description of Application]"
!define INSTALLER_NAME "[CHANGEME Installer Filename .exe]"
!define MAIN_APP_EXE "[CHANGEME Main Application Executable]"
!define ICON "[CHANGEME Installer Icon Filename .ico]"
!define BANNER "[CHANGEME Installer Banner Filename .bmp]"
#!define LICENSE_TXT "[CHANGEME License Text Document]"

!define INSTALL_DIR "$PROGRAMFILES64\${APP_NAME}"
!define INSTALL_TYPE "SetShellVarContext all"
!define REG_ROOT "HKLM"
!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}"
!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
!define REG_START_MENU "Start Menu Folder"

var SM_Folder

######################################################################

VIProductVersion  "${VERSION}"
VIAddVersionKey "ProductName"  "${APP_NAME}"
VIAddVersionKey "CompanyName"  "${COMP_NAME}"
VIAddVersionKey "LegalCopyright"  "${COPYRIGHT}"
VIAddVersionKey "FileDescription"  "${DESCRIPTION}"
VIAddVersionKey "FileVersion"  "${VERSION}"

######################################################################

SetCompressor /SOLID Lzma
Name "${APP_NAME}"
Caption "${APP_NAME}"
OutFile "${INSTALLER_NAME}"
BrandingText "${APP_NAME}"
InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" ""
InstallDir "${INSTALL_DIR}"

######################################################################

!define MUI_ICON "${ICON}"
!define MUI_UNICON "${ICON}"
!define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "${BANNER}"

######################################################################

!include "MUI2.nsh"

!define MUI_ABORTWARNING
!define MUI_UNABORTWARNING

!insertmacro MUI_PAGE_WELCOME

!ifdef LICENSE_TXT
!insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}"
!endif

!insertmacro MUI_PAGE_DIRECTORY

!ifdef REG_START_MENU
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "${APP_NAME}"
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}"
!define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}"
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}"
!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder
!endif

!insertmacro MUI_PAGE_INSTFILES

!insertmacro MUI_PAGE_FINISH

!insertmacro MUI_UNPAGE_CONFIRM

!insertmacro MUI_UNPAGE_INSTFILES

!insertmacro MUI_UNPAGE_FINISH

!insertmacro MUI_LANGUAGE "English"

######################################################################

Section -MainProgram
	${INSTALL_TYPE}

	SetOverwrite ifnewer
	SetOutPath "$INSTDIR"
	File /r "install_dir\\"

SectionEnd

######################################################################

Section -Icons_Reg
SetOutPath "$INSTDIR"
WriteUninstaller "$INSTDIR\uninstall.exe"

!ifdef REG_START_MENU
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
CreateDirectory "$SMPROGRAMS\$SM_Folder"
CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
CreateShortCut "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe"

!ifdef WEB_SITE
WriteIniStr "$INSTDIR\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}"
CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME} Website.lnk" "$INSTDIR\${APP_NAME} website.url"
!endif
!insertmacro MUI_STARTMENU_WRITE_END
!endif

!ifndef REG_START_MENU
CreateDirectory "$SMPROGRAMS\${APP_NAME}"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe"

!ifdef WEB_SITE
WriteIniStr "$INSTDIR\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Website.lnk" "$INSTDIR\${APP_NAME} website.url"
!endif
!endif

WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "DisplayName" "${APP_NAME}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "UninstallString" "$INSTDIR\uninstall.exe"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "DisplayIcon" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "DisplayVersion" "${VERSION}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "Publisher" "${COMP_NAME}"

!ifdef WEB_SITE
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "URLInfoAbout" "${WEB_SITE}"
!endif
SectionEnd

######################################################################

Section Uninstall
${INSTALL_TYPE}

RmDir /r "$INSTDIR"

!ifdef REG_START_MENU
!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder
Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk"
!ifdef WEB_SITE
Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME} Website.lnk"
!endif
Delete "$DESKTOP\${APP_NAME}.lnk"

RmDir "$SMPROGRAMS\$SM_Folder"
!endif

!ifndef REG_START_MENU
Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\${APP_NAME}\Uninstall ${APP_NAME}.lnk"
!ifdef WEB_SITE
Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Website.lnk"
!endif
Delete "$DESKTOP\${APP_NAME}.lnk"

RmDir "$SMPROGRAMS\${APP_NAME}"
!endif

DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
SectionEnd

NSIS Dockerized

For those with dockerized pipelines here are two dockerfiles that can be used to quickly deploy NSIS. The first installs NSIS from Debian’s Stretch repository which includes v2.51, though it should easily be adapted to future releases or testing/sid.

FROM debian:stretch

RUN apt-get update -y && \
    DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y nsis nsis-doc nsis-pluginapi && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /installer

ENTRYPOINT [ "makensis", "-V4" ]
FROM debian:stretch as builder

RUN apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
      build-essential \
      scons \
      unzip \
      wget \
      ca-certificates \
      zlib1g-dev

ENV NSIS_VERSION=3.03

WORKDIR /nsis

RUN wget -q -O nsis-${NSIS_VERSION}-src.tar.bz2 "http://downloads.sourceforge.net/project/nsis/NSIS 3/${NSIS_VERSION}/nsis-${NSIS_VERSION}-src.tar.bz2?use_mirror=autoselect" && \
    wget -q -O nsis.zip "http://downloads.sourceforge.net/project/nsis/NSIS 3/${NSIS_VERSION}/nsis-${NSIS_VERSION}.zip?use_mirror=autoselect" && \
    tar -jxf nsis-${NSIS_VERSION}-src.tar.bz2 && \
    unzip nsis.zip -d /nsis

WORKDIR /nsis/nsis-$NSIS_VERSION-src

RUN scons NSIS_MAX_STRLEN=8192 NSIS_CONFIG_CONST_DATA_PATH=no SKIPSTUBS=all SKIPPLUGINS=all SKIPUTILS=all SKIPMISC=all PREFIX=/nsis/nsis-$NSIS_VERSION install-compiler && \
    mv /nsis/nsis-$NSIS_VERSION/makensis /nsis/nsis-$NSIS_VERSION/Bin/ #no idea why this is necessary, it looks for everything in ../

RUN mv /nsis/nsis-$NSIS_VERSION /nsis/nsis


FROM debian:stretch as runner

COPY --from=builder /nsis/nsis/ /nsis/

WORKDIR /installer

ENTRYPOINT [ "/nsis/Bin/makensis", "-V4" ]

Final Thoughts

Now that the installer’s ready and is part of the CI pipeline, here is some food for thought as your use of NSIS grows.

  • The NSIS wiki can be very outdated and contains lots of dead links. It’s best to get verification on what you read there to ensure the most modern approach is taken.
  • By default NSIS has a maximum string length of 1024 characters and will truncate longer strings, which can cause issues if you’re modifying path names or environment variables etc. Consider using the large strings special build if need be.
  • If an MSI is absolutely necessary in place of an executable installer, the `wixtools` package provides a set of tooling to build one. However, it supports a very limited subset of the standard and provides no user interface.
  • The installer assets are compressed as part of the build, with LZMA in the case of the example, so further size reduction is unlikely with zipfiles or executable compressors (UPX and family). Though, if you’re installing native binaries, don’t forget to `strip` your executables.
  • NSIS installers can be signed like any other Windows executable which will result in a less angry-looking UAC prompt. You can sign them from Linux using `signcode` which is a component of Mono, or `osslsigncode` from your distribution’s repository.