joliclic blog

Aller au contenu | Aller au menu | Aller à la recherche

Tag - dist_xul_app_en

Fil des billets - Fil des commentaires

vendredi, mai 27 2011

Distribute your XULRunner app - 5 - Synthesis - all platforms

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.

5 - Synthesis - all platforms

5.1 Changes to the app

Let summarize the changes we have made to the application itself, for each platform:

  • Linux
    • We have added some png icons into the myapp/chrome/icons/default/ folder.
      This allows to have our icon for our windows and taskbar.
      In details, 16x16px, 32x32px and 48x48px.

      We can keep this change globally, it will have no impact on other platforms.

  • Windows
    • Similarly as for Linux, we have added some .ico icon into the myapp/chrome/icons/default/ folder, for the same reasons.
      And we have built this icon from Linux with the icotool program.

      We can keep this change globally, it will have no impact on other platforms.

  • Mac OSX
    • We have added a main menubar in all our non-modal windows, with some entries with a special id.
      This allows to have our owns entries in the Main Menu of Mac.
      These entries are hidden (in XUL sens) by default.

      We can keep this change globally, it will have no impact on other platforms, because these XUL elements are hidden for other platforms.

    • We have added some JavaScript code to our main js file, to allow quitting properly our app. Because on a Mac the app doesn't quit when we close all windows.

      We can keep this change globally, this code is multi platform. More, we can use it on the other platform as well.

    • We have added a new file, an hidden XUL window. This window is used to populate the Main Mac Menu when all our windows are closed.

      We can keep this change globally, it will have no impact on other platforms.

    • And we have added a new preference (browser.hiddenWindowChromeURL), to use the previous hidden window.

      We can keep this change globally, it will have no impact on other platforms.

So, finally these are small changes, and all of them can be added globally, because they don't disturb the untargeted platforms.

5.2 Added data

For a better integration to each platforms, we have added some specific data:

  • Linux
    • A shell script used as launcher, which relies on XULRunner or Firefox.
      A symbolic link to this launcher into /usr/bin/ is also created by our installers.

    • A .desktop file, for desktop integration, understood by all main Window Manager.

    • And some icons (png and svg), used by the desktop files.

  • Windows
    • A real launcher in C, in fact XAL (XUL App Launcher), which allows to easily launch our app with Firefox, without any black command window, and can be customized with our icon.
      This launcher can be compiled directly from Linux.

    • A batch script as launcher, is possible too, but with some inherent defaults.

  • Mac OSX
    • For this platform, we have in fact encapsulated our app into a special folder structure with some data, an Application Bundle

    • A shell script used as launcher, which uses Firefox (in fact a symbolic link to it, placed in our bundle)

    • A icns icon, used by the bundle for the executable and in the dock.
      And we have built this icon from Linux with the png2icns program.

    • A Info.plist file and a PkgInfo file, describing the bundle.

5.3 Created installers

  • Multi-platform

    No real desktop integration, and with some real limitations on Mac, but a tar.gz works and is simple.

  • Linux

    We have created a deb package for Debian/Ubuntu based distributions, a similar deb for Maemo, and a rpm for Red Hat/Fedora based distributions.

  • Windows

    We have built a real installer/uninstaller, directly from Linux, based on NSIS.

  • Mac OSX

    The easiest distribution method on Mac, is to create a dmg. We have been able to build it from Linux, and with an experimental program, to compress it.

    A simple tar.gz was created too.

5.4 A global and reusable script

In each chapters, we have used some dedicated bash scripts to create all our installers and other data.
I propose now a global script to performs all the tasks in one shot.

More, this script can be used with any other XULRunner application, we only have to adapt a config file.

I don't write this script here, you'll find it in the joined archive of this chapter, but here's the config file:

#!/bin/bash

#_______________________________________________________________________________
#
# script version: 1.0
# date: 2011-05-20
#_______________________________________________________________________________

# exit the script on errors
set -e

#_______________________________________________________________________________
#
# common configuration
#_______________________________________________________________________________

# The name of the application, will be the name of the executable for example.
# It should contain only [a-zA-Z_-.] and no whitespace.
APP_NAME=myapp

# The name of the application displayed on screen.
APP_DISPLAY_NAME=MyApp

# The absolute path to the directory containing this script. Don't modify this
# variable if you don't know what you are doing.
CUR_DIR=$(dirname "$0")
CUR_DIR=$(readlink -f -- "$CUR_DIR")

# The main folder of the sources (absolute path), containing the app, data,...
# Can be defined before, in some other scripts.
if [ ! $MAIN_DIR ]; then
MAIN_DIR=$(readlink -f -- "$CUR_DIR/..")
fi

# The folder where all resulted builds (installers, archives,...) will be copied
DIST_DIR=$MAIN_DIR/dist

# The temporary folder where all build are done
TMP_DIR=$MAIN_DIR/tmp

# Folder which contains extra useful programs for the build script
TOOLS_DIR=$MAIN_DIR/tools

# Folder which contains all build scripts, and the current config file
BUILDERS_DIR=$MAIN_DIR/builders

# The folder of the real sources of the XULRunner application
APP_SRC_DIR=$MAIN_DIR/$APP_NAME

# The folder of the extra data of the app (icons,...)
APP_DATA_DIR=$MAIN_DIR/data

# The folder of the used icons
# This folder must contains:
# icon16.png, icon22.png, icon26.png, icon32.png, icon40.png, icon48.png,
# icon128.png, icon48.svg, icon.ico, icon.icns, icon48.txt
# You can find some shell scripts in the data/icons folder to create the ico,
# icns et base64 icons.
APP_ICON_DIR=$APP_DATA_DIR/icons

# The current version of the app
APP_VERSION=1.0

# You can use a file named version in the main directory too, and uncomment the
# following line to set this variable
#APP_VERSION=$(cat "$MAIN_DIR/version")

# if the app contains some compiled code, and so it's architecture dependent,
# uncomment the following line
#ARCH_DEPENDENT=1


#_______________________________________________________________________________
#
# Linux specific
#_______________________________________________________________________________

# the folder which contains the Linux specifics data
LINUX_DIR=$APP_DATA_DIR/linux

# the desktop file, will be renamed as $APP_NAME.desktop
DESKTOP_FILE=$LINUX_DIR/desktop

# If this variable is set, the desktop file will be adapted, i.e. the strings
# @@APP_VERSION@@, @@APP_NAME@@, @@APP_DISPLAY_NAME@@, and
# @@APP_DESKTOP_CATEGORIE@@,..., will be replaced by their values. Simply
# comment the following line to not use this feature.
OPT_ADAPT_DESKTOP=1

# this value is displayed in a tooltip by the desktop
APP_DESKTOP_COMMENT="a Hello World XULRunner app"

# A value chosen in http://standards.freedesktop.org/menu-spec/latest/apa.html
# If you use multiple value, use a semicolon (;) as separator
# DON'T forget the last semicolon, even if there is only one value
APP_DESKTOP_CATEGORIE='Utility;'

# You can use this variable to put extra lines to the Desktop file, for example
# a localized comment
APP_DESKTOP_EXTRA='Comment[fr]=un Bonjour Monde pour XULRunner.\n'

# the generic Linux launcher, a shell script, for our app. Will be renamed as
# $APP_NAME.sh
LINUX_LAUNCHER=$LINUX_DIR/launcher.sh


#_______________________________________________________________________________
#
# deb package specific
#_______________________________________________________________________________

# Comment the following line if you don't want to generate a deb package
OPT_BUILD_DEB=1

# the debian folder used to build our generic deb
DEBIAN_DIR=$APP_DATA_DIR/debian

# If this variable is set, the files 'changelog', 'control', 'menu',
# 'myapp.link', and 'copyright' of the debian folder will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, @@DEB_CONTROL_VERSION@@, @@DEB_CONTROL_SECTION@@,
# @@DEB_MENU_SECTION@@, ..., will be replaced by their values. Simply comment
# the following line to not use this feature.
OPT_ADAPT_DEBIAN=1

# The version of the generated deb package.
DEB_CONTROL_VERSION=${APP_VERSION}-1

# the category of the app. See http://packages.debian.org/en/sid/
DEB_CONTROL_SECTION=Utilities

DEB_CONTROL_MAINTAINER='John Doe <johndoe@example.com>'

# author of the generated deb
DEB_CONTROL_AUTHOR=$DEB_CONTROL_MAINTAINER

DEB_CONTROL_HOMEPAGE='<http://example.com/myapp/>'

# name of the generated package
DEB_CONTROL_PACKAGE=$APP_NAME

# long description of the app for the generated deb. Can be multiline, see
# http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Description
# for the formatting (in short, add at least a space at the beginning of all
# lines apart the first)
DEB_CONTROL_DESCRIPTION='simple Hello World.
Powered by XULRunner.'

# if the app have other dependencies than Firefox, list them, separated by a
# comma. Important, this list MUST begin by a comma.
DEB_CONTROL_EXTRA_DEPENDENCIES=

# if the app contains some compiled code, and you want to use the
# ${shlibs:Depends} variable with debhelper, uncomment the following line
#DEB_CONTROL_USE_SHLIBS_DEPENDS=1

# the category for the Debian menu, see
# http://www.debian.org/doc/packaging-manuals/menu-policy/ch2.html#s2.1
DEB_MENU_SECTION='Applications/Programming'

DEB_DATE="$(date -R)"

DEB_COPYRIGHT='<Copyright (C) 2010 John Doe>'

# the short license for the generated deb
DEB_SHORT_LICENSE=$(cat "$LINUX_DIR/deb_short_license.txt")


#_______________________________________________________________________________
#
# maemo deb specific
#_______________________________________________________________________________

# Note that the debian specific variables are used as well to generate the
# maemo deb, so they must set.

# the APP_NAME specific for Maemo, can be the same as APP_NAME
MOBILE_APP_NAME=$APP_NAME

# the APP_DISPLAY_NAME specific for Maemo, can be the same as APP_DISPLAY_NAME
MOBILE_APP_DISPLAY_NAME=$APP_DISPLAY_NAME

# the name of the app for the package. Should be different than APP_NAME,
# otherwise there can be collision with the generic deb created by the other
# script
MAEMO_PKG_APPNAME=${APP_NAME}-mobile

# the desktop file for Maemo, will be renamed as $APP_NAME.desktop
MAEMO_DESKTOP_FILE=$LINUX_DIR/maemodesktop

# the launcher for Maemo, a shell script, for our app. Will be renamed as
# $APP_NAME.sh
MAEMO_LAUNCHER=$LINUX_DIR/maemolauncher.sh

# Comment the following line if you don't want to generate a deb package
OPT_BUILD_MAEMO_DEB=1

# the debian folder used to build our deb for Maemo
MAEMODEBIAN_DIR=$APP_DATA_DIR/maemodebian

# the categorie of the app for Maemo. See
# http://wiki.maemo.org/Packaging/Guidelines#Sections
DEB_CONTROL_SECTION_MAEMO=user/utilities

# the name of the generated deb for Maemo.
# Should really the same as MAEMO_PKG_APPNAME, see its comment.
DEB_CONTROL_PACKAGE_MAEMO=$MAEMO_PKG_APPNAME

# the description of the generated deb, especially for Maemo. See the comments
# for DEB_CONTROL_DESCRIPTION in this file for formatting.
DEB_CONTROL_DESCRIPTION_MAEMO=$DEB_CONTROL_DESCRIPTION

# like DEB_CONTROL_EXTRA_DEPENDENCIES, but for Maemo
DEB_CONTROL_EXTRA_DEPENDENCIES_MAEMO=

# the base64 file (text) of the icon for the Maemo deb.
# Can be generated by the script 'build_icon_base64.sh' in the data/icons dir.
MAEMO_BASE64_ICON="$APP_ICON_DIR/icon48.txt"


#_______________________________________________________________________________
#
# rpm package specific
#_______________________________________________________________________________

# Comment the following line if you don't want to generate a rpm package
OPT_BUILD_RPM=1

# path to the spec file used to generate the rpm
RPM_SPEC_FILE=$LINUX_DIR/rpmspec

# If this variable is set, the .spec file will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, @@RPM_VERSION@@, @@RPM_RELEASE@@, @@RPM_GROUP@@,
# @@RPM_LICENSE@@, @@RPM_URL@@, @@RPM_SOURCE@@, @@RPM_DATE@@, ...
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_RPM=1

# version of the app for the spec file: must contain only integer and digit, no
# strings like 'beta'
RPM_VERSION=$APP_VERSION

# the version of the generated rpm, not the app version
RPM_RELEASE='1%{?dist}'

# the short version of the generated rpm, without any string relative to the
# distribution or architecture
#RPM_RELEASE_SHORT=1
RPM_RELEASE_SHORT=$(echo $RPM_RELEASE | sed "s/%.*$//")

# breve summary of the app for the package. One line only.
RPM_SUMMARY='simple Hello World powered by XULRunner.'

# the category of the app. See /usr/share/doc/rpm/GROUPS
RPM_GROUP=Development/Tools

# keyword license
RPM_LICENSE='MPLv1.1 or GPLv2+ or LGPLv2+'

# url where the source of our app can be found
RPM_URL='http://example.com/myapp/'

# the name of the dummy tar.gz source archive, it will be built by this script.
RPM_SOURCE=${APP_NAME}-${RPM_VERSION}.tar.gz

# If the app need some build dependencies, uncomment the following line and add
# them here, separated by a comma. BUT you shouldn't have to, the logic here is
# that the built should have been done before, this script simply package the
# result.
#RPM_BUILDREQUIRES=

# if the app have other dependencies than Firefox, list them, separated by a
# comma. Important, this list MUST begin by a comma.
RPM_EXTRA_REQUIRES=

# long description of the app for the generated rpm. Can be multiline.
RPM_DESCRIPTION='simple Hello World
Powered by XULRunner.'

LC_TIME_BUFFER=$LC_TIME
export LC_TIME="en_EN.utf8"
RPM_DATE=$(date +"%a %b %e %Y")
export LC_TIME="$LC_TIME_BUFFER"

RPM_MAINTAINER='John Doe <johndoe@example.com>'


#_______________________________________________________________________________
#
# Windows specific
#_______________________________________________________________________________

# the folder which contains the Windows specifics data
WIN_DIR=$APP_DATA_DIR/win

# the version of the app used by some other variables. String.
WIN_VERSION=$APP_VERSION

# comment this line if you don't want to build xal, the launcher in C
OPT_BUILD_WIN_XAL=1

# source directory of xal
XAL_SRC_DIR=$TOOLS_DIR/xal-src

# the rc file used to build xal
WIN_RESOURCE=$WIN_DIR/resource.rc

# If this variable is set, the resource (.rc) file for xal will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@WINRES_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_WINRES=1

WINRES_FILEVERSION='0,1,0,0'
# the FILEVERSION for Windows (<int,int,int,int>) will be deducted from the
# APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_WINRES_FVERSION=1

WINRES_PRODUCTVERSION='0,1,0,0'
# the PRODUCTVERSION for Windows (<int,int,int,int>) will be deducted from the
# APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_WINRES_PVERSION=1

WINRES_Comments='Published under the MPL 1.1/GPL 2.0/LGPL 2.1 licenses'
WINRES_CompanyName='John Doe Organization'
WINRES_FileDescription=$APP_DISPLAY_NAME
WINRES_FileVersion=$WIN_VERSION
WINRES_InternalName=$APP_NAME
WINRES_LegalCopyright='(c) 2010 John Doe'
WINRES_ProductName=$APP_DISPLAY_NAME
WINRES_ProductVersion=$WIN_VERSION

# Comment the following line if you don't want to generate the nsis installer
# for Windows
OPT_BUILD_WIN_INSTALLER=1

# the source folder of the nsis scripts used by this script
NSIS_SRC_DIR=$APP_DATA_DIR/nsis

# If this variable is set, the nsis script will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@NSIS_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_NSIS=1

NSIS_PRODUCT_NAME=$APP_DISPLAY_NAME
NSIS_PRODUCT_INTERNAL_NAME=$APP_NAME
NSIS_PRODUCT_VERSION=$WIN_VERSION
NSIS_PRODUCT_WIN_VERSION='1.0.0.0'
# for nsis, the PRODUCTVERSION for Windows (<int,int,int,int>) will be deducted
# from the APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_NSIS_WINVERSION=1

# The license file text MUST be in the main root app folder, so in $APP_SRC_DIR.
# Otherwise, you can edit the nsis script and adapt it
NSIS_LICENCE_NAME='LICENSE.txt'

# the name of the generated nsis installer
NSIS_INSTALLER_NAME="$APP_DISPLAY_NAME-$APP_VERSION-install.exe"


#_______________________________________________________________________________
#
# Mac OSX specific
#_______________________________________________________________________________

# the folder which contains the Mac OSX specifics data
MAC_DIR=$APP_DATA_DIR/mac

# the simple and basic shell launcher for Mac. Used only for the
# multiplatform tar.gz
MAC_BASIC_LAUNCHER=$MAC_DIR/basic_launcher.sh

# the previous basic launcher will be renamed with this value
# Trick: if the extension is 'command', the file can double-clicked in the Mac
# Finder, then a terminal will be opened and the script will be launched.
MAC_BASIC_LAUNCHER_NAME=${APP_NAME}-mac.command

# Comment the following line if you don't want to generate the Application
# Bundle of the app for Mac.
OPT_BUILD_MAC_BUNDLE=1

# path to the used skeleton of the bundle
DMG_SKELET_DIR=$MAC_DIR/bundle_skelet

# If this variable is set, the nsis script will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@MAC_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_MAC_INFO=1

MAC_BUNDLE_EXECUTABLE=${APP_NAME}-mac.sh
MAC_BUNDLE_INFOSTRING="$APP_DISPLAY_NAME $APP_VERSION"
MAC_BUNDLE_ICONFILE=$APP_NAME
MAC_BUNDLE_IDENTIFIER="net.yourcompany.${APP_NAME}"
MAC_BUNDLE_NAME=$APP_DISPLAY_NAME
MAC_BUNDLE_SHORTVERSIONSTRING=$APP_VERSION
MAC_BUNDLE_VERSION=$APP_VERSION

# with this option, the eventual icon 'myapp.icns' in the Contents/Resources
# folder of the used bundle skelet will be removed, and the icns icon of the
# icons folder will be added to the bundle renamed as MAC_BUNDLE_ICONFILE
MAC_BUNDLE_OVERWRITE_ICON=1

# comment the following line if you don't want to generate a dmg image
OPT_BUILD_MAC_DMG=1
# comment the following line if you don't want to customize the dmg image
OPT_CUSTOMIZE_DMG=1
# source folder of the data added to the dmg, to customize it
# Note that you have to recreate manually these customizations files if the
# DMG_VOLNAME is different than 'MyApp', in particular the background image
# will not be displayed. See the Howto.
# Comment the line to disabled this option.
DMG_CUSTOM_DATA_DIR=$MAC_DIR/dmg_extra_data

# the name of the mounted dmg disk image
DMG_VOLNAME="$APP_DISPLAY_NAME"
# the name of the generated dmg
DMG_NAME="$APP_NAME-$APP_VERSION.dmg"

# comment the following line if you don't want to compress the dmg. You need
# the <dmg> program to be able to do that
OPT_COMPRESS_DMG=1
# path to the dmg program, used to compress the dmg.
# see http://github.com/planetbeing/libdmg-hfsplus
# and http://shanemcc.co.uk/libdmg/
TOOL_DMG="$TOOLS_DIR/dmg"


#_______________________________________________________________________________
#
# Multi-platform tar.gz archive (usable on Linux, Windows, Mac OSX)
#_______________________________________________________________________________

# Note that al lot of previous variables (Linux, Windows, and Mac OSX specific)
# will be used to build this archive

# Comment the following line if you don't want to generate multi-platform tar.gz
# Note that if ARCH_DEPENDENT is active, this archive will NOT be built
OPT_BUILD_MULTI_TARGZ=1

# the name of the generated tar.gz
TARGZ_NAME=${APP_NAME}-${APP_VERSION}-multiplatform


#_______________________________________________________________________________

BUILD_CONFIG_INCLUDED=1

To use this script for your own application, in short you have to overwrite all data included in the data folder, image, files... then open and adapt the build_config.sh script to your own.

In detail:

  • replace all icons in the data/icons folder by your owns.
    If you don't have the .ico and .icns ones, use the scripts build_ico.sh and build_icns.sh to build them. And use the build_base64.sh script to generate the base64 icon for the Maemo package.

  • Edit and modify the file deb_short_license.txt in the data/linux folder.

  • If you want to create a dmg for Mac OSX and customize it, you should recreate the used data, in particular the .DS_STORE file, and overwrite them in the data/mac/dmg_extra_data folder. Be careful, there're some hidden files in this folder on Linux, because their names begin with a dot (.) (CTRL+H in Nautilus to show them ;).

  • Be sure to have a license file into the main folder of your app, this is required to build the Windows installer, and this is really a good practice.

  • All other data could be used as is, because their contents will parsed by the scripts and use the config file, but you can edit and change them as well.

  • If you want to create a compressed dmg for Mac OSX, you have to download the libdmg program from the author page or this modified version, and place it in the tools folder.

  • Finally, edit and adapt the config.sh file in the builders folder.
    The most important parameters are APP_NAME and APP_DISPLAY_NAME, a lot of others parameters use them.
    And of course the path to the sources of your app, it could be a good idea to place them at the same level of the myapp example, but that's not required.
    Replace of course all strings specifics to the myapp example.

Then, in a terminal:

  • cd myapp-src-global
  • sh ./build_all.sh

And you will find into the myapp-src-all/dist/ folder all the created installers.

5.5 Conclusions

Creating XULRunner applications is easy, powerful, and multiplatform, like developing Firefox extensions.

You can use some wizards from mozdev.org to generate a skeleton to begin a new app (be careful, you will have to adapt the chrome registration for Firefox 4, the wizard need apparently to be updated for this part).

We have seen in this how-to some small parts that we need to taking care for a better desktop integration, and that it is relatively easy to create installers, from Linux, for several platforms. You can even reuse the global script, or get inspiration from it, to create these installers for your own application.

Now it's up to you to make some nice apps, I hope this how-to and the global script will help you.
Have fun ;) !

Nicolas Martin

You can download all the samples of this chapter 5, the global script and XAL included, in the myapp-src-global.tar.gz archive.

Or retrieve this entire how-to in its dedicated page, downloadable with all the examples.

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 5, are under Public Domains too.

jeudi, mai 26 2011

Distribute your XULRunner app - 4 - Mac OSX

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.

4 - Mac OSX

4.1 Icons for Mac OSX

We need a icns icon. On Mac the windows doesn't have a icon, but, later, we will use one in the Dock. And it will be used for the bundle that we will create.

We can generate this .icns file directly from Linux. The restriction is that we cannot embed mutiple color depth images, but multiple sizes is possible.
We need the program png2icns from the icnsutils package.
On Debian/Ubuntu:

apt_get install icnsutils

We obtain our icns file from several png with:

png2icns icon.icns icon128.png icon48.png icon32.png icon16.png

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

4.2 Create a launcher

Like for Linux, we will use a shell script for our launcher. But we have several problems to resolve.

The first one is not too difficult. The readlink program available on Mac OSX is not the GNU one, and we can't use the -f option. We will resolve ourself this functionality.

A more serious problem, is that we can't determine where Firefox is installed. So, here, we will impose a limitation, Firefox must be installed in its default location, i.e. /Applications/ .

Note: if you have any suggestions to determine where is installed Firefox, please let me know ;) .

Here this script, myapp-mac.sh, but that's not exactly this one we will use later:

#!/bin/sh

set -e

# see http://stackoverflow.com/questions/7665/how-to-resolve-symbolic-links-in-a-shell-script
# get the absolute path of the executable
SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && \
pwd -P) && SELF_PATH=$SELF_PATH/$(basename -- "$0")

# resolve symlinks
while [ -h $SELF_PATH ]; do
DIR=$(dirname -- "$SELF_PATH")
SYM=$(readlink $SELF_PATH)
SELF_PATH=$(cd $DIR && cd $(dirname -- "$SYM") && pwd)/$(basename -- "$SYM")
done

CUR_DIR=$(dirname "$SELF_PATH")

if [ -x /Applications/Firefox.app/Contents/MacOS/firefox-bin ]; then
/Applications/Firefox.app/Contents/MacOS/firefox-bin -app "$CUR_DIR/application.ini" $@
elif [ -x /Library/Frameworks/XUL.framework/xulrunner-bin ]; then
/Library/Frameworks/XUL.framework/xulrunner-bin "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Firefox or XULRunner!"
fi

If we try this launcher, we can see 2 other problems, the icon shown in the dock is the Firefox one, and the main menu, when we close all our app windows, is the Firefox one.
There is a solution to these problems, as we will see in the next parts.

Note that if you really want to use a such script, without a bundle, you may want use a .command extension rather than .sh, because then the file can be double-clicked in the Mac Finder, this will open a terminal then the app.

4.3 Some Mac specificities

There is a particularity on Mac, there is a main Menu controlled by the operating system, corresponding to the current focused application. And when all windows of a same application are closed, this menu still appeared, as long as the application quit.

This is well handled by a XULRunner application, but we must add some code for that.

Some documentation on MDN:

If your app uses a main menubar element, XULRunner/Firefox will use it (in fact the first menubar) to create this special menu. If you don't have one, you should create it, hidden by default for the other platforms, this will allow the menu construction anyway.

The entries in the apple menu are constructed from elements with reserved id. You should at least create some entries, to allow to quit the app for example.

Here's the list of available id corresponding to these entries (sources):

element id corresponding menu item
aboutName About This App
menu_preferences Preferences...
menu_mac_services Services
menu_mac_hide_app Hide App
menu_mac_hide_others Hide Others
menu_mac_show_all Show All
menu_FileQuitItem Quit

And here's an example of a minimal xul menu for this usage:

  <commandset id="main-commands">
<command id="cmd:quit" oncommand="myappQuitApplication();"/>
</commandset>

<keyset id="ui-keys">
<key id="key:quitApp" key="Q" modifiers="accel" command="cmd:quit"/>
<key id="key:hideApp" key="H" modifiers="accel"/>
<key id="key:hideOthersApp" key="H" modifiers="accel,alt"/>
</keyset>

<menubar id="main-menubar" hidden="true">
<menu id="mac-menu">
<menupopup>
<menuitem id="menu_mac_hide_app" label="Hide My App" key="key:hideApp"/>
<menuitem id="menu_mac_hide_others" label="Hide Others" key="key:hideOthersApp"/>
<menuitem id="menu_mac_show_all" label="Show All"/>
<menuitem id="menu_FileQuitItem" label="Quit" key="key:quitApp" command="cmd:quit"/>
</menupopup>
</menu>
</menubar>

Of course, like the other part of the app, the strings used in the labels and keys should be localized. But that's not the purpose of this howto.

The myappQuitApplication function has been added in the main.js file of the our app:

function myappQuitApplication() {
const Cc = Components.classes;
const Ci = Components.interfaces;

let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eAttemptQuit);

return true;
}

Finally, the last problem: when all windows of our app are closed, in fact the app doesn't quit, and the main menu remains. More, as is, this is the Firefox menu that we see.
In fact, there is a special hidden window that XULRunner uses for this menu. So we have to create a such window, and to set a preference to specify it.

Here's the added preference in defaults/preferences/pref.js:

pref("browser.hiddenWindowChromeURL", "chrome://myapp/content/hiddenWindow.xul");

And the content of hiddenWindow.xul, which contains only our main menubar:

<window id="hiddenWindow"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript">
<![CDATA[
function myappQuitApplication() {
const Cc = Components.classes;
const Ci = Components.interfaces;

let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eAttemptQuit);

return true;
}
]]>
</script>

<commandset id="main-commands">
<command id="cmd:quit" oncommand="myappQuitApplication();"/>
</commandset>

<keyset id="ui-keys">
<key id="key:quitApp" key="Q" modifiers="accel" command="cmd:quit"/>
<key id="key:hideApp" key="H" modifiers="accel"/>
<key id="key:hideOthersApp" key="H" modifiers="accel,alt"/>
</keyset>

<menubar id="main-menubar" hidden="true">
<menu id="mac-menu">
<menupopup>
<menuitem id="menu_mac_hide_app" label="Hide My App" key="key:hideApp"/>
<menuitem id="menu_mac_hide_others" label="Hide Others" key="key:hideOthersApp"/>
<menuitem id="menu_mac_show_all" label="Show All"/>
<menuitem id="menu_FileQuitItem" label="Quit" key="key:quitApp" command="cmd:quit"/>
</menupopup>
</menu>
</menubar>
</window>

4.4 Create a Application Bundle

On Mac OSX, applications are packaged in a special format named Application Bundle. This is in fact a folder, named with .app as extension, and with a special structure.

Some useful documentation:

Here's a proposal .app folder for our app:

    |- MyApp.app/
|- Contents/
|- MacOS/
|- chrome/
|- defaults/
|- application.ini
|- chrome.manifest
|- foxstub
|- myapp-mac.sh
|- Resources/
|- myapp.icns
|- Info.plist
|- PkgInfo

The MacOS folder contains in fact all the files and folders of our XULRunner app.

The Resources folder contains only our icon in icns format.

The PkgInfo file contains only the string APPL????. I'm not sure this file is really necessary, it's seems it's important for compatibilty with Mac OS 9.
The value specifies that this bundle is an application, and as we don't have a valid 4 characters identifier, we use 4 question marks (????).

The Info.plist file describe our app:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>myapp-mac.sh</string>
<key>CFBundleGetInfoString</key>
<string>MyApp 1.0</string>
<key>CFBundleIconFile</key>
<string>myapp</string>
<key>CFBundleIdentifier</key>
<string>net.yourcompany.myapp</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>MyApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0.0.0</string>
<key>NSAppleScriptEnabled</key>
<true/>
</dict>
</plist>

Some comment on these properties:

The CFBundleExecutable point to our launcher, the shell script named myapp-mac.sh.

The value of CFBundleIconFile is myapp because our icon in the Resources folder is named myapp.icns. Now, the end user sees our bundle with our icon.

The CFBundleIdentifier is formatted as an inversed domain name dot application name. It must be a unique identifier for our app.

The CFBundlePackageType specifies that this is an application bundle (APPL).

The value of CFBundleSignature is ???? because we don't have a signature. This is supposed to be an unique 4 characters identifier, and supposed to be obtained by an Apple registration. As I understand this is required essentially for Mac OS 9. Don't worry, I have not seen any problem with our value ;) .

Now let's see a nice trick. As is, the end user sees our app with our icon, but when he launches it, the icon in the Dock is the Firefox one.
In the tree folder of our bundle, you can see a file named foxstub, this file is in fact a symbolic link to the Firefox executable.

This link is obtained simply, in the MacOS folder, with:

ln -s /Applications/Firefox.app/Contents/MacOS/firefox-bin foxstub

Now when the user launches our app, this is our icon that's displayed in the dock :) .

In fact, our icon appears in the dock, then disappears, and appears again, because our script is launched, then the linked executable. But that's not a big problem.

Here's the modified launcher script myapp-mac.sh, which now uses the symbolic link to firefox:

#!/bin/sh

set -e

# see http://stackoverflow.com/questions/7665/how-to-resolve-symbolic-links-in-a-shell-script
# get the absolute path of the executable
SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && \
pwd -P) && SELF_PATH=$SELF_PATH/$(basename -- "$0")

# resolve symlinks
while [ -h $SELF_PATH ]; do
DIR=$(dirname -- "$SELF_PATH")
SYM=$(readlink $SELF_PATH)
SELF_PATH=$(cd $DIR && cd $(dirname -- "$SYM") && pwd)/$(basename -- "$SYM")
done

CUR_DIR=$(dirname "$SELF_PATH")

if [ -x "$CUR_DIR/foxstub" ]; then
"$CUR_DIR/foxstub" -app "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Firefox or XULRunner!"
fi

We will generate our application bundle with a bash script. It's very simple, we have just to copy the content of our app into the MacOS folder. Here's the content of build_mac.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"

# create the .app folder
mkdir -v "$TMP_DIR/MyApp.app"

# copy our bundle skeleton
# (our launcher myapp-mac.sh and our symbolic link foxstub are included)
cp -rv data/bundle_skelet/. "$TMP_DIR/MyApp.app/"

# copy our app
cp -rv myapp/* "$TMP_DIR/MyApp.app/Contents/MacOS/"

# apply the correct permissions
chmod -R 755 "$TMP_DIR/MyApp.app"

# and copy the result in our main src folder
rm -frv MyApp.app
cp -Rv "$TMP_DIR/MyApp.app" ./

This script will be continued in the next parts. But for the moment, as result, we have created the Application Bundle MyApp.app into our main source folder.

4.5 Package as tar.gz and dmg

To distribute our bundle, we need to create an archive. I propose here 2 solutions, each with advantages and disadvantages.

Create a tar.gz

The first and the simplest, is to create a tar.gz. This format is well handled by Mac OSX. The only problem, is that the final user can find it not really friendly. Here what's he have to do:

  1. download the myapp.tar.gz file.
  2. open it. In fact the default behavior is to uncompress the archive into the same folder.
  3. go into the uncompressed folder
  4. move by drag and drop the bundle where he wants, for example in /Applications/

The problem is phase 2, some users doesn't understand that the archive is uncompressed, and where.

To create this tar.gz, we add the following lines in our build_mac.sh script:

# create a sub-directory for the tar.gz creation
mkdir "$TMP_DIR/myapp-1.0"

# copy our bundle
# (It would be possible to add other files, the license for example)
cp -rv "$TMP_DIR/MyApp.app" "$TMP_DIR/myapp-1.0/"

rm -fv myapp-mac-1.0.tar.gz

cd "$TMP_DIR"
tar -zcvf "../myapp-mac-1.0.tar.gz" myapp-1.0
cd -
Create a dmg

The second solution is to create a dmg archive. In fact this is a disk image format.
This is a usual solution on this platform, and used by Firefox for example.

We can create a dmg from Linux, but with a limitation: we can't generate a compressed dmg. Well, in fact there's a experimental way to do it, that I will propose to you just after, but it's still experimental.
The most useful article that I have found about this subject is this post from the author of DMDirc. We will follow the same methods.

The easiest way on linux is to use mkisofs, or genisoimage. genisoimage is a fork of the former, used in some distributions, and as a symlink named mkisofs is installed as well on these distributions, I will use the command mkisofs in the following script.
To install it on Debian/Ubuntu:

apt-get install genisoimage

We add, in our build script build_mac.sh:

# create the dmg
rm -fv myapp-1.0.dmg
mkisofs -V 'MyApp' -no-pad -r -apple -o "myapp-1.0.dmg" "$TMP_DIR/myapp-1.0"

With this line, we create the file myapp-1.0.dmg, which contains all files and folders included in the $TMP_DIR/myapp-1.0 folder, with the volume name 'MyApp' (the title of the window when the dmg is mounted on Mac). And all files and folders in the dmg have the 755 rights.

Compress this dmg

The only solution found to perform this task on Linux, mentioned in this post, is to use the dmg program from the libdmg-hfsplus project.
The author clearly says:

THE CODE HEREIN SHOULD BE CONSIDERED HIGHLY EXPERIMENTAL

Note that the last commit of the project that I have tried doesn't compile (somebody have reported the bug).
And the author of DMDirc report another bug in a previous version. But I have tried its slightly modified version, with success!
So, you should give it a try.

Here the added lines to our build script:

# try to compress our dmg, if the the command <dmg> is available
# see http://github.com/planetbeing/libdmg-hfsplus
# and http://shanemcc.co.uk/libdmg/
if [ -x "./tools/dmg" ]; then
# rename temporarily our dmg
mv myapp-1.0.dmg myapp-1.0.dmg.pre
# compress
"./tools/dmg" dmg myapp-1.0.dmg.pre myapp-1.0.dmg
rm -fv myapp-1.0.dmg.pre
fi

Note that I don't include the dmg program in the joined archive, you have to download it yourself from the previous links, and place it into the tools folder.

Create a dmg from Mac OSX

For information, to create a dmg directly on Mac OSX, there's the program hdiutil. To create an auto open, compressed, dmg, from a folder named "myFolder", something like this should work:

#!/bin/bash

hdiutil create -volname "myapp-1.0" -fs HFS+ -srcfolder "myFolder" -format UDRW myapp-temp.dmg

hdiutil convert myapp-temp.dmg -format UDZO -imagekey zlib-level=9 -o myapp.dmg

rm -fv myapp-temp.dmg
Customize the dmg

It's possible to customize our dmg when it is mounted, i.e. the displayed window, for example: icon size, background image, window size...

The idea is to create a read/write dmg on a mac, customize it manually, and then make a copy of the file .DS_STORE and all the data. Later we will use these data when we will create our dmg by script.
This part can only be done directly and manually on a Mac.

create a read/write disk image

On a Mac, open "Disk Utility" from the "Utilities" folder.
Click on "New Image".
Choose the same name as the future volume name of our dmg (MyApp).
Don't use a journaled format, to be able to select a "small" size for this dmg. The size doesn't matter, it have to be big enough to copy our app and other data.
As Image Format, choose read/write disk image. Then click on "create".

Add our data on the disk image

Now mount this dmg by clicking on it. And open it.
Copy our app in the open window. We can copy the symbolic link to /Applications too, if it exists. Otherwise we will create it in a terminal later.

Open a Terminal from the Utilities.
Go into the mounted disk image:
cd /Volumes/MyApp
then create a folder named .background:
mkdir .background
because the name begin by a dot (.), this folder is hidden, like on Linux.
copy in this folder our background image:
cp pathToOurImage/background.png /Volumes/MyApp/.background/
And, if needed, create the symbolic link to /Applications:
ln -s /Applications /Volumes/MyApp/Applications

Customize the disk image

Now give the focus to the window of the mounted disk, and open "Show View Options" from the View menu.
Select picture for background, then click on the button.
We have to select the background.png file from the mounted disk, but because this png is in a hidden folder we can't see it. Don't worry, type Apple+Shift+G:
then type /Volumes/MyApp/.background/, then enter.
now select our png.
Choose the icon size, for example 128, arrange them by none, then drag them where you want, and adapt the window size to our background.

Now close this window, and eject the mounted disk. Then mount it again, and open it.

Copy the resulting customization

In a terminal, copy the created file .DS_STORE at the root of the mounted disk:
cp /Volumes/MyApp/.DS_STORE pathToSave/myDS_STORE

Now we have finally all needed files. When we create our dmg from our script, we just have to copy the file .DS_STORE and the folder .background with the file background.png. The resulting dmg will have the same customization.
Copy these data into our sources:

    |- samples_chapter_4/
|- data/
|- bundle_skelet/
|- dmg_extra_data/
|- .background/
|- background.png
|- Applications
|- .DS_STORE

And we add the following lines in our build_mac.sh script, before creating the dmg:

# copy eventual extra data, by example a symbolic link to /Applications
if [ $(ls -A "data/dmg_extra_data" | wc -c) -ne 0 ]; then
cp -Rv data/dmg_extra_data/. "$TMP_DIR/myapp-1.0/"
fi

Note that this customization is dependent of the used volume name, especially the background image, because the .DS_STORE file uses absolute paths to the mounted disk, and the volume name is part of it.
So, if you change the volume name, you have to recreate manually the customization...

Launch our build script

To launch our script (on Linux), and create our dmg and tar.gz, in a terminal:

  • cd samples_chapter_4
  • sh ./build_mac.sh

And as result, we have finally the folder MyApp.app, and the files myapp-1.0.dmg and myapp-1.0.tar.gz in our main source folder.

Nicolas Martin

You can download all the samples of this chapter 4 (Mac OSX) in the samples_chapter_4.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.

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

mercredi, mai 25 2011

Distribute your XULRunner app - 3 - Windows

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:

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 :) .

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.

mardi, mai 24 2011

Distribute your XULRunner app - 2.4 - Package as rpm

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.

2.4 Package as rpm

2.4.1 Prerequisites

In this part we will create a rpm package, for rpm based distributions, like Red Hat, Fedora, Suse,... (I have tested only with Fedora, please let me know if there's any problems for others).

We can create this rpm from other Linux distributions as well, Debian based for example (that's what I do), we just need the rpmbuild executable, we have just to install rpm:

apt-get install rpm

Some documentation links:

I suppose we don't have any needed compilation, but if you have some, simply do it before packaging (despite the fact this is not recommended in this guide).

2.4.2 RPM files

To build a rpm, we're supposed to setup a user configuration for all rpm creation. But it is possible to do it in a local folder, that's what we will do.

We need to create the following folders, and a myapp.spec file:

    |- samples_chapter_2/
|- myapp/
|- rpmbuild/
|- BUILD/
|- RPMS/
|- SOURCES/
|- SPECS/
|- SRPMS/
|- myapp.spec

In fact this is the same structure than the required rpm environnement, but locally.

The .spec file is the 'script' used by rpmbuild to create the rpm. It is supposed to use a source tar.gz archive, that we will build especially later.

Here's the content of our spec:

Name:           myapp
Version: 1.0
Release: 1%{?dist}
Summary: simple Hello World powered by XULRunner.

Group: Development/Tools
License: MPLv1.1 or GPLv2+ or LGPLv2+
URL: http://example.com/myapp/
Source: myapp-1.0.tar.gz
BuildArch: noarch
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id} -un)

#BuildRequires:
Requires: firefox >= 3.6

%description
simple Hello World
Powered by XULRunner.

%prep
%setup

%build

%install
rm -rf %{buildroot}
mkdir -p %{buildroot}%{_libdir}
cp -R myapp %{buildroot}%{_libdir}/
mkdir -p %{buildroot}%{_bindir}

ln -s %{_libdir}/myapp/myapp.sh %{buildroot}%{_bindir}/myapp

mkdir -p %{buildroot}%{_datadir}/applications
cp data/myapp.desktop %{buildroot}%{_datadir}/applications/

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/16x16/apps
cp data/icons/icon16.png \
%{buildroot}%{_datadir}/icons/hicolor/16x16/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/22x22/apps
cp data/icons/icon22.png \
%{buildroot}%{_datadir}/icons/hicolor/22x22/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/32x32/apps
cp data/icons/icon32.png \
%{buildroot}%{_datadir}/icons/hicolor/32x32/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/48x48/apps
cp data/icons/icon48.png \
%{buildroot}%{_datadir}/icons/hicolor/48x48/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
cp data/icons/icon48.svg \
%{buildroot}%{_datadir}/icons/hicolor/scalable/apps/myapp.svg

%post
touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :
if [ -x %{_bindir}/gtk-update-icon-cache ]; then
%{_bindir}/gtk-update-icon-cache -q %{_datadir}/icons/hicolor;
fi

%postun
touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :
if [ -x %{_bindir}/gtk-update-icon-cache ]; then
%{_bindir}/gtk-update-icon-cache -q %{_datadir}/icons/hicolor;
fi


%clean
rm -rf $RPM_BUILD_ROOT


%files
%defattr(-,root,root,-)
%{_bindir}/myapp
%{_libdir}/myapp
%{_datadir}/applications/myapp.desktop
%{_datadir}/icons/hicolor/16x16/apps/myapp.png
%{_datadir}/icons/hicolor/22x22/apps/myapp.png
%{_datadir}/icons/hicolor/32x32/apps/myapp.png
%{_datadir}/icons/hicolor/48x48/apps/myapp.png
%{_datadir}/icons/hicolor/scalable/apps/myapp.svg
#%doc

%changelog
* Fri Oct 8 2010 John Doe <johndoe@example.com> 1.0-1
- first publication

Let see in detail some entries of this file:

The Name should not contain any whitespace characters.

The Version must contain only number and digit, no string like "beta".

The Group entry must be chosen in the list in the /usr/share/doc/rpm/GROUPS file. You should have it on your computer since you have installed rpm.

The Release is the version of the rpm for this version of the app, and %{?dist} will be replaced by rpmbuild by the current distribution. It will not be completed on a non rpm distribution, you can remove it if you want.

For the License entry, you must choose a short keyword from this list.

The Source entry must match exactly the tar.gz that we will create later.

The BuildArch: noarch line specify that our app is not architecture specific. If your app contains some compiled code, you must remove this line, and a rpm specific to the current architecture will be build.

The BuildRoot value is a usual one.

The BuildRequires is commented (with a #), as we have no compilations.

Finally, the Requires entry contains the other packages that are needed by our app. You could add others, separated by comma (,). Note that you can't declare alternative dependencies like with deb. That's why we declare Firefox and not XULRunner as dependencies, in the eventual case where the Firefox of the distribution is not XULRunner based.

Later we will copy the tar.gz archive of our app into the SOURCES folder.
Then the %prep section and the %setup instruction will unpack it into the BUILD folder.
The %build section is empty, because we have no compilation to do.

In the %install section, we install our app.
These are some shell commands, the current working directory is the unpacked source tar.gz. %{buildroot} is the root of the local installation of our app, this will be the computer root / in the real installation.
%{_libdir}, %{_bindir}, and %{_datadir} corresponds respectively to /usr/lib/, /usr/bin/, and /usr/share/. I guess that's the case for main rpm based distros, If I'm wrong, please correct me, it means that some these variables should be redefined.
So in this section, we re-create the initial buildroot folder. Then we create the link to our launcher in /usr/bin/, copy our app in /usr/lib/, and copy the desktop file and our icons into the right location in /usr/share/.

The %post and %postun sections declare some actions after that our app will be installed and uninstalled. We update the icons database for the windows manager.
If the app handle some mime types, i.e. can open some type of files, this should be specified in the .desktop file. And you must add the following code in these %post and %postun sections:

if [ -x /usr/bin/update-desktop-database ]; then
update-desktop-database &> /dev/null || :
fi

The %files section is required, and contains the complete list of files/folders installed by our app.

Finally, the format of the %changelog section, is described in this doc.

2.4.3 Create the rpm

So now that we have all that we need, we will create a script to create a fake source archive for rpmbuild, which contains our app, our desktop file, and our icons. We will place this archive in the SOURCES folder, then invoke rpmbuild locally with our spec file. And in fact, we will doing all of this in a temporary folder, to let our sources clean.

Note: rmpbuild seems to bug if there's some whitespace in the path of the working folder, so be careful with the name of the (sub)directories where you place your sources.
The following script will abort if this is the case.

Here this script:

#!/bin/bash

# exit the script on errors
set -e

TMP_DIR=./tmp
SRC_VERSION=1.0

# 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 the rmpbuild tree folder
cp -rv rpmbuild/* "$TMP_DIR/"

# create a source tar.gz archive for rpmbuild into SOURCES
mkdir -v "$TMP_DIR/myapp-$SRC_VERSION"
cp -rv myapp "$TMP_DIR/myapp-$SRC_VERSION/"
cp -rv data "$TMP_DIR/myapp-$SRC_VERSION/"
cd "$TMP_DIR"
# clean eventual tempory files
find . -type f -name *.*~ -exec rm {} \;
tar -zcvf "$TMP_DIR/SOURCES/myapp-$SRC_VERSION.tar.gz" \
"myapp-$SRC_VERSION"
cd -
rm -rv "$TMP_DIR/myapp-$SRC_VERSION"

# rmpbuild seems to have a bug, it can't handle a _topdir with whitespace
if [ -n "$(echo "$TMP_DIR" | grep '[ ]')" ]; then
echo "the path '$TMP_DIR' contains whitespace, rpmbuild will failed. script aborted!"
exit 1
fi

# build the rpm from our spec, specify our local build tree
rpmbuild --define="_topdir $TMP_DIR" -bb "$TMP_DIR/myapp.spec"

# and finally copy our created rpm in to into the current folder
cp -v "$TMP_DIR/RPMS/noarch/myapp-$SRC_VERSION-1.noarch.rpm" ./

To create our rpm, in a terminal:

  • cd samples_chapter_2
  • sh ./build_rpm.sh

And as result we have finally the file myapp-1.0-1.noarch.rpm in the myapp-src folder :) .

Nicolas Martin

You can download all the samples of this chapter 2 (Linux) in the samples_chapter_2.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.

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

lundi, mai 23 2011

Distribute your XULRunner app - 2.3 - Special case, deb for Maemo

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.

2.3 Special case : deb for Maemo

2.3.1 Maemo generalities

Maemo is a Linux, Debian based, distribution, for Mobile. And Firefox Mobile, aka Fennec, is available on this platform.
Fennec, like the desktop version of Firefox, is able to launch a XULRunner application via the -app argument.

So, a priori, we can launch any XULRunner applications. Of course, we should care of the specificities of mobile devices, small screen (but with high resolution), and touch screen. That's not the purpose of this howto. But as is, our app should work.

Because this platform is Debian based, we will distribute our app with a deb. We have just to adapt some parts of the previous chapter to the specificities of Maemo.

We can find some information in the Maemo Wiki.

2.3.2 Adapt the launcher

The change is that we invoke Fennec rather than Firefox.
The new launcher, myapp.sh:

#!/bin/sh

CUR_DIR=$(dirname $(readlink -f "$0"))

if [ -x /usr/bin/fennec ]; then
/usr/bin/fennec -app "$CUR_DIR/application.ini" $@
elif [ -x /usr/bin/xulrunner ]; then
/usr/bin/xulrunner "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Fennec or XULRunner!"
fi

2.3.3 A new file system organization - Where place our app

On Maemo, due to the memory space on mobile devices, the applications should reside in the folder /opt/ rather than in /usr/lib/ or /usr/share/.

So, we will copy our app in /opt/myapp/.
A sub-directory would possible too, /opt/myorganization/myapp/ for example.

For the icons, we will use different dimensions than for the desktop, like Fennec does itself: a 26x26px png, a 40x40px png, and a 48x48 svg.
Their final locations change a little too, they must be placed in:

  • /usr/share/icons/hicolor/26x26/hildon/myapp.png
  • /usr/share/icons/hicolor/40x40/hildon/myapp.png
  • /usr/share/icons/hicolor/scalable/hildon/myapp.svg

The location of the .desktop file change too, it must be placed into /usr/share/applications/hildon/ .

The .desktop file and the images are not so heavy, we can put them as is, but it should be possible to place them in our main app folder and use symbolic links too.

2.3.4 Additions to the .desktop file

Some information can be found in this page of the Maemo Wiki.

Some additional keys can be used in the desktop file. Not all that I add are documentated, but they seems to be used by a lot of programs, perhaps for some older version of Maemo.
Here's some lines added to our desktop file:

X-Icon-Path=/usr/share/icons
X-Window-Icon=myapp
X-Window-Icon-dimmed=myapp
X-Osso-Type=application/x-executable

2.3.5 Changes to the Debian files

To avoid to overwrite the generic deb created in the previous chapter, we will rename this new deb package for Maemo. We will use 'myapp-mobile'. To be consistent with the debhelper process, here the list of all this concerned changes:

  • In the changelog file, the line myapp (1.0-1) unstable; urgency=low becomes myapp-mobile (1.0-1) unstable; urgency=low

  • In the control file, the value for the Source and Package fields is changed to myapp-mobile

  • The file myapp.links is renamed as myapp-mobile.links

Other changes to the control file:

According to this documentation, we must choose a Section in this list:

  • user/desktop
  • user/development
  • user/education
  • user/games
  • user/graphics
  • user/multimedia
  • user/navigation
  • user/network
  • user/office
  • user/science
  • user/system
  • user/utilities

We can add extra data to the control file, used to build the deb.
The first is XB-Maemo-Display-Name: MyApp .

The second is a base64 icon, used by the application manager of Maemo, XB-Maemo-Icon-26.

We will use a bash script to create this base64 value from the 48px png icon.

First we need the uuencode program, on debian/ubuntu:

apt-get install sharutils

then, we create a Bash script named build_base64.sh to convert a 48x48px png into the desired text value:

#!/bin/bash

set -e

# base64 conversion
uuencode -m icon48.png icon48.png > icon48.txt

# remove the first line (begin-base64 ...)
sed -i '1d' icon48.txt

# and the last line (====)
sed -i '$d' icon48.txt

# add 4 spaces at the beginning of each lines
sed -i "s/^/ /" icon48.txt

# and add a blank line at the end of the file
echo '' >> icon48.txt

You can find this script into the data/icons folder of the samples_chapter_2.tar.gz archive. To launch it (into the icons folder):

sh build_base64.sh

We can now use the content of icon48.txt as the value of XB-Maemo-Icon-26 in our control file.

2.3.6 Build the deb

And, that's all, we can create our deb like in the previous chapter.

I don't write here the build_maemodeb.sh script, it's almost the same as the one to build the generic debian package. You'll find it in the joined archive.

  • cd samples_chapter_2
  • sh ./build_maemodeb.sh

Note: I suppose in this howto that we don't have compiled code in our app.
If this is not the case, we must use scratchbox to compile, and we must use scratchbox to build the deb too!
And, like in the previous chapter, don't use the line Architecture: all in the control file.

Nicolas Martin

You can download all the samples of this chapter 2 (Linux) in the samples_chapter_2.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.

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

dimanche, mai 22 2011

Distribute your XULRunner app - 2.2 - Package as deb

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.

2.2 Package as deb

2.2.1 Synopsis

Ok, so we have our launcher, and all the files needed for the desktop integration (see chapter 2.1). We will now create a deb package, for Debian based distributions, like Ubuntu.

The main documentation for deb creation is the Debian Policy Manual. And you can find useful too this other doc from Debian and this one from Ubuntu.

I suppose that our app doesn't contain any compiled code, but if you have some, simply compile it before packaging. We will create this deb manually.

We will use debhelper and fakeroot. We have to create some files used by the debian scripts, copy our sources and our desktop files in the right location, then run some scripts.

For that, we need to install debhelper and fakeroot.

on Debian/Ubuntu:

  • apt-get install debhelper fakeroot

On Fedora, unfortunatly debhelper is not yet available in the default repository, but it should be one day, see the bugs #591190, #591389, #642160, and #591332.

On OpenSuse there's a package named 'deb' from openSuse which install it.

On others... let me know ;)

The created deb will install myapp in the /usr/lib/ folder, this is the standard location on Linux.

2.2.2 Debian files

To build our deb, we need to create a debian folder alongside our application, with several files into it used by the debhelper programs.
You can use dh_make to create a such skeleton, but here we will create these files by hand:

    |- samples_chapter_2/
|- myapp/
|- debian/
|- changelog
|- compat
|- control
|- copyright
|- menu
|- postinst
|- myapp.links

The changelog file is described in detail here in the doc, it will contain:

myapp (1.0-1) unstable; urgency=low

* Initial release

-- John Doe <johndoe@example.com> Fri, 26 Nov 2010 10:10:35 +0100

Note: be precise on the formatting of this file, it must be really precise, see the doc.

Note: the date must be in RFC822 format, you can obtain it with date -R in a terminal.

The compat file contains simply the debhelper compatibility, so just 7 .

The control file is the more important one. Here's a sample:

Source: myapp
Section: Utilities
Priority: extra
Maintainer: John Doe <johndoe@example.com>
Build-Depends: debhelper (>= 7)
Standards-Version: 3.7.2
Homepage: <http://example.com/myapp/>

Package: myapp
Architecture: all
Depends: xulrunner (>= 1.9.2) | firefox (>= 3.6)
Description: simple Hello World.
Powered by XULRunner.

Choose a Section from this list.

If your application contains some compiled code, you must remove the line Architecture: all , because its goal is precisely to create a platform independent package. Without this entry, the deb will be specified for the current platform.

If your app requires some other dependencies, add them separated by a comma. If your app contains some compiled code, you can want to use the ${shlibs:Depends} value, when the package will be built, some dependencies will be determined. Example

Depends: xulrunner (>= 1.9.2) | firefox (>= 3.6), ${shlibs:Depends}

The format of the field Description is described in detail in this doc.

The copyright file can contain by example:

This package was debianized by John Doe <johndoe@example.com> on
Sat, 16 Oct 2010 17:55:17 +0200

It was downloaded from <http://example.com/myapp/>

Upstream Author(s):
John Doe <johndoe@example.com>

Copyright:
<Copyright (C) 2010 John Doe>

License:
***** BEGIN LICENSE BLOCK *****
Version: MPL 1.1/GPL 2.0/LGPL 2.1

The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.

The Original Code is MyApp.

The Initial Developer of the Original Code is John Doe.
Portions created by the Initial Developer are Copyright (C) 2010
the Initial Developer. All Rights Reserved.

Contributor(s):
John Doe (johndoe@example.com)

Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.

***** END LICENSE BLOCK *****

The menu file contains:

?package(myapp):needs="X11" section="Applications/Programming"\
title="MyApp" command="/usr/bin/myapp"

Choose a section in this list.

The postinst file is a script launched after the deb installation.  It allows to rebuild the cache of the system icons for example:

#!/bin/sh
# postinst script for myapp
#
# see: dh_installdeb(1)

set -e

# summary of how this script can be called:
# * <postinst> `configure' <most-recently-configured-version>
# * <old-postinst> `abort-upgrade' <new version>
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
# <new-version>
# * <postinst> `abort-remove'
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
# <failed-install-package> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package

case "$1" in
configure)
# for icons of the app
if [ -x /usr/bin/gtk-update-icon-cache ]; then
gtk-update-icon-cache -f /usr/share/icons/hicolor
fi

# uncomment only if the installed app handle some mime-type
#if [ "$2" == "" ]; then
# if [ -x /usr/bin/update-desktop-database ]; then
# update-desktop-database /usr/share/applications
# fi
#fi
;;

abort-upgrade|abort-remove|abort-deconfigure)
;;

*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac

# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.

#DEBHELPER#

exit 0

Finally, the myapp.links file contains the links to create when the deb will be installed:

/usr/lib/myapp/myapp.sh /usr/bin/myapp

2.2.3 Create the deb

Ok, we have now all the necessary files, we can generate our deb. We have to create a folder named like our app name into the debian folder, and under it copy the files like in the final root system:

    |- debian/
|- myapp/
|- usr
|- lib
|- myapp/
|- share
|- applications/
|- myapp.desktop
|- icons/
|- hicolor/
|- 16x16/
|- apps
|- myapp.png
|- 22x22/
|- apps
|- myapp.png
|- 32x32/
|- apps
|- myapp.png
|- 48x48/
|- apps
|- myapp.png
|- scalable/
|- apps
|- myapp.svg

And we will apply some debhelper scripts.

Here a complete shell script, named build_deb.sh, which create a temporary 'tmp' folder, we copy into it our debian files, our application, and our desktop files, then create the deb:

#!/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 the debian folder into TMP_DIR
cp -rv "./debian" "$TMP_DIR/"

# create into TMP_DIR the folder where copy our app
mkdir -pv "$TMP_DIR/debian/myapp/usr/lib"

# and copy the app
cp -rv "./myapp" "$TMP_DIR/debian/myapp/usr/lib/"

# create the folder for the .desktop file and copy it
mkdir -pv "$TMP_DIR/debian/myapp/usr/share/applications"
cp -v "./data/myapp.desktop" \
"$TMP_DIR/debian/myapp/usr/share/applications/"

# create the folder and copy our icons
mkdir -pv "$TMP_DIR/debian/myapp/usr/share/icons/hicolor/16x16/apps"
cp -v "./data/icons/icon16.png" \
"$TMP_DIR/debian/myapp/usr/share/icons/hicolor/16x16/apps/myapp.png"

mkdir -pv "$TMP_DIR/debian/myapp/usr/share/icons/hicolor/22x22/apps"
cp -v "./data/icons/icon22.png" \
"$TMP_DIR/debian/myapp/usr/share/icons/hicolor/22x22/apps/myapp.png"

mkdir -pv "$TMP_DIR/debian/myapp/usr/share/icons/hicolor/32x32/apps"
cp -v "./data/icons/icon32.png" \
"$TMP_DIR/debian/myapp/usr/share/icons/hicolor/32x32/apps/myapp.png"

mkdir -pv "$TMP_DIR/debian/myapp/usr/share/icons/hicolor/48x48/apps"
cp -v "./data/icons/icon48.png" \
"$TMP_DIR/debian/myapp/usr/share/icons/hicolor/48x48/apps/myapp.png"

mkdir -pv "$TMP_DIR/debian/myapp/usr/share/icons/hicolor/scalable/apps"
cp -v "./data/icons/icon48.svg" \
"$TMP_DIR/debian/myapp/usr/share/icons/hicolor/scalable/apps/myapp.svg"

cd "$TMP_DIR"

# clean eventual tempory files
find . -type f -name *.*~ -exec rm {} \;

# deb creation
fakeroot dh_link
fakeroot dh_fixperms
fakeroot dh_installdeb
# uncomment the following line if you have some compiled code and use ${shlibs:Depends} in control file
#dh_shlibdeps
fakeroot dh_gencontrol
fakeroot dh_md5sums
fakeroot dh_builddeb

echo "deb created for myapp :)"

To create our deb, in a terminal:

  • cd samples_chapter_2
  • sh ./build_deb.sh

And as result we have finally the file myapp_1.0-1_all.deb in the samples_chapter_2 folder :) .

Nicolas Martin

You can download all the samples of this chapter 2 (Linux) in the samples_chapter_2.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.

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

- page 1 de 2