Ce billet existe aussi en français

This post is part of a series about how to package a XULRunner application.
See the preamble for the context case.

3 - Windows

3.1 Icons for Windows

We need a icon in .ico format. It will be used by ours windows, and by our launcher.

It's easy to make a .ico file on Linux, we just need to install the program icotool, from the icoutils package.
On Debian/Ubuntu:

apt-get install icoutils

Then we create a ico file from several png at different size, 16x16 px, 32x32 px, and 48x48 px. It would be possible to add more png, with different color depth as well.

icotool -c -o icon.ico icon16.png icon32.png icon48.png

You can find a bash script with this command line in the sample archive related to this chapter 3 (in samples_chapter_3/data/icons/).

To add our icon to our windows, rather than the firefox or xulrunner one, we have just to put this icon, renamed as default.ico, into a icons/default folder situated in our main chrome folder.
This icon will be used by all XUL <window> without an id attribute.
But the main XUL <window> of myapp has the attribute id="main", so for this window we must have an icon named main.ico, situated in the same folder:

    |- samples_chapter_3/
|- myapp/
|- chrome
|- icons/
|- default/
|- default.ico
|- main.ico

3.2 Create a launcher (batch or C)

The simplest launcher is a batch script. Here the content of myapp.bat:

set CUR_DIR=%~dp0

START firefox -app $CUR_DIR\application.ini

This file must be placed into the main folder of our app. This works, it launches our app via the application.ini file of our app.
But there's something annoying, a black command window is opened too.

To avoid the previously mentioned default, we will create a launcher in C.

We use the code of XAL (XUL Application Launcher). This is a light C program, MIT licensed, it launches a XULRunner application with Firefox, with the -app argument and the application.ini. It must be placed in the main application folder (like the previous batch). Bonus, it handles additional arguments (like -jsconsole) if used in command line. And we can add our icon, and some application information via a .rc file.

This executable can be compiled with any C compiler, its code is independent of the Mozilla code. I personaly compile it with MinGW, on Linux, with success. If you plan to use another compiler, edit the build file, and adapt the CC and RESCOMP variables.

To install MinGW on Debian/Ubuntu:

apt-get install mingw32

I don't publish here the C source of this code, you'll find it in the archive related to this chapter 3, or in its dedicated page.

We can customize this launcher, using a resource (.rc) file, insert our icon and specify some information (vendor, app name, version,...).

Here the content of the myapp-res.rc file:


BLOCK "StringFileInfo"
BLOCK "000004B0"
VALUE "Comments", "Published under the MPL 1.1/GPL 2.0/LGPL 2.1 licenses"
VALUE "CompanyName", "John Doe Organization"
VALUE "FileDescription", "MyApp"
VALUE "FileVersion", "1.0.0"
VALUE "InternalName", "MyApp"
VALUE "LegalCopyright", "(c) 2010 John Doe"
VALUE "ProductName", "MyApp"
VALUE "ProductVersion", "0.0.0"
BLOCK "VarFileInfo"
VALUE "Translation", 0x000, 1200

Some details about this content:

The line APPLICATION_ICON ICON "myapp.ico" integrate our icon

The FILEVERSION and PRODUCTVERSION entries use a special format, in short integer comma integer comma integer comma integer.

The line VALUE "Translation", 0x000, 1200 specify that the strings for information use Unicode.

We can compile this launcher, using our resource file, with the build.sh script distributed with XAL:

sh build.sh myapp myapp-res.rc

After compilation, the size of the result file is ~21Kb, so very small (note that the size of the icon is included).

3.3 Create a NSIS script

There are several solution to create Windows installers. Here, we will use NSIS, because: it's open source, we can build our installer from Linux, and it's easy and powerful.
Firefox itself use NSIS for its Windows installer.

To install NSIS tools on Debian/Ubuntu:

apt-get install nsis

NSIS uses its own script language to create installers. We can use some default pages, create some custom ones, manage files (un)installation, act on the Windows Registry,...
I will not do a complete NSIS documentation here, see their wiki for that, there's a lot of explanations and examples.
You should have some local examples and doc after installing nsis, in /usr/share/doc/nsis/Examples and /usr/share/doc/nsis/Doc.

But here's the main proposed script here, and I will explain what actions are performed after:

!define PRODUCT_NAME "MyApp"
!define PRODUCT_VERSION "1.0"


!define HKEY_ROOT "HKLM"
!define UN_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"




; The name of the product installed

; The file to write

SetCompressor /final /solid lzma
ShowInstDetails show
ShowUninstDetails show

; The default installation directory

; Request application privileges for Windows Vista
RequestExecutionLevel admin

Var Shortcuts_Dialog
Var Shortcuts_Label
Var Shortcuts_SM_Checkbox
Var Shortcuts_SM_Checkbox_State
Var Shortcuts_D_Checkbox
Var Shortcuts_D_Checkbox_State

Var Previous_Uninstall
Var Previous_Uninstall_dir
Var TempUninstallPath

!include "MUI2.nsh"
!include "FileFunc.nsh"


VIAddVersionKey "ProductName" "${PRODUCT_NAME}"
;VIAddVersionKey "CompanyName" "${CompanyName}"
;VIAddVersionKey "LegalTrademarks" "${BrandShortName} is a Trademark of"
;VIAddVersionKey "LegalCopyright" "${CompanyName}"
VIAddVersionKey "LegalCopyright" ""
VIAddVersionKey "FileVersion" "${PRODUCT_VERSION}"
VIAddVersionKey "ProductVersion" "${PRODUCT_VERSION}"
VIAddVersionKey "FileDescription" "${PRODUCT_NAME} Installer"
VIAddVersionKey "OriginalFilename" "${INSTALLER_NAME}"


; Pages

!insertmacro MUI_PAGE_WELCOME
Page custom onShortcutsPageCreate
!insertmacro MUI_PAGE_FINISH


!insertmacro MUI_LANGUAGE "English"
!insertmacro MUI_LANGUAGE "French"

!include ./l10n/fr.nsh
!include ./l10n/en_US.nsh


Function .onInit
; an eventual previous version of the app should not be currently running.
; Abort if any.
; Explanation, when the application is running, a window with the className
; productnameMessageWindow exists
StrCmp $0 0 +3
MessageBox MB_OK|MB_ICONEXCLAMATION "${PRODUCT_NAME} is running. Please close it first" /SD IDOK

StrCpy $Shortcuts_SM_Checkbox_State 1
StrCpy $Shortcuts_D_Checkbox_State 1

Function un.onInit
; see Function .onInit
StrCmp $0 0 +3
MessageBox MB_OK|MB_ICONEXCLAMATION "${PRODUCT_NAME} is running. Please close it first" /SD IDOK

; custom page creation, for the shortcuts installation, using nsDialog
Function onShortcutsPageCreate

nsDialogs::Create 1018
Pop $Shortcuts_Dialog

${If} $Shortcuts_Dialog == error

${NSD_CreateLabel} 0 6 100% 12u $(l10n_CREATE_ICONS_DESC)
Pop $Shortcuts_Label

${NSD_CreateCheckbox} 15u 20u 100% 10u $(l10n_ICONS_STARTMENU)
Pop $Shortcuts_SM_Checkbox
GetFunctionAddress $0 OnSMCheckbox
nsDialogs::OnClick $Shortcuts_SM_Checkbox $0

${If} $Shortcuts_SM_Checkbox_State == ${BST_CHECKED}
${NSD_Check} $Shortcuts_SM_Checkbox

${NSD_CreateCheckbox} 15u 40u 100% 10u $(l10n_ICONS_DESKTOP)
Pop $Shortcuts_D_Checkbox
GetFunctionAddress $0 OnDCheckbox
nsDialogs::OnClick $Shortcuts_D_Checkbox $0

${If} $Shortcuts_D_Checkbox_State == ${BST_CHECKED}
${NSD_Check} $Shortcuts_D_Checkbox


; event when the Start Menu shortcut is (un)checked in the custom page
Function OnSMCheckbox
${NSD_GetState} $Shortcuts_SM_Checkbox $Shortcuts_SM_Checkbox_State
Pop $0 # HWND

; event when the Desktop shortcut is (un)checked in the custom page
Function OnDCheckbox
${NSD_GetState} $Shortcuts_D_Checkbox $Shortcuts_D_Checkbox_State
Pop $0 # HWND

Function WriteUninstallReg
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayName" \
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "UninstallString" \
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "QuietUninstallString" \
"$INSTDIR\uninstall.exe /S"
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "InstallLocation" \
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayIcon" \
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayVersion" \

${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD ${HKEY_ROOT} ${UN_KEY} "EstimatedSize" "$0"

; The stuff to install
Section ""
; uninstall an eventual previous installation
ReadRegStr $Previous_Uninstall ${HKEY_ROOT} ${UN_KEY} "UninstallString"
${If} $Previous_Uninstall != ""
StrCpy $Previous_Uninstall_dir $Previous_Uninstall
${GetParent} $Previous_Uninstall $Previous_Uninstall_dir

IfFileExists "$Previous_Uninstall" myUninstallPrevious myInstall
goto myInstall

; copy the previous uninstaller into TEMP
StrCpy $TempUninstallPath "$TEMP\${TMP_UNINSTALL_EXE}"
CopyFiles /SILENT "$Previous_Uninstall" "$TempUninstallPath"
IfErrors myInstall

ExecWait '"$TempUninstallPath" /S _?=$Previous_Uninstall_dir'

Delete "$TempUninstallPath"

;MessageBox MB_OK "UNINSTALL: finished"


; copy the files

WriteUninstaller "uninstall.exe"

Call WriteUninstallReg

${If} $Shortcuts_SM_Checkbox_State == ${BST_CHECKED}
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" \

${If} $Shortcuts_D_Checkbox_State == ${BST_CHECKED}
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" \

; Uninstaller

Section "Uninstall"
; Remove installed files and uninstaller
!include ./uninstall_files.nsi
Delete "$INSTDIR\uninstall.exe"

; remove installed directories
!include ./uninstall_dirs.nsi
RMDir /r "$INSTDIR\extensions"

; Remove shortcuts, if any
;TODO remove eventual quicklaunch Too

; Remove the installation directory used (if empty)

; and delete the registry key for uninstall
DeleteRegKey ${HKEY_ROOT} ${UN_KEY}

Some descriptions about this script:

It uses the default "modern" skin. But it's possible to customize it.

It uses some localized parts, including some other nsis scripts (described later).

It uses some default nsis pages, and a custom one to allow the creation of shortcuts on the desktop and in the start menu.

The installation and uninstallation are aborted if our application is currently running.
In fact, when our app is running, XULRunner/Firefox create a native Windows window with a class MyAppMessageWindow, this class name is the value of the field Name of the application.ini concatenated with "MessageWindow".
The script just checks if such a window with this class name is opened, then abort.

It creates some minimal Registry entries to allow the uninstallation of the app with the "add/remove programs" tool.

If our app is already installed, the previous version is uninstalled before the new installation, using our previous uninstaller. And, to be more precise, this previous uninstaller is copied and launched from the Windows temp folder.

For the uninstaller, we need the complete list of installed files and directories. These lists will be dynamically created later, for the moment in this script we just include them as other nsis scripts (uninstall_files.nsi and uninstall_dirs.nsi).

Now here the content of one of the localized included script, en_US.nsh:

;!!! IMPORTANT: this must file must be edited with ANSI charset (in fact the
; Windows CP-1252 charset), but if there's no special character, UTF-8 is ok,
; because it's a subset ;)

"Set Up Shortcuts"
"Create Program Icons"
"Create icons for ${PRODUCT_NAME}:"
"On my &Desktop"
"In my &Start Menu Programs folder"

We have to be careful with the encoding of these files, this English one is not a problem, but the French one must be edited with the Windows CP-1252 charset for exemple. These variables are simply used in our main script with, for example, $(l10n_SHORTCUTS_PAGE_TITLE).

Note that the ampersand & in the string definitions define an access key for controls in the UI, here there's alt+D and alt+S.

3.4 Create the installer

We have now all the files to create the installer. Let see our sources tree:

|- data/
|- icons/
|- icon.ico
|- myapp.bat
|- myapp-res.rc
|- myapp/
|- win
|- nsis/
|- l10n/
|- en_US.nsh
|- fr.nsh
|- myapp.nsi
|- xal-src/
|- build.sh
|- clean.sh
|- main.c

We have to build our launcher from the C source and our data (icon and resource). And create 2 additional nsis scripts, listing our app files. Then we can create the installer with makensis. We will doing this in a temporary folder. Here's the script, named build_win.sh:


# exit the script on errors
set -e


# get the absolute path of our TMP_DIR folder
TMP_DIR=$(readlink -f -- "$TMP_DIR")

# re-create the TMP_DIR folder
rm -rfv "$TMP_DIR"
mkdir -v "$TMP_DIR"

# copy our app
cp -rv myapp "$TMP_DIR/"

# copy the XAL launcher sources
cp -rv win/xal-src "$TMP_DIR/xal"
# and our icon and resource file
cp -v data/icons/icon.ico "$TMP_DIR/xal/myapp.ico"
cp -v data/myapp-res.rc "$TMP_DIR/xal/"

# build the launcher
cd "$TMP_DIR/xal/"
sh build.sh myapp myapp-res.rc
cd -

# copy the launchers in the right folder
cp -v data/myapp.bat "$TMP_DIR/myapp/"
cp -v "$TMP_DIR/xal/myapp.exe" "$TMP_DIR/myapp/"

# delete the xal sources
rm -rv "$TMP_DIR/xal"

# create the nsis script listing the files to unsinstall
cd "$TMP_DIR/myapp"
find . -maxdepth 1 -type f > ../uninstall_files.nsi
sed -i -r "s|^\./(.*)$| Delete \"\$INSTDIR\\\\\1\"|" ../uninstall_files.nsi

# and the list of directory
ls -d */ > ../uninstall_dirs.nsi
sed -i -r "s|^(.*)/$| RMDir /r \"\$INSTDIR\\\\\1\"|" ../uninstall_dirs.nsi
cd -

# copy the other nsis scripts
cp -rv win/nsis/* "$TMP_DIR/"

# and create the installer
makensis "$TMP_DIR/myapp.nsi"

# finally, copy our installer in the root sources dir
cp -v "$TMP_DIR/MyApp-1.0-install.exe" ./

echo "Windows installer for myapp created."

To launch this script, in a terminal:

  • cd samples_chapter_3
  • sh ./build_win.sh

And as result we have finally the file MyApp-1.0-install.exe in the samples_chapter_3 folder :) .

Nicolas Martin

You can download all the samples of this chapter 3 (Windows) in the samples_chapter_3.tar.gz archive.

The myapp application, from developer.mozilla.org is in the Public Domain.

The icon used is from the Tango Desktop Project, and is in the Public Domain.

The C launcher XUL App Launcher (XAL) is under the MIT license.

All other added data, and sample files, of this chapter 3, are in the Public Domain too.