Distribute your XULRunner app
3 - Windows
- 3.1 Icons for Windows
- 3.2 Create a launcher (batch or C)
- 3.3 Create a NSIS script
- 3.4 Create the installer
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:
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 :) .
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.