PortableApps.com - a developer's perspective

PortableApps.com Menu

This post is about how you can give your customers the convenience of installing applications on portable drives, such as USB memory keys, allowing them to take their working environments – applications, settings, and data – from machine to machine.

A few weeks ago, a customer asked me whether one of my applications (Writer’s Café) could be made compatible with the PortableApps.com suite she was running from her USB memory key. Writer’s Café could already be installed on an external drive, but it used a different directory layout and wasn’t appearing in the PortableApps.com menu.

I wasn’t familiar with PortableApps.com but I checked out the web site (http://www.portableapps.com), found a lot of good reviews and decided it was worth spending some time making Writer’s Café compatible with it. After all, writing software is the kind of thing that people want to take around with them. Currently PortableApps.com is for Windows only, but this could change in future. It comes with a whole suite of free programs such as FireFox, Thunderbird, OpenOffice.org and Pidgin, and the PortableApps.com site provides tools for developers to ‘wrap’ applications to force them to leave no data on the computer’s local drives on exit. If you’re adapting your own application, then you won’t need to jump through as many hoops, though you will still need to use the PortableApps.com installer and tell your application where to put its settings and data.

Location flexibility

First, we’ll look at what you need to do to make your application portable regardless of any particular directory layout standard. (In this context, ‘portable’ means ‘portable between machines’, and not ‘available on different operating systems’ – though your app may very well be portable in both those senses.) I’ll write from the perspective of working with C++ and wxWidgets but it shouldn’t be hard to adapt this to your favourite language and toolkit.

For your application to be portable, it will need to be flexible about where it keeps the following information:

  1. Application data – per-user files that you would normally put under Documents and Settings\<user>\Application Data\<your app>. These are typically writeable files, such as a contact database.
  2. Application settings – data that you would normally store in the registry on Windows, or in a config file somewhere under the user’s directory on other operating systems.
  3. Application support files – typically read-only files that might be installed with the executable, such as translations, images, and other resources.
  4. Document files – the default location for data that the user creates as a result of using the application.

Normally, these locations are determined by API calls, but we need a way to override these defaults when in portable mode. My method is to have the installer copy a file called startup.cnf to the directory containing the executable; the application reads a few settings from it at initialisation time. You can determine the executable location with wxStandardPaths::GetExecutablePath, or your own function if you want to also take into account, say, an environment variable that can be set from a launch script on Linux.

My startup.cnf file for PortableApps.com installation looks like this:

[Settings]
ConfigLocation=%APPDIR%/Data/settings.cnf
AppSupportFilesDir=%APPDIR%/App/WritersCafe
RunningFromExternalDrive=1

ConfigLocation tells the application where to store settings – it will then use wxFileConfig instead of wxConfig so settings are not written to the registry. (wxWidgets’ hierarchy of wxConfigBase-derived classes makes this easy – your application class can have a pointer to a wxConfigBase, and you can simply assign it a wxFileConfig or wxConfig depending on startup.cnf settings. Subsequent access will remain identical in either case.) %APPDIR% is replaced by the actual application directory and the path normalized with wxFileName::Normalize in case ConfigLocation contains relative path components. While ConfigLocation specifies the settings file location, it also implies the Application Data location, because it’s natural to keep writeable app data next to the app settings; so there is no separate Application Data key. However, you could add this as a separate setting for maximum flexibility.

AppSupportFilesDir tells the application where to find the read-only support files. My application already stores m_appDir so the location only has to be found once when finding resources; so m_appDir is simply set to the AppSupportFilesDir value if present. If the resources are always stored next to the executable, then it’s not necessary to specify this key.

RunningFromExternalDrive indicates whether the application is running from an external drive or not. Normally this will be 1, or startup.cnf probably wouldn’t be present anyway. When the application detects this mode, it will attempt to deduce the location of the drive from the full executable path, and if it fails (this is more likely on Linux or Mac than on Windows), it will prompt the user for the drive location.

Perhaps conspicuous for its absence is DocumentDir – the default document directory. You could add this key, or you could do what Writer’s Café does and simply deduce that if running from an external drive, the initial document directory should be Documents at the root of the drive, with the ability to customize this in the application preferences.

Dealing with changing drive letters

Because the drive name can change between sessions (for example if other devices have been plugged in, or the program is running on a different computer), any absolute paths stored between sessions will need a bit of massaging. The way I do this is to save the name of the current drive along with absolute paths such as the preferred default document directory and recently-used files. When reading old settings, I check the original drive name against the absolute paths and if there’s a match, replace the old drive with the new drive that the application is currently running on. If you’re using standard classes to read in file names, for example wxFileHistory::Load, you’ll need to adapt this to read in the file names explicitly and do the drive name substitution. Don’t forget to reverse the order if using wxFileHistory::AddFileToHistory since this function adds the file name to the front of the list and you may find that your history list is in reverse order.

Here's some code you can use for loading a file history from a wxConfig object. GetFilenameSubstitutingVolume gets the new file name given the old file name, old volume and new volume, and FileHistoryLoadWithVolumeSubstitution loads the file history with the new names.

/// Find the file substituting this volume. Tests existence first.
bool GetFilenameSubstitutingVolume(const wxString& filename, wxString& newFilename, const wxString& newVolume, const wxString& oldVolume)
{
    if (filename.IsEmpty() || !IsAbsolutePath(filename))
        return false;

    if (!oldVolume.IsEmpty() && !newVolume.IsEmpty())
    {
        if (oldVolume.Length() != 1 /* don't accept "/" */ && filename.Left(oldVolume.Length()).Lower() == oldVolume.Lower())
        {
            wxString fname = filename.Mid(oldVolume.Length());
            if (fname[0] == wxT('\\') || fname[0] == wxT('/'))
                fname = fname.Mid(1);

            newFilename = AppendPaths(newVolume, fname);
#ifdef __WXMSW__
            newFilename.Replace(wxT("/"), wxT("\\"));
#else
            newFilename.Replace(wxT("\\"), wxT("/"));
#endif
            return wxFileExists(newFilename);
        }
    }
    return false;
}

/// Load file history with volume substitution if file not found
void FileHistoryLoadWithVolumeSubstitution(wxFileHistory& fileHistory, wxConfigBase& config, const wxString& newVolume, const wxString& oldVolume)
{
    wxArrayString files;

    int i = 0;
    wxString buf;
    buf.Printf(wxT("file%d"), (int)i+1);
    wxString historyFile;
    while ((i < fileHistory.GetMaxFiles()) && config.Read(buf, &historyFile) && (!historyFile.empty()))
    {
        if (!wxFileExists(historyFile))
        {
            wxString filename2;
            if (GetFilenameSubstitutingVolume(historyFile, filename2, newVolume, oldVolume))
            {
                files.Insert(filename2, 0);

                // Make sure the new filename is reflected in the config file, so it matches
                // the external drive we will save on exit.
                config.Write(buf, filename2);
            }
        }
        else
            files.Insert(historyFile, 0);

        i ++;
        buf.Printf(wxT("file%d"), (int)i+1);
        historyFile = wxEmptyString;
    }

    for (i = 0; i < (int) files.GetCount(); i++)
    {
        wxString f(files[i]);
        fileHistory.AddFileToHistory(f);
    }
}

Dealing with unplugged drives

A problem common with portable apps is what happens when a user unplugs the drive when the application still has open documents that on the drive. When the application tries to save the document and/or its settings, the result may be confusing error messages and suboptimal behaviour, possibly including infinite loops if the application refuses to exit without saving. It’s a good idea to wrap your file-writing code inside a function that prompts the user to retry, abort or ignore a failed save operation – Writer’s Café also gives the user the opportunity to save the file in a different location in case of hardware failure.

Self-installation onto an external drive

Writer’s Café has a wizard that allows the user to install the application to an external drive, using its own standard for directory organisation that allows for multiple executables for different operating systems, while sharing settings. However this is only convenient for users if they don’t mind installing the program to a local drive in the first place, albeit temporarily. But it’s worth considering (for the non-PortableApps.com case) if you don’t want to have a separate external-drive installer on each platform you support. The Writer's Café wizard can install its own files to the external drive, or it can download the latest version (for multiple platforms) from the web site.

Setting the correct executable name

PortableApps.com relies on the executable containing a user-friendly application name for display in its menu, so add a VERSIONINFO block to your .rc file if you haven’t already. In the example below, the symbols.h file contains version strings for the application that can be changed centrally.

#include "symbols.h"

1 VERSIONINFO
        FILEVERSION wcVERSION_NUMBER_MAJOR,wcVERSION_NUMBER_MINOR,0,0
        PRODUCTVERSION wcVERSION_NUMBER_MAJOR,wcVERSION_NUMBER_MINOR,0,0
        FILEOS VOS__WINDOWS32
        FILETYPE VFT_APP
        BEGIN
            BLOCK "StringFileInfo"
            BEGIN
                BLOCK "040904E4"
                BEGIN
                    VALUE "CompanyName", "Anthemion Software Ltd.\000"
                    VALUE "FileDescription", "Writer's Café\000"
                    VALUE "FileVersion", wcVERSION_NUMBER_RC_STRING
                    VALUE "InternalName", "Writer's Café\000"
                    VALUE "LegalCopyright", "(c) Anthemion Software Ltd.\000"
                    VALUE "LegalTrademarks", "\000"
                    VALUE "OriginalFilename", "WritersCafe.exe\000"
                    VALUE "ProductName", "Writer's Café\000"
                    VALUE "ProductVersion", wcVERSION_NUMBER_RC_STRING
                    VALUE "Comments", "\000"

                    ; These values below can be removed at will
                    ; VALUE "PrivateBuild", "Your Private Build\000"
                    ; VALUE "SpecialBuild", "Your Special Build\000"
                END
            END

            BLOCK "VarFileInfo"
            BEGIN
                VALUE "Translation", 0x0409 0x04E4
            END
        END

More on PortableApps.com installation

OK, so your application now has the ability to run from, and store all its data on, an external drive instead of a local drive. Now you can create an setup program to install the application to the correct locations in the user’s PortableApps.com suite.

First you need to download the Nullsoft Scriptable Install System (NSIS), from http://nsis.sourceforge.net/Main_Page. You also need to install the following plugins to the NSIS Plugins directory: MoreInfo.zip, Registry.zip, NewAdvSplash.zip, and FindProc.zip. Make sure the DLLs from each file go into the NSIS Plugin directory and not a subdirectory, or NSIS will fail to compile your script.

Now create a new build directory where your NSIS scripts and application files will go. When you have established the installer has compiled correctly and installs the right files to the right locations, you can set about automating the process by copying application files and installer scripts to this build directory and updating version numbers if necessary.

An application that has been installed into the PortableApps.com suite typically has a location <drive>\PortableApps\WritersCafePortable. Writer’s Café uses a directory layout like this, conforming to the PortableApps spec:

WritersCafePortable\App\AppInfo
WritersCafePortable\App\WritersCafe
WritersCafePortable\Data
WritersCafePortable\Other\Source
WritersCafePortable\writerscafe.exe
WritersCafePortable\startup.cnf

There are other possible directories, but these are the ones that Writer’s Café uses. Note that the Writer’s Café executable is directly under WritersCafePortable, and the only other file here is startup.cnf. All the read-only application resources are kept under App\WritersCafe.

The contents of startup.cnf is exactly as shown earlier.

In App\AppInfo we put appinfo.ini and appicon.ico. At the time of writing, these aren’t used – the PortableApps.com menu simply scans the subdirectories for .exe files and extracts the application name and icon from the executable – but may be used in future. appinfo.ini looks like this:

[Format]
Type=PortableApps.comFormat
Version=0.9.8

[Details]
Name=Writers Cafe
Publisher=Anthemion Software Ltd.
Homepage=PortableApps.com/WritersCafePortable
Category=Office
Description=Writer's Cafe is a suite of writing tools for fiction authors.

[License]
Shareable=true
OpenSource=false
Freeware=false
CommercialUse=true

[Version]
PackageVersion=2.18.0
DisplayVersion=2.18

[Control]
Icons=1
Start=writerscafe.exe

In App\WritersCafe we place all the resources (translations, image files etc.) that normally accompany the executable. In Other\Source we place all the necessary NSIS scripts. For Writer’s Café, these are:

Attrib.nsh
GetParameters.nsh
GetParent.nsh
PortableApps.comInstaller.bmp
PortableApps.comInstaller.nsi
PortableApps.comInstallerConfig.nsh
PortableApps.comInstallerLANG_ENGLISH.nsh
ReplaceInFile.nsh
StrRep.nsh

We get PortableApps.comInstaller.nsi and PortableApps.comInstallerConfig.nsh from this page:

http://portableapps.com/node/14939

and the other scripts from the Source directory of other apps in the PortableApps.com suite.

There are just a few things to alter in PortableApps.comInstallerConfig.nsh. The following block contains the lines that I edited for Writer’s Café:

;== Basic Information.  Basic information about the portable app
!define NAME "Writer's Cafe Portable"
!define SHORTNAME "WritersCafePortable"
!define VERSION "2.18.0.0"
!define FILENAME "WritersCafe_Portable_2.18"
!define CHECKRUNNING "writerscafe.exe"
!define CLOSENAME "Writer's Cafe Portable "

Now all we need to do is run the NSIS compiler on PortableApps.comInstaller.nsi, and it will create WritersCafe_Portable_2.18.paf.exe. This file can be run directly or it can be invoked from the PortableApps.com menu, under Options, and your application name should appear in the menu.

If you already have a script that builds your application installer, you should find it’s not much work at all to add PortableApps.com support. As an indication, here’s the relevant part of my own release script using the MSYS shell interpreter. Basically it populates the directory structure just described in a fresh directory, copying the program files and scripts to the relevant locations before invoking the NSIS compiler. The doreplace function uses sed to replace strings such as version numbers with the appropriate values.

# Make PortableApps.com version
make_portableappssetup()
{
    # We assume that we've already done the Windows Setup version, so
    # the image directory is populated.

    PORTABLEAPPSDIR=$APPDIR/deliver/WritersCafePortable

    if [ -d "$PORTABLEAPPSDIR" ]; then
        rm -f -r "$PORTABLEAPPSDIR"
    fi

    mkdir "$PORTABLEAPPSDIR"
    mkdir "$PORTABLEAPPSDIR/App"
    mkdir "$PORTABLEAPPSDIR/App/AppInfo"
    mkdir "$PORTABLEAPPSDIR/Data"
    mkdir "$PORTABLEAPPSDIR/Other"
    mkdir "$PORTABLEAPPSDIR/Other/Source"

    if [ ! -d "$SETUPIMAGEDIR" ]; then
        echo *** No $SETUPIMAGEDIR.
        exit
    fi

    cp $APPDIR/scripts/portableapps/*.nsh $APPDIR/scripts/portableapps/*.nsi $APPDIR/scripts/portableapps/*.bmp "$PORTABLEAPPSDIR/Other/Source"
    cp "$APPDIR/scripts/portableapps/appinfo.ini" "$PORTABLEAPPSDIR/App/AppInfo"
    cp "$SETUPIMAGEDIR/writerscafe.ico" "$PORTABLEAPPSDIR/App/AppInfo/appicon.ico"

    doreplace "$PORTABLEAPPSDIR/App/AppInfo/appinfo.ini" "s/%VERSION%/$VERSION/g"
    doreplace "$PORTABLEAPPSDIR/Other/Source/PortableApps.comInstallerConfig.nsh" "s/%VERSION%/$VERSION/g"

    cp -r "$SETUPIMAGEDIR" "$PORTABLEAPPSDIR/App/WritersCafe"
    mv "$PORTABLEAPPSDIR/App/WritersCafe/writerscafe.exe" "$PORTABLEAPPSDIR/writerscafe.exe"

    echo "[Settings]" > "$PORTABLEAPPSDIR/startup.cnf"
    echo "ConfigLocation=%APPDIR%/Data/settings.cnf" >> "$PORTABLEAPPSDIR/startup.cnf"
    echo "AppSupportFilesDir=%APPDIR%/App/WritersCafe" >> "$PORTABLEAPPSDIR/startup.cnf"
    echo "RunningFromExternalDrive=1" >> "$PORTABLEAPPSDIR/startup.cnf"

    # Now invoke installation compiler

    echo "Compiling NSIS PortableApps.com installer..."
    "$NSISCOMPILER" //V2 "$PORTABLEAPPSDIR/Other/Source/PortableApps.comInstaller.nsi"

    PORTABLEAPPINSTALLER="$APPDIR/deliver/WritersCafe_Portable_$VERSION.paf.exe"

    if [ -f "$PORTABLEAPPINSTALLER" ]; then
        echo Created $PORTABLEAPPINSTALLER.
    else
        echo "*** Warning - did not create $PORTABLEAPPINSTALLER."
    fi
}

Summary

We've seen how to:

  • adapt your application to store and read its data on the external drive when necessary;
  • deal with issues such as changing drive letters and unplugged drives;
  • compile an installer for use with the PortableApps.com suite.

Have fun adapting your projects for use on external drives, and - with a bit of luck - attracting new customers as a result!

The PortableApps.com folk are in talks with USB drive manufacturers and it will be interesting to see how the project progresses, especially with expansion to take into account commercial applications and multiple platforms.

For more information, please see the PortableApps.com web site at http://www.portableapps.com. You can also check out the portability features of Writer's Café at http://www.writerscafe.co.uk.