Using the NSIS Install Engine

Whilst packaging up a plugin based on the Skype API that I had been writing, I was looking for a way to bundle it all up cleanly. The finished plugin consisted of an .exe, a COM wrapper DLL (the very useful Skype4COM wrapper), and some HTML documentation for the C++ code (generated by doxygen). I thought this might be a good opportunity to have a look at NSIS, the installer generator that is used by a huge number of open-source (and other) projects.

My install requirements were quite simple. The following steps needed to be performed:

  • Copy the .exe and associated HTML docs to the target system;
  • Copy the COM DLL to the target system;
  • Register the COM server (by invoking regsvr32);
  • Create any necessary Start Menu items (and an uninstaller);
  • Open a web browser automatically on the index page documentation.

Obviously, the uninstaller would need to remove all files, and also unregister the COM server before uninstalling.

I downloaded NSIS from the web site and fired up the NSIS Compiler. This is a very simple interface that allows you to drag-and-drop .NSI script files into its interface and execute them. The NSIS scripting runtime is a very simple scripting language, with some basic language constructs, and the ability to define and import macros and plugins for extended functionality.
A screenshot of the compiler interface is shown below:

NSIS Compiler Interface

Here is the full installation script for my plugin, annotated with some comments.

; Skype Plugin NSIS Installer Script File
; Rory Winston <rory@researchkitchen.co.uk>
;--------------------------------
;Include some predefined NSIS libraries (for modern UI look and feel)

  !include "MUI.nsh"

;--------------------------------

XPStyle on

;General

  ;Name and file
  Name "Skype Plugin"
  OutFile "SkypePluginInstaller.exe"

  ;Default installation folder
  InstallDir "$PROGRAMFILES\Skype Plugin"

  ;Get installation folder from registry if available
  InstallDirRegKey HKCU "Software\Skype Plugin" ""

;--------------------------------
;Interface Settings

  !define MUI_ABORTWARNING

;--------------------------------
;Pages
; These are predefined "wizard pages" that NSIS can generate for us. Note that I 
; am including a custom license file on the MUI_PAGE_LICENSE page.
  !insertmacro MUI_PAGE_LICENSE "NOTICE.txt"
  !insertmacro MUI_PAGE_COMPONENTS
  !insertmacro MUI_PAGE_DIRECTORY
  !insertmacro MUI_PAGE_INSTFILES

  !insertmacro MUI_UNPAGE_CONFIRM
  !insertmacro MUI_UNPAGE_INSTFILES

;--------------------------------
;Languages

  !insertmacro MUI_LANGUAGE "English"

; Show a custom splash screen on installer entry
Function .onInit
        InitPluginsDir
        File /oname=$PLUGINSDIR\splash.bmp "installer.bmp"

        splash::show 1000 $PLUGINSDIR\splash

        Pop $0 ; $0 has '1' if the user closed the splash screen early,
                        ; '0' if everything closed normally, and '-1' if some error occurred.
FunctionEnd

;--------------------------------
;Installer Sections

Section "Install Skype Plugin" SecSkypePlugin

  SetOutPath "$INSTDIR"

  ; Copy all of the required files (note the /r switch to recursively copy directories)
  File "SkypePlugin.exe"
  File "Tariffs.dat"
  File /r "..\docs"
  File "Skype4COM.dll"

  ; Register the COM server
  RegDLL Skype4COM.dll

  ;Store installation folder
  WriteRegStr HKCU "Software\Skype Plugin" "" $INSTDIR

  ;Create uninstaller
  WriteUninstaller "$INSTDIR\Uninstall.exe"

  ; Open the docs using the default (shell-assigned) browser
  ExecShell "open" "$INSTDIR\docs\html\index.html"

SectionEnd

; Optional section (can be disabled by the user)
Section "Start Menu Shortcuts"

  CreateDirectory "$SMPROGRAMS\Skype Plugin"
  CreateShortCut "$SMPROGRAMS\Skype Plugin\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0
  CreateShortCut "$SMPROGRAMS\Skype Plugin\Skype Plugin.lnk" "$INSTDIR\SkypePlugin.exe" "" "$INSTDIR\SkypePlugin.exe" 0
  CreateShortCut "$SMPROGRAMS\Skype Plugin\Documentation.lnk" "$INSTDIR\docs\html\index.html" "" "$INSTDIR\docs\html\index.html" 0
SectionEnd


;--------------------------------
;Descriptions

  ;Language strings
  LangString DESC_SecSkypePlugin ${LANG_ENGLISH} "Skype Plugin Installation."

  ;Assign language strings to sections
  !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
    !insertmacro MUI_DESCRIPTION_TEXT ${SecSkypePlugin} $(DESC_SecSkypePlugin)
  !insertmacro MUI_FUNCTION_DESCRIPTION_END

;--------------------------------
;Uninstaller Section

Section "Uninstall"

  Delete "$INSTDIR\Uninstall.exe"

  ; Unregister the COM server
  UnRegDLL "$INSTDIR\Skype4COM.dll"

  ; Delete all installed files
  RMDir "$INSTDIR"

  ; And registry entries
  DeleteRegKey /ifempty HKCU "Software\Skype Plugin"

  ; Remove shortcuts, if any
  Delete "$SMPROGRAMS\Skype Plugin\*.*"
  RMDir "$SMPROGRAMS\Skype Plugin"

SectionEnd

Some points of note:

  • The RegDLL and UnRegDLL directives will register and unregister a COM server on your behalf;
  • NSIS provides “hooks” at various points of the installer lifecycle. For instance, the splash screen in shown by the .onInit handler.
  • The ExecShell command (which seems to be a high-level wrapper around the ShellExecute function), can be used to open arbitrary files.

NSIS is a great application, and incredibly easy to pick up and use. The installers it produces are very slick looking, and can be extensively customised. It’s no wonder that this framework is now so popular.

Web Retrieval With Ruby

I recently had the need to automatically retrieve and parse a table of BT fixed-line call tariff data. Normally I would use Perl for this sort of thing. However on this occasion, I decided this might be a good opportunity to learn a bit of Ruby.


require 'net/http'
require 'html/tree'
require 'html/xmltree'
require 'http-access2'

client=HTTPAccess2::Client.new()
url = 'http://www.bt.com/...' # long URI omitted

parser = HTMLTree::Parser.new(false,false)
parser.feed(client.get_content(url))

tariffs = Array.new()

# Iterate through each <tr>
rows = parser.html.select { |ea| ea.tag == 'tr' }

# Extract and normalize the content
rows.each { |row|
texts = row.select { |item| item.data? }. # just look at cdata
collect { |data| data.strip }. # strip it
select { |data| data.size > 0 } # and keep the non-blank fields
texts = texts.join('|')

# Only store the contents that contain actual call tariff data
tariffs.push(texts) if (/^[^|]+\|((\d)+\.(\d)+\|){2}(\d)+\.(\d)+$/ =~ texts)
}

# Send to stdout so we can run $ ./client.rb > tariffs.dat
puts tariffs

This produces a pipe-delimited output of call tariffs by country. My initial impressions of Ruby (I’m way behind the curve here) are:

  • It’s very “Perl-like” in some ways – you can see a definite Perl influence in the language.
  • I love the iterator and closure syntax: collect(), map(), etc. It’s very clean and intuitive.
  • The idea of code blocks as first-class objects seems to be integral to Ruby: in the code above, the output of a select {} block is passed to a collect {} block, which is passed in turn to another select {} block (all done within an each {} block). Very reminiscent of the simple building block approach of Unix shell commands.

There seems to be a lot of hype around Ruby at the moment, mainly driven by Rails. However, the basic language itself is quite exciting in that it seems to be as useful and concise as Perl, whilst having some syntactic advantages that make it more readable and maintainable.