joliclic code

[version française]

Distribute your XULRunner app

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:

APPLICATION_ICON ICON "myapp.ico"

1 VERSIONINFO
    FILEVERSION     0,1,0,0
    PRODUCTVERSION  0,0,0,0
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "000004B0"
        BEGIN
            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"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x000, 1200
    END
END

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_INTERNAL_NAME "myapp"
!define PRODUCT_VERSION "1.0"
!define PRODUCT_WIN_VERSION "1.0.0.0"

!define MESSAGEWINDOW_NAME "${PRODUCT_NAME}MessageWindow"

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

!define LICENSE_PATH "${PRODUCT_INTERNAL_NAME}\LICENSE.txt"

!define INSTALLER_NAME "${PRODUCT_NAME}-${PRODUCT_VERSION}-install.exe"
!define TMP_UNINSTALL_EXE "${PRODUCT_INTERNAL_NAME}_uninstall.exe"

;--------------------------------
;Variables

; The name of the product installed
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"

; The file to write
OutFile "${INSTALLER_NAME}"

SetCompressor /final /solid lzma
ShowInstDetails show
ShowUninstDetails show

; The default installation directory
InstallDir $PROGRAMFILES\${PRODUCT_NAME}

; 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"

VIProductVersion "${PRODUCT_WIN_VERSION}"

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}"

!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_INTERNAL_NAME}.exe"

;--------------------------------
; Pages

    !insertmacro MUI_PAGE_WELCOME
    !insertmacro MUI_PAGE_LICENSE "${LICENSE_PATH}"
    !insertmacro MUI_PAGE_DIRECTORY
    Page custom onShortcutsPageCreate
    !insertmacro MUI_PAGE_INSTFILES
    !insertmacro MUI_PAGE_FINISH
    
    !insertmacro MUI_UNPAGE_WELCOME
    !insertmacro MUI_UNPAGE_CONFIRM
    !insertmacro MUI_UNPAGE_INSTFILES
    
    
    !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
    FindWindow $0 "${MESSAGEWINDOW_NAME}"
    StrCmp $0 0 +3
        MessageBox MB_OK|MB_ICONEXCLAMATION "${PRODUCT_NAME} is running. Please close it first" /SD IDOK
        Abort
    
    StrCpy $Shortcuts_SM_Checkbox_State 1
    StrCpy $Shortcuts_D_Checkbox_State 1
FunctionEnd

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

; custom page creation, for the shortcuts installation, using nsDialog
Function onShortcutsPageCreate
    !insertmacro MUI_HEADER_TEXT $(l10n_SHORTCUTS_PAGE_TITLE) \
        $(l10n_SHORTCUTS_PAGE_SUBTITLE)
    
    nsDialogs::Create 1018
    Pop $Shortcuts_Dialog
    
    ${If} $Shortcuts_Dialog == error
        Abort
    ${EndIf}

    ${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
    ${EndIf}

    ${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
    ${EndIf}

    nsDialogs::Show
FunctionEnd

; 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
FunctionEnd

; 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
FunctionEnd

Function WriteUninstallReg
    WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayName" \
        "${PRODUCT_NAME} (${PRODUCT_VERSION})"
    WriteRegStr ${HKEY_ROOT} ${UN_KEY} "UninstallString" \
        "$INSTDIR\uninstall.exe"
    WriteRegStr ${HKEY_ROOT} ${UN_KEY} "QuietUninstallString" \
        "$INSTDIR\uninstall.exe /S"
    WriteRegStr ${HKEY_ROOT} ${UN_KEY} "InstallLocation" \
        "$INSTDIR"
    WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayIcon" \
        "$INSTDIR\${PRODUCT_INTERNAL_NAME}.exe"
    WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayVersion" \
        "${PRODUCT_VERSION}"
    
    ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
    IntFmt $0 "0x%08X" $0
    WriteRegDWORD ${HKEY_ROOT} ${UN_KEY} "EstimatedSize" "$0"
FunctionEnd

; The stuff to install
Section ""
    ; uninstall an eventual previous installation
    ReadRegStr $Previous_Uninstall ${HKEY_ROOT} ${UN_KEY} "UninstallString"
    ClearErrors
    ${If} $Previous_Uninstall != ""
        StrCpy $Previous_Uninstall_dir $Previous_Uninstall
        ${GetParent} $Previous_Uninstall $Previous_Uninstall_dir
        
        IfFileExists "$Previous_Uninstall" myUninstallPrevious myInstall
    ${Else}
        goto myInstall
    ${EndIf}
  
    myUninstallPrevious:
        ; copy the previous uninstaller into TEMP
        ClearErrors
        StrCpy $TempUninstallPath "$TEMP\${TMP_UNINSTALL_EXE}"
        CopyFiles /SILENT "$Previous_Uninstall" "$TempUninstallPath"
        IfErrors myInstall
        
        ClearErrors
        ExecWait '"$TempUninstallPath" /S _?=$Previous_Uninstall_dir'
        
        ClearErrors
        Delete "$TempUninstallPath"
        
        ;MessageBox MB_OK "UNINSTALL: finished"
    
    myInstall:
        SetOutPath $INSTDIR
        
        ; copy the files
        File /r ${PRODUCT_INTERNAL_NAME}\*
        
        WriteUninstaller "uninstall.exe"
        
        Call WriteUninstallReg
    
        ${If} $Shortcuts_SM_Checkbox_State == ${BST_CHECKED}
            CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" \
                "$INSTDIR\${PRODUCT_INTERNAL_NAME}.exe"
        ${EndIf}
        
        ${If} $Shortcuts_D_Checkbox_State == ${BST_CHECKED}
            CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" \
                "$INSTDIR\${PRODUCT_INTERNAL_NAME}.exe"
        ${EndIf}
SectionEnd

;--------------------------------
; Uninstaller

Section "Uninstall"
    MessageBox MB_OK|MB_ICONEXCLAMATION "$INSTDIR" /SD IDOK
    ; 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
    Delete "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
    Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
    ;TODO remove eventual quicklaunch Too
    
    ; Remove the installation directory used (if empty)
    RMDir "$INSTDIR"
  
    ; and delete the registry key for uninstall
    DeleteRegKey ${HKEY_ROOT} ${UN_KEY}
SectionEnd

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 ;)

LangString l10n_SHORTCUTS_PAGE_TITLE ${LANG_ENGLISH} \
    "Set Up Shortcuts"
LangString l10n_SHORTCUTS_PAGE_SUBTITLE ${LANG_ENGLISH} \
    "Create Program Icons"
LangString l10n_CREATE_ICONS_DESC ${LANG_ENGLISH} \
    "Create icons for ${PRODUCT_NAME}:"
LangString l10n_ICONS_DESKTOP ${LANG_ENGLISH} \
    "On my &Desktop"
LangString l10n_ICONS_STARTMENU ${LANG_ENGLISH} \
    "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:

    |-samples_chapter_3
        |- 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:

#!/bin/bash

# exit the script on errors
set -e

TMP_DIR=./tmp

# 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:

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

2011-06-15 - 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.