Creating a Distribution Package of Your Java Application
Say, you have written a Java application and now it is time to create windows installer package to upload it to your site so that anybody could download it, run and enjoy your proggy. Of course, one can distribute a plain JAR file, but not all users will be able to start it successfully.
It is better to give them a normal plain installer package that they know how to run through the wizard. You may also want to bundle a JRE with your application so your program will run on every machine with no JRE installed. I would also advise obfuscate your code, thus, no evil hacker could get your code too easily. It also makes sense to wrap your Java application in a regular windows executable (EXE) file.
You will need the following tools and libraries that are distributed free of charge:
1. Ant tool.
2. NSIS: a scriptable win32 installer/uninstaller system.
3. ProGuard: java shrinker, optimizer, and obfuscator.
4. Launch4j: Cross-platform Java executable wrapper for creating lightweight Windows native EXEs.
5. NSIS Ant task: to compile NSIS scripts.
The goal is to create a single build.xml file that will compile (JAVAC), obfuscate, shrink, optimize (ProGuard) our code, create an EXE file (Launch4j) and pack it all including other files (like license text file) to an installer package (NSIS).
The ant build file consists of a number of targets that can depend upon other targets in the same build file. That means before the target does something useful it will run the targets it depends upon:
Initialization Target
First, let’s define a number of properties:
XML:
-
<property name="src" location="d:\myproggy\src" />
-
<property name="lib" location="lib" />
-
<property name="images" location="images" />
-
<property name="nsis" location="nsis" />
-
<property name="dist" location="dist" />
-
<property name="html" location="html" />
-
<property name="build" location="build" />
-
<property name="launch4j.dir" location="../.." />
-
<property name="original.dir" location="d:\myproggy" />
You may want to justify the paths according to your system.
The target init will depend upon target ‘clean’ – we want to clean everything before the build starts:
XML:
-
<target name="clean" description="clean up">
-
<delete dir="${build}"/>
-
<delete dir="${html}"/>
-
<delete dir="${images}"/>
-
<delete dir="${lib}"/>
-
<delete dir="${dist}"/>
-
</target>
After it is over, the target ‘init’ will run.
XML:
-
<target name="init" depends="clean">
-
<tstamp />
-
<mkdir dir="${build}" />
-
<mkdir dir="${html}" />
-
<mkdir dir="${images}" />
-
<mkdir dir="${lib}" />
-
<mkdir dir="${dist}" />
-
-
<copy todir="${html}">
-
<fileset dir="${original.dir}/html/"/>
-
</copy>
-
-
<copy todir="${images}">
-
<fileset dir="${original.dir}/images/"/>
-
</copy>
-
-
<copy todir="${lib}">
-
<fileset dir="${original.dir}/lib"/>
-
</copy>
-
-
</target>
We create a number of folders, copy source code, libraries, html, and image files.
Compile Target
XML:
-
<target name="compile" depends="init" description="compile the source">
-
<javac srcdir="${src}" destdir="${build}" classpathref="dist.classpath" source="1.5" debug="on" />
-
-
<copy todir="${build}">
-
<fileset dir="${src}">
-
<include name="*.vm"/>
-
<include name="*.properties"/>
-
</fileset>
-
</copy>
-
-
</target>
Compile the classes and copy the required files, such as Velocity templates and property files to the folder with the freshly compiled classes.
JAR Target
Next step is to create a JAR file and feed it to ProGuard that will “obfuscate, shrink, optimize” our code. ProGuard task has a number of limitations and requirements, so while my example is pretty clear you may want to read the ProGuard documentation. In a few words you name the libraries that are used by your application, specify you jar file name, and the name of the jar that will be the result of ProGuard’s work. ProGuard modifies the names of classes, so you need to keep the name of your Main-Class untouched. Anyway, consult the docs or you may run into some issues here.
XML:
-
<target name="jar" depends="compile" description="create the jar">
-
-
<fileset dir="${lib}" id="lib.dist.fileset">
-
<include name="**/*.jar" />
-
</fileset>
-
-
<pathconvert pathsep=" " property="dist.classpath" refid="lib.dist.fileset">
-
<map from="${lib}" to=".\lib" />
-
</pathconvert>
-
-
<!-- Put everything in ${build} into a jar file -->
-
<jar jarfile="${ant.project.name}.jar">
-
<fileset dir="${build}" includes="**/*" />
-
<manifest>
-
<!-- SET YOUR MAIN CLASS HERE -->
-
<attribute name="Main-Class" value="com.javazing.MyProggyLauncher" />
-
<attribute name="Class-Path" value=". ${dist.classpath}" />
-
</manifest>
-
</jar>
-
-
<!-- obfuscate and optimize by ProGuard -->
-
<taskdef resource="proguard/ant/task.properties" classpath="${launch4j.dir}/lib/proguard.jar" />
-
<proguard>
-
-
-libraryjars jre/lib/rt.jar
-
-libraryjars lib/commons-collections-3.1.jar
-
-libraryjars lib/commons-io-1.2.jar
-
-libraryjars lib/commons-lang-2.1.jar
-
-libraryjars lib/commons-logging.jar
-
-libraryjars lib/hsqldb.jar
-
-libraryjars lib/jdom-1.0.jar
-
-libraryjars lib/log4j.jar
-
-libraryjars lib/lucene-core-1.9.1.jar
-
-libraryjars lib/org.eclipse.core.commands_3.2.0.I20060511-0800a.jar
-
-libraryjars lib/org.eclipse.equinox.common_3.2.0.v20060512.jar
-
-libraryjars lib/org.eclipse.jface.text_3.2.0.v20060518-0800.jar
-
-libraryjars lib/org.eclipse.jface_3.2.0.I20060511-0800a.jar
-
-libraryjars lib/org.eclipse.osgi_3.2.0.v20060510.jar
-
-libraryjars lib/org.eclipse.swt.win32.win32.x86_3.2.0.v3224.jar
-
-libraryjars lib/org.eclipse.text_3.2.0.v20060518-0800.jar
-
-libraryjars lib/rome-0.8.jar
-
-libraryjars lib/velocity-1.4.jar
-
-
-injars ${ant.project.name}.jar
-
-outjars obfuscated_${ant.project.name}.jar
-
-
-keepclasseswithmembers public class * {
-
public static void main(java.lang.String[]);
-
}
-
-
-dontskipnonpubliclibraryclasses
-
-
-keep public class com.javazing.MyProggyLauncher
-
-
-keep class * implements java.sql.Driver
-
-
-
</proguard>
-
-
</target>
EXE Target
After the JAR target, we have a jar file that needs to be wrapped to become a native Windows executable file. We use Launch4j tool here. Launch4j needs an xml file with the settings:
proggy.xml file:
XML:
-
<launch4jConfig>
-
<dontWrapJar>false</dontWrapJar>
-
<headerType>0</headerType>
-
<jar>..\obfuscated_my_cool_proggy.jar</jar>
-
<outfile>..\my_cool_proggy.exe</outfile>
-
<errTitle>MyProggy</errTitle>
-
<jarArgs></jarArgs>
-
<chdir>.</chdir>
-
<customProcName>false</customProcName>
-
<stayAlive>false</stayAlive>
-
<icon>proggy.ico</icon>
-
<jre>
-
<path>jre</path>
-
<minVersion>1.5.0</minVersion>
-
<maxVersion></maxVersion>
-
<initialHeapSize>0</initialHeapSize>
-
<maxHeapSize>0</maxHeapSize>
-
<args></args>
-
</jre>
-
<splash>
-
<file>proggy.bmp</file>
-
<waitForWindow>true</waitForWindow>
-
<timeout>60</timeout>
-
<timeoutErr>true</timeoutErr>
-
</splash>
-
</launch4jConfig>
You may want to get an ICO & BMP images for your application. The icon file is the image to be rendered at the left top corner of every Windows application. The BMP file will be displayed as a splash file when the proggy starts. Java 1.6 has built-in splash functionality, so you may want to use it along with some nice PNG image with shadows and other alpha tricks, and not the Launch4J’s ugly bmp file.
XML:
-
<target name="exe" depends="jar">
-
<taskdef name="launch4j" classname="net.sf.launch4j.ant.Launch4jTask" classpath="${launch4j.dir}/launch4j.jar
-
:${launch4j.dir}/lib/xstream.jar" />
-
<launch4j configFile="./l4j/proggy.xml" />
-
-
</target>
Distibution Target
NSIS comes with a number of templates, samples, some nice graphics and GUI application. Therefore, you will definitely want to see its samples. my_proggy.nsi is the name of the script that will guide you installation and deinstallation processes: create folders, copy files, create menu group, create desktop icons etc.
Sample NSIS script:
NSIS:
-
!include "MUI.nsh"
-
-
!define MUI_ICON "proggy.ico"
-
!define MUI_UNICON "proggy.ico"
-
-
Function .onInit
-
# the plugins dir is automatically deleted when the installer exits
-
InitPluginsDir
-
File /oname=$PLUGINSDIR\splash.bmp "D:\myproggy\splash.bmp"
-
#optional
-
#File /oname=$PLUGINSDIR\splash.wav "C:\myprog\sound.wav"
-
-
splash::show 1000 $PLUGINSDIR\splash
-
-
Pop $0 ; $0 has '1' if the user closed the splash screen early,
-
; '0' if everything closed normally, and '-1' if some error occurred.
-
FunctionEnd
-
-
-
;--------------------------------
-
;General
-
-
;Name and file
-
Name "${PRODUCT_NAME}"
-
OutFile "${EXE_NAME}"
-
-
;Default installation folder
-
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
-
-
;Get installation folder from registry if available
-
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" ""
-
-
;--------------------------------
-
;Interface Configuration
-
-
!define MUI_HEADERIMAGE
-
!define MUI_HEADERIMAGE_BITMAP "proggy_header.bmp" ; optional
-
-
;--------------------------------
-
;Variables
-
-
Var MUI_TEMP
-
Var STARTMENU_FOLDER
-
-
;--------------------------------
-
;Interface Settings
-
-
!define MUI_ABORTWARNING
-
-
;--------------------------------
-
;Pages
-
-
!insertmacro MUI_PAGE_LICENSE "license.rtf"
-
!insertmacro MUI_PAGE_DIRECTORY
-
-
;Start Menu Folder Page Configuration
-
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU"
-
!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${PRODUCT_NAME}"
-
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
-
!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
-
!insertmacro MUI_PAGE_INSTFILES
-
!insertmacro MUI_UNPAGE_CONFIRM
-
!insertmacro MUI_UNPAGE_INSTFILES
-
-
;--------------------------------
-
;Languages
-
-
!insertmacro MUI_LANGUAGE "English"
-
-
;--------------------------------
-
;Installer Sections
-
Section "install"
-
-
;Add files
-
SetOutPath "$INSTDIR"
-
-
File "${FOLDER}\${EXE_NAME}"
-
File "${FOLDER}\settings.ini"
-
-
SetOutPath "$INSTDIR"
-
file /r ${FOLDER}\images
-
file /r ${FOLDER}\html
-
file /r ${FOLDER}\jre
-
file /r ${FOLDER}\lib
-
file ${FOLDER}\evaluation_license.txt
-
file ${FOLDER}\license.rtf
-
-
;create desktop shortcut
-
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${EXE_NAME}" ""
-
-
;create start-menu items
-
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
-
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0
-
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${EXE_NAME}" "" "$INSTDIR\${EXE_NAME}" 0
-
-
;write uninstall information to the registry
-
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${SHORT_NAME}" "DisplayName" "${SHORT_NAME} (remove only)"
-
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${SHORT_NAME}" "UninstallString" "$INSTDIR\Uninstall.exe"
-
-
WriteUninstaller "$INSTDIR\Uninstall.exe"
-
-
SectionEnd
-
;--------------------------------
-
;Uninstaller Section
-
-
Section "Uninstall"
-
-
;ADD YOUR OWN FILES HERE...
-
;Delete Files
-
RMDir /r "$INSTDIR\*.*"
-
-
;Remove the installation directory
-
RMDir "$INSTDIR"
-
-
;Delete Start Menu Shortcuts
-
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
-
Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*"
-
RmDir "$SMPROGRAMS\${PRODUCT_NAME}"
-
-
;Delete Uninstaller And Unistall Registry Entries
-
DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\${SHORT_NAME}"
-
DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SHORT_NAME}"
-
-
Delete "$INSTDIR\Uninstall.exe"
-
-
RMDir "$INSTDIR"
-
-
!insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP
-
-
Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk"
-
-
;Delete empty start menu parent diretories
-
StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP"
-
-
startMenuDeleteLoop:
-
ClearErrors
-
RMDir $MUI_TEMP
-
GetFullPathName $MUI_TEMP "$MUI_TEMP\.."
-
-
IfErrors startMenuDeleteLoopDone
-
-
StrCmp $MUI_TEMP $SMPROGRAMS startMenuDeleteLoopDone startMenuDeleteLoop
-
startMenuDeleteLoopDone:
-
-
DeleteRegKey /ifempty HKCU "Software\${PRODUCT_NAME}"
-
-
SectionEnd
-
-
;Function that calls a messagebox when installation finished correctly
-
Function .onInstSuccess
-
MessageBox MB_OK "You have successfully installed ${PRODUCT_NAME}. Use the desktop icon to start the program."
-
FunctionEnd
-
-
Function un.onUninstSuccess
-
MessageBox MB_OK "You have successfully uninstalled ${PRODUCT_NAME}."
-
FunctionEnd
And the appropriate target:
XML:
-
<target name="dist" depends="exe" description="make installation package for distribution">
-
-
<taskdef name="nsis" classname="net.sf.nsisant.Task">
-
<classpath location="nsisant-1.2.jar"/>
-
</taskdef>
-
-
<nsis script="nsis/my_proggy.nsi" verbosity="4" out="nsis_build.log" noconfig="yes">
-
<define name="VERSION" value="1.0"/>
-
<define name="PRODUCT_NAME" value="My Proggy"/>
-
<define name="EXE_NAME" value="${ant.project.name}.exe"/>
-
<define name="SHORT_NAME" value="MyCoolProggy"/>
-
<define name="FOLDER" value="D:\launch4j\MyProggy"/>
-
</nsis>
-
-
<copy file="${nsis}/${ant.project.name}.exe" tofile="${dist}/${ant.project.name}.exe"/>
-
-
<delete file="${nsis}/${ant.project.name}.exe"/>
-
-
<delete dir="${build}"/>
-
<delete dir="${html}"/>
-
<delete dir="${images}"/>
-
<delete dir="${lib}"/>
-
-
<delete file="${ant.project.name}.jar"/>
-
<delete file="${ant.project.name}.exe"/>
-
<delete file="obfuscated_${ant.project.name}.jar" />
-
-
-
</target>
It is really convenient. After you make some changes in the code, you run a single file and have an installer package ready.
By the way, it also makes sense to check for the required system paths before the build process. Create a .BAT file: