Introduction
Xojo is a great tool for developing cross-platform applications from a single codebase. It allows code to be developed and then built for any or all of the following desktop platforms:
- MacOS 32-bit,
- MacOS 64-bit,
- Windows 32-bit,
- Windows 64-bit,
- Linux 32-bit and
- Linux 64-bit
- Raspberry Pi.
The IDE includes a scripting language that allows application builds to be automated so one menu selection can build for all of these platforms. A sample script to do achieve this is available in the Xojo blog.
While this is great and saves a lot of work it has its limitations. First, the folder structure into which the built applications are placed, and the location of that build folder, cannot be changed. The will always be built into the same folder as the project file.
Secondly, while the applications are ready to run, they are not packaged ready for deploying and installing on end users' machines.
In this article I describe a way that the whole process from source code to finished installer can be achieved with one command and all on a single development machine.
Specifically, we will look at developing an application on a Mac and, on the same machine, automatically creating installers for all platforms. The process will generate disk images (.dmg) for the Mac builds, Packages (.deb) for the Linux platforms (including Raspberry Pi) and executable (.exe) setup programs for Windows.
This can all be accomplished without the need to pay for any additional software beyond the Xojo build licence.
In addition to the barebones software installed on a new Mac you will need the following:
- Xcode.
- The dpkg packaging utility to create the Linux packages.
- The free Windows installer program, INNO Setup.
- Wine, a suite of programs which allow many Windows programs (in this case Inno Setup) to run under Unix type operating systems such as Linux and MacOS.
Installing the Software
Xcode
This is an Apple product and is available free of charge in Mac App Store. When you install it, make sure yo also install the command-line tools.
Dpkg and Wine
Both of these are Linux applications and the best way to install Linux software is to use some form of package manager. One of the most
commonly used Linux package mangers on the Mac is brew. So, if brew is not already installed on your system then it can be installed via the terminal with the command,
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
Once brew has been installed you can then use it to install dpkg with the command,
brew install dpkg
In a similar fashion Wine can be installed but first, Wine needs a graphical interface to be installed. This can be done with the line,
brew cask install xquartz
and when that is complete Wine it self can installed with,
brew install wine
Inno Setup
Once wine is installed you are ready to install the InnoSetup program for creating Windows installers. If you haven't done so already, download Inno Setup from here http://www.jrsoftware.org/isdl.php, navigate to the directory it downloaded to and install it with Wine. For example,
wine innosetup-5.5.9.exe
All that remains to install now is the two scripts required to turn a Xojo project into a collection of installers.
The Scripts
The first required script is the Xojo script shown here (and available for download at the bottom of this page). This script should be copied into the Xojo scripts folder which is inside the Xojo folder usually located in Applications. Typically it will something like,
/Applications/Xojo 2018 Release 1/Scripts
'---------------------------------------------------------------------------------------------
'
' IMPORTANT! - for this script to work the "Use Builds Folder" option must be enabled.
'
' It can be found in the "Shared" section of the "Build Settings".
'
'---------------------------------------------------------------------------------------------
'
' ------- User Adjustable Parameters -------
'
'
' Specify which targets to build by commenting/uncommenting appropriate lines below.
'
Dim platforms() As Integer
platforms.Append(tMac32)
platforms.Append(tMac64)
platforms.Append(tWin32)
platforms.Append(tWin64)
platforms.Append(tLx32)
platforms.Append(tLx64)
platforms.Append(tRPi)
'
' Location to place all build output files.
'
Dim target As String = "$HOME/Desktop/XojoBuilds"
'
' The shell script to be run after each build.
'
Dim script As String = "~/ShellScripts/xojoinstallers"
'
' Flag to determine whether log file should be automatically opened on completion.
' If there are errors the file will be opened independently of this flag, as will the error log.
' Any value other than "Yes" will stop the log from opening.
'
Dim openLog As String = "Yes"
'
' ----------------------------------------------------------------------------------------
'
'
' Sensible names for target platforms.
'
Const tMac32 = 7
Const tMac64 = 16
Const tWin32 = 3
Const tWin64 = 19
Const tLx32 = 4
Const tLx64 = 17
Const tRPi = 18
'
' Define required variables.
'
Dim path As String ' Will hold the full path to the executable file.
Dim cmd As String ' Will hold the command string to execute the shell script.
Dim output As String = "" ' Will hold total output from multiple runs of the shell script'
Dim i As Integer ' Iterator variable.
Dim response As String ' Required to receive response from completion dialog.
'
' Application version to be passed to the shell script.
'
Dim version As String = PropertyValue("app.version")
'
' Iterate through each of the specified targets
'
For i=0 To platforms.Ubound
' Build the application
path = BuildApp(platforms(i))
' Execute the shell script
cmd = script + " " + path + " " + ProjectShellPath + " " + target + " " + Str(platforms(i)) +_
" " + version + " " + Str(i) + " " + Str(platforms.Ubound) + " " + openLog
output = output + DoShellCommand(cmd)
Next
'
' Report script completion.
'
' response = ShowDialog("Create Installers", "All Tasks complete. See xxxx for details.","OK", "", "", -1)
Beep
response = ShowDialog("Create Installers Process Complete", output,"OK", "", "", -1)
'Print("Build process completed")
There are several places at the top of this script that can be edited to make certain changes to its operation. In particular you can specify,
- which target platforms will be built,
- the location to send all output files,
- the bash script file to run after each build (see below) and
- whether or not the log file should open automatically on completion of all builds.
The second script is a bash shell script that will be called by the Xojo script for each target the Xojo builds. The bash script shown below move all of the built files to the specified location and then create each of the application installers. The application installers will all reside in a single directory upon completion.
This script can be copied to anywhere on your Mac provided the appropriate line of the Xojo script is edited to point to it. In my case I named the script xojoinstallers and stored in a folder called ShellScripts in my home directory.
#!/bin/bash
#########################################
# #
# Post-build script for Xojo Desktop #
# #
#########################################
#
# Script Arguments
#
# $1 is the complete path to the newly built application file.
# $2 is the complete path to the Xojo project file.
# $3 is the path to the desired build location.
# $4 is the build's target platform as defined in Xojo.
# $5 is version number of application.
#
# This script is generally called in a loop with a different target each time.
# $6 is the current iteration number.
# $7 the total number of iterations that will occur. It allows the final iteration to be identified and thus the log file to be displayed.
# $8 is a flag which, if set to 'Yes' will automatically open the log file at the end of the run even if there were no errors.
#
# Check the number of passed arguments is correct.
# This is promarily done to stop the script from being run accidently from the command line
# as it could accidently delete impoertant files.
#
if [ ! $# == 8 ]; then
echo Incorrect number of arguments provided \($#\). This script will now exit.
exit 1
fi
#
# Information fields for various installers.
#
publisher="Gluon Field"
url="www.gluonfield.com"
#
# The location of Wine - required to run the Windows setup program.
#
PATHTOWINE=/usr/local/bin
#
# The location of the INNO Setup program.
#
INNOPATH=~/.wine/drive_c/Program\ Files/Inno\ Setup\ 5
#
# Set some values relevant to the current build target.
# - tdir is the directory name that will be used for the target
# - os distinguishes between platforms: Mac, Windows, Linux and Raspberry Pi
# - arch is the architecture value used in Linux .deb packages.
#
case $4 in
7) tdir=Mac32
os=M
;;
16) tdir=Mac64
os=M
;;
3) tdir=Win32
os=W
;;
19) tdir=Win64
os=W
;;
4) tdir=Linux32
os=L
arch=i386
;;
17) tdir=Linux64
os=L
arch=amd64
;;
18) tdir=RPi
os=L
arch=armhf
;;
esac
#
# If this is the first iteration get rid of the previous log and error files, if they exist.
#
shelldir=$(dirname "$BASH_SOURCE")
if [ $6 == 0 ]; then
if [ -f "$shelldir/log.txt" ]; then
rm "$shelldir/log.txt"
fi
if [ -f "$shelldir/err.txt" ]; then
rm "$shelldir/err.txt"
fi
fi
#
# This brace is the start of the code block which will have its stdout and stderr redirected to the log/err files.
#
{
echo
echo "Commencing Script for $tdir."
echo
#
# Get the build folder, path to the executable, the executable name and the project name.
# The package name is the same as the executable name but with any underscores replaced with dashes.
#
dir=$(dirname "$1")
appname=$(basename "$1")
package=$(echo "$appname" | tr _ -)
project=$(basename "$2")
project=$(echo "$project" | cut -f 1 -d '.')
#
# Move into the build directory for this target.
#
cd "$dir"
#
# If the traget directory does not yet exist the create it.
#
if [ ! -d "$3" ]; then
mkdir "$3"
fi
#
# If this is the first iteration delete any old build location directories (and their contents) and create new ones.
#
if [ $6 == 0 ]; then
if [ -d "$3/$project/Installers" ]; then
rm -rf "$3/$project/Installers"
fi
if [ -d "$3/$project" ]; then
rm -rf "$3/$project"
fi
fi
#
# Create the project directory if it isn't already there.
#
if [ ! -d "$3/$project" ]; then
mkdir "$3/$project"
fi
#
# Create a directory for the finished installers if it isn't already there.
#
if [ ! -d "$3/$project/Installers" ]; then
mkdir "$3/$project/Installers"
fi
#
# If the directory for this target is already there then delete it and its contents.
#
if [ -d "$3/$project/$tdir" ]; then
rm -rf "$3/$project/$tdir"
fi
#
# Now make a new target directory.
#
mkdir "$3/$project/$tdir"
#
# Code to deal with a Linux target. This will create a .deb package.
#
if [ "$os" == L ] ; then
#
# Create the directory structure for the package and move the files into it from the Xojo build folder.
#
mkdir "$3/$project/$tdir/usr"
mkdir "$3/$project/$tdir/usr/local"
mkdir "$3/$project/$tdir/usr/local/bin"
mv * "$3/$project/$tdir/usr/local/bin"
mkdir "$3/$project/$tdir/DEBIAN"
mv "$3/$project/$tdir/usr/local/bin/appicon"* "$3/$project/$tdir/usr/local/bin/$appname Libs"
#
# Now make the control file for the deb package,
#
echo "Package: $package" > "$3/$project/$tdir/DEBIAN/control"
#
# If this is a Raspberry Pi application then add the libunwind8 dependency required since Xojo 2018r1.
#
if [ $tdir == "RPi" ]; then
echo "Depends: libunwind8" >> "$3/$project/$tdir/DEBIAN/control"
fi
echo "Version: $5" >> "$3/$project/$tdir/DEBIAN/control"
echo "Maintainer: The Gluon Field" >> "$3/$project/$tdir/DEBIAN/control"
echo "Architecture: $arch" >> "$3/$project/$tdir/DEBIAN/control"
echo "Description: $appname" >> "$3/$project/$tdir/DEBIAN/control"
#
# ...and then the desktop file.
#
mkdir "$3/$project/$tdir/usr/share"
mkdir "$3/$project/$tdir/usr/share/applications"
fname="$appname.desktop"
echo "[Desktop Entry]" > "$3/$project/$tdir/usr/share/applications/$fname"
echo "Name=$project" >> "$3/$project/$tdir/usr/share/applications/$fname"
echo "Comment=$project Version: $5" >> "$3/$project/$tdir/usr/share/applications/$fname"
echo "Exec=/usr/local/bin/$appname" >> "$3/$project/$tdir/usr/share/applications/$fname"
echo "Icon=/usr/local/bin/$appname Libs/appicon_128.png" >> "$3/$project/$tdir/usr/share/applications/$fname"
echo "Terminal=false" >> "$3/$project/$tdir/usr/share/applications/$fname"
echo "Type=Application" >>"$3/$project/$tdir/usr/share/applications/$fname"
echo "MimiType=application" >> "$3/$project/$tdir/usr/share/applications/$fname"
#
# Now create Linux deb package and move it into the Installers folder.
#
/usr/local/bin/dpkg-deb -b "$3/$project/$tdir"
mv "$3/$project/$tdir.deb" "$3/$project/Installers"
#
# Code to deal with a MacOS target. This will create a .deb package.
#
elif [ "$os" == M ]; then
#
# Move the app out of the Xojo build folder and into the target, then create a directory for the package file.
#
mv * "$3/$project/$tdir"
mkdir "$3/$project/$tdir/pkg"
#
# Create the package file,
#
productbuild --component "$3/$project/$tdir/$appname" "/Application" "$3/$project/$tdir/pkg/$tdir.pkg"
#
# ... then put it into a disk image.
#
hdiutil create -srcfolder "$3/$project/$tdir/pkg" -ov "$3/$project/Installers/$tdir.dmg"
#
# Code to deal with a Windows target. This will create a .deb package.
#
elif [ "$os" == W ]; then
#
# Move all of the files from the Xojo build folder into the target folder.
#
mv * "$3/$project/$tdir"
#
# Now make the Inno Script
#
iss="$3/$project/Xscript.iss"
echo "#define MyAppName \"$project\"" > "$iss"
echo "#define MyAppVersion \"$5\"" >> "$iss"
echo "#define MyAppPublisher \"$publisher\"" >> "$iss"
echo "#define MyAppURL \"$url\"" >> "$iss"
echo "#define MyAppExeName \"$appname\"" >> "$iss"
echo >> "$iss"
echo " [Setup]" >> "$iss"
echo "AppId={{$(uuidgen)}" >> "$iss"
echo "AppName={#myAppName}" >> "$iss"
echo "AppVersion={#myAppVersion}" >> "$iss"
echo "AppPublisher={#myAppPublisher}" >> "$iss"
echo "AppPublisherURL={#myAppURL}" >> "$iss"
echo "AppSupportURL={#myAppURL}" >> "$iss"
echo "AppUpdatesURL={#myAppURL}" >> "$iss"
echo "DefaultDirName={pf}\{#MyAppName}" >> "$iss"
echo "DefaultGroupName={#MyAppName}" >> "$iss"
outdir="$3/$project/Installers"
echo "OutputDir=Z:${outdir//\//\\}" >> "$iss"
if [ $tdir == "Win32" ]; then
echo "OutputBaseFilename=setup32" >> "$iss"
else
echo "OutputBaseFilename=setup64" >> "$iss"
fi
echo "Compression=lzma" >> "$iss"
echo "SolidCompression=yes" >> "$iss"
echo >> "$iss"
echo " [Languages]" >> "$iss"
echo "Name: \"english\"; MessagesFile: \"compiler:Default.isl\"" >> "$iss"
echo >> "$iss"
echo " [Tasks]" >> "$iss"
echo "Name: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked" >> "$iss"
echo "Name: \"quicklaunchicon\"; Description: \"{cm:CreateQuickLaunchIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked;" >> "$iss"
echo >> "$iss"
echo " [Files]" >> "$iss"
#
# List all of the files to be included in the installer.
#
#
# First change the internal field separator to be an end of line so file names with spaces aren't split.
#
SAVEIFS=$IFS
IFS=$'\n'
FILES=$(find "$3/$project/$tdir" -type f)
for f in $FILES
do
echo "Source: \"Z:${f//\//\\}\"; DestDir: \"{app}\"; Flags: ignoreversion" >> "$iss"
done
#
# Now reinstate the original internal field separator.
#
IFS=$SAVEIFS
echo >> "$iss"
echo " [Icons]" >> "$iss"
echo "Name: \"{group}\{#MyAppName}\"; Filename: \"{app}\{#MyAppExeName}\"" >> "$iss"
echo "Name: \"{group}\{cm:UninstallProgram,{#MyAppName}}\"; Filename: \"{uninstallexe}\"" >> "$iss"
echo "Name: \"{commondesktop}\{#MyAppName}\"; Filename: \"{app}\{#MyAppExeName}\"; Tasks: desktopicon" >> "$iss"
echo "Name: \"{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}\"; Filename: \"{app}\{#MyAppExeName}\"; Tasks: quicklaunchicon" >> "$iss"
echo >> "$iss"
echo " [Run]" >> "$iss"
echo "Filename: \"{app}\{#MyAppExeName}\"; Description: \"{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}\"; Flags: nowait postinstall skipifsilent" >> "$iss"
#
# Move to the INNO installation folder, copy the script there and run INNO's command line compiler in Wine.
# Wine 'fixme' erros are suppressed so they do not appear in the error log file."
#
cd "$INNOPATH"
mv "$iss" .
WINEDEBUG=fixme-all $PATHTOWINE/wine ISCC.exe XScript.iss
fi
#
# Final output to the log file.
#
echo
echo "$tdir Script complete."
echo
echo "---------------------------------------------------------------------------------------------------------------"
#
# This brace is the end of the code block which will have its stdout and stderr redirected to the log/err files.
#
} 2>&1 >>$shelldir/log.txt | tee -a $shelldir/log.txt >> $shelldir/err.txt
#
# If this is the final iteration then open the log file in the default application for text files before exiting.
#
if [ $6 == $7 ]; then
# open -t $shelldir/log.txt
if [ -s $shelldir/err.txt ]; then
echo "There were errors, see the err.txt file located in"
echo "$shelldir/"
open -t $shelldir/log.txt
open -t $shelldir/err.txt
else
if [ $8 == "Yes" ]; then
open -t $shelldir/log.txt
fi
echo "The process completed without error."
fi
fi
#
# END OF SCRIPT
#
Of course, the script could be modified to automate other processes such as code signing or deploying the installers to a server for distribution.
Add new comment