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:

  1. <property name="src" location="d:\myproggy\src" />
  2.     <property name="lib" location="lib" />
  3.     <property name="images" location="images" />
  4.     <property name="nsis" location="nsis" />
  5.     <property name="dist" location="dist" />
  6.     <property name="html" location="html" />
  7.     <property name="build" location="build" />
  8.     <property name="launch4j.dir" location="../.." />
  9.     <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:

  1. <target name="clean" description="clean up">
  2.         <delete dir="${build}"/>
  3.         <delete dir="${html}"/>
  4.         <delete dir="${images}"/>
  5.         <delete dir="${lib}"/>
  6.         <delete dir="${dist}"/>
  7.     </target>

After it is over, the target ‘init’ will run.

XML:

  1. <target name="init" depends="clean">
  2.         <tstamp />
  3.         <mkdir dir="${build}" />
  4.         <mkdir dir="${html}" />
  5.         <mkdir dir="${images}" />
  6.         <mkdir dir="${lib}" />
  7.         <mkdir dir="${dist}" />
  8.        
  9.         <copy todir="${html}">
  10.             <fileset dir="${original.dir}/html/"/>
  11.         </copy>
  12.        
  13.         <copy todir="${images}">
  14.             <fileset dir="${original.dir}/images/"/>
  15.         </copy>  
  16.  
  17.         <copy todir="${lib}">
  18.             <fileset dir="${original.dir}/lib"/>
  19.         </copy>  
  20.        
  21.     </target>

We create a number of folders, copy source code, libraries, html, and image files.

Compile Target

XML:

  1. <target name="compile" depends="init" description="compile the source">
  2.         <javac srcdir="${src}" destdir="${build}" classpathref="dist.classpath" source="1.5" debug="on" />
  3.        
  4.           <copy todir="${build}">
  5.             <fileset dir="${src}">
  6.               <include name="*.vm"/>
  7.               <include name="*.properties"/>
  8.             </fileset>
  9.           </copy>      
  10.           
  11.     </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:

  1. <target name="jar" depends="compile" description="create the jar">
  2.    
  3.         <fileset dir="${lib}" id="lib.dist.fileset">
  4.             <include name="**/*.jar" />
  5.         </fileset>
  6.        
  7.         <pathconvert pathsep=" " property="dist.classpath" refid="lib.dist.fileset">
  8.             <map from="${lib}" to=".\lib" />
  9.         </pathconvert>
  10.        
  11.         <!-- Put everything in ${build} into a jar file -->
  12.         <jar jarfile="${ant.project.name}.jar">
  13.             <fileset dir="${build}" includes="**/*" />
  14.             <manifest>
  15.                 <!-- SET YOUR MAIN CLASS HERE -->
  16.                 <attribute name="Main-Class" value="com.javazing.MyProggyLauncher" />
  17.                 <attribute name="Class-Path" value=". ${dist.classpath}" />
  18.             </manifest>
  19.         </jar>
  20.        
  21.         <!-- obfuscate and optimize by ProGuard -->
  22.         <taskdef resource="proguard/ant/task.properties" classpath="${launch4j.dir}/lib/proguard.jar" />
  23.         <proguard>
  24.        
  25.             -libraryjars jre/lib/rt.jar
  26.             -libraryjars lib/commons-collections-3.1.jar
  27.             -libraryjars lib/commons-io-1.2.jar
  28.             -libraryjars lib/commons-lang-2.1.jar
  29.             -libraryjars lib/commons-logging.jar
  30.             -libraryjars lib/hsqldb.jar
  31.             -libraryjars lib/jdom-1.0.jar
  32.             -libraryjars lib/log4j.jar
  33.             -libraryjars lib/lucene-core-1.9.1.jar
  34.             -libraryjars lib/org.eclipse.core.commands_3.2.0.I20060511-0800a.jar
  35.             -libraryjars lib/org.eclipse.equinox.common_3.2.0.v20060512.jar
  36.             -libraryjars lib/org.eclipse.jface.text_3.2.0.v20060518-0800.jar
  37.             -libraryjars lib/org.eclipse.jface_3.2.0.I20060511-0800a.jar
  38.             -libraryjars lib/org.eclipse.osgi_3.2.0.v20060510.jar
  39.             -libraryjars lib/org.eclipse.swt.win32.win32.x86_3.2.0.v3224.jar
  40.             -libraryjars lib/org.eclipse.text_3.2.0.v20060518-0800.jar
  41.             -libraryjars lib/rome-0.8.jar
  42.             -libraryjars lib/velocity-1.4.jar         
  43.            
  44.             -injars      ${ant.project.name}.jar
  45.             -outjars     obfuscated_${ant.project.name}.jar
  46.  
  47.             -keepclasseswithmembers public class * {
  48.             public static void main(java.lang.String[]);
  49.             }
  50.  
  51.             -dontskipnonpubliclibraryclasses
  52.  
  53.             -keep public class com.javazing.MyProggyLauncher
  54.            
  55.             -keep class * implements java.sql.Driver
  56.            
  57.            
  58.         </proguard>
  59.        
  60.     </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:

  1. <launch4jConfig>
  2.   <dontWrapJar>false</dontWrapJar>
  3.   <headerType>0</headerType>
  4.   <jar>..\obfuscated_my_cool_proggy.jar</jar>
  5.   <outfile>..\my_cool_proggy.exe</outfile>
  6.   <errTitle>MyProggy</errTitle>
  7.   <jarArgs></jarArgs>
  8.   <chdir>.</chdir>
  9.   <customProcName>false</customProcName>
  10.   <stayAlive>false</stayAlive>
  11.   <icon>proggy.ico</icon>
  12.   <jre>
  13.     <path>jre</path>
  14.     <minVersion>1.5.0</minVersion>
  15.     <maxVersion></maxVersion>
  16.     <initialHeapSize>0</initialHeapSize>
  17.     <maxHeapSize>0</maxHeapSize>
  18.     <args></args>
  19.   </jre>
  20.   <splash>
  21.     <file>proggy.bmp</file>
  22.     <waitForWindow>true</waitForWindow>
  23.     <timeout>60</timeout>
  24.     <timeoutErr>true</timeoutErr>
  25.   </splash>
  26. </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:

  1. <target name="exe" depends="jar">
  2.         <taskdef name="launch4j" classname="net.sf.launch4j.ant.Launch4jTask" classpath="${launch4j.dir}/launch4j.jar
  3.             :${launch4j.dir}/lib/xstream.jar" />
  4.         <launch4j configFile="./l4j/proggy.xml" />
  5.        
  6.     </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:

  1. !include "MUI.nsh"
  2.  
  3.   !define MUI_ICON "proggy.ico"
  4.   !define MUI_UNICON "proggy.ico"
  5.  
  6. Function .onInit
  7.     # the plugins dir is automatically deleted when the installer exits
  8.     InitPluginsDir
  9.     File /oname=$PLUGINSDIR\splash.bmp "D:\myproggy\splash.bmp"
  10.     #optional
  11.     #File /oname=$PLUGINSDIR\splash.wav "C:\myprog\sound.wav"
  12.  
  13.     splash::show 1000 $PLUGINSDIR\splash
  14.  
  15.     Pop $0 ; $0 has '1' if the user closed the splash screen early,
  16.             ; '0' if everything closed normally, and '-1' if some error occurred.
  17. FunctionEnd
  18.  
  19.  
  20. ;--------------------------------
  21. ;General
  22.  
  23.   ;Name and file
  24.   Name "${PRODUCT_NAME}"
  25.   OutFile "${EXE_NAME}"
  26.  
  27.   ;Default installation folder
  28.   InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
  29.  
  30.   ;Get installation folder from registry if available
  31.   InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" ""
  32.  
  33. ;--------------------------------
  34. ;Interface Configuration
  35.  
  36.   !define MUI_HEADERIMAGE
  37.   !define MUI_HEADERIMAGE_BITMAP "proggy_header.bmp" ; optional
  38.  
  39. ;--------------------------------
  40. ;Variables
  41.  
  42.   Var MUI_TEMP
  43.   Var STARTMENU_FOLDER
  44.  
  45. ;--------------------------------
  46. ;Interface Settings
  47.  
  48.   !define MUI_ABORTWARNING
  49.  
  50. ;--------------------------------
  51. ;Pages
  52.  
  53.   !insertmacro MUI_PAGE_LICENSE "license.rtf"
  54.   !insertmacro MUI_PAGE_DIRECTORY
  55.  
  56.   ;Start Menu Folder Page Configuration
  57.   !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU"
  58.   !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${PRODUCT_NAME}"
  59.   !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
  60.   !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
  61.   !insertmacro MUI_PAGE_INSTFILES
  62.   !insertmacro MUI_UNPAGE_CONFIRM
  63.   !insertmacro MUI_UNPAGE_INSTFILES
  64.  
  65. ;--------------------------------
  66. ;Languages
  67.  
  68.   !insertmacro MUI_LANGUAGE "English"
  69.  
  70. ;--------------------------------
  71. ;Installer Sections     
  72. Section "install"
  73.  
  74. ;Add files
  75.   SetOutPath "$INSTDIR"
  76.  
  77.   File "${FOLDER}\${EXE_NAME}"
  78.   File "${FOLDER}\settings.ini"
  79.  
  80.   SetOutPath "$INSTDIR"
  81.   file /r ${FOLDER}\images
  82.   file /r ${FOLDER}\html
  83.   file /r ${FOLDER}\jre
  84.   file /r ${FOLDER}\lib
  85.   file ${FOLDER}\evaluation_license.txt
  86.   file ${FOLDER}\license.rtf
  87.  
  88. ;create desktop shortcut
  89.   CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${EXE_NAME}" ""
  90.  
  91. ;create start-menu items
  92.   CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
  93.   CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0
  94.   CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${EXE_NAME}" "" "$INSTDIR\${EXE_NAME}" 0
  95.  
  96. ;write uninstall information to the registry
  97.   WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${SHORT_NAME}" "DisplayName" "${SHORT_NAME} (remove only)"
  98.   WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${SHORT_NAME}" "UninstallString" "$INSTDIR\Uninstall.exe"
  99.  
  100.   WriteUninstaller "$INSTDIR\Uninstall.exe"
  101.  
  102. SectionEnd
  103. ;--------------------------------
  104. ;Uninstaller Section
  105.  
  106. Section "Uninstall"
  107.  
  108.   ;ADD YOUR OWN FILES HERE...
  109. ;Delete Files
  110.   RMDir /r "$INSTDIR\*.*"   
  111.  
  112. ;Remove the installation directory
  113.   RMDir "$INSTDIR"
  114.  
  115. ;Delete Start Menu Shortcuts
  116.   Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
  117.   Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*"
  118.   RmDir  "$SMPROGRAMS\${PRODUCT_NAME}"
  119.  
  120. ;Delete Uninstaller And Unistall Registry Entries
  121.   DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\${SHORT_NAME}"
  122.   DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SHORT_NAME}"
  123.  
  124.   Delete "$INSTDIR\Uninstall.exe"
  125.  
  126.   RMDir "$INSTDIR"
  127.  
  128.   !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP
  129.    
  130.   Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk"
  131.  
  132.   ;Delete empty start menu parent diretories
  133.   StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP"
  134.  
  135.   startMenuDeleteLoop:
  136.     ClearErrors
  137.     RMDir $MUI_TEMP
  138.     GetFullPathName $MUI_TEMP "$MUI_TEMP\.."
  139.    
  140.     IfErrors startMenuDeleteLoopDone
  141.  
  142.     StrCmp $MUI_TEMP $SMPROGRAMS startMenuDeleteLoopDone startMenuDeleteLoop
  143.   startMenuDeleteLoopDone:
  144.  
  145.   DeleteRegKey /ifempty HKCU "Software\${PRODUCT_NAME}"
  146.  
  147. SectionEnd
  148.  
  149. ;Function that calls a messagebox when installation finished correctly
  150. Function .onInstSuccess
  151.   MessageBox MB_OK "You have successfully installed ${PRODUCT_NAME}. Use the desktop icon to start the program."
  152. FunctionEnd
  153.  
  154. Function un.onUninstSuccess
  155.   MessageBox MB_OK "You have successfully uninstalled ${PRODUCT_NAME}."
  156. FunctionEnd

And the appropriate target:

XML:

  1. <target name="dist"  depends="exe" description="make installation package for distribution">
  2.    
  3.         <taskdef name="nsis" classname="net.sf.nsisant.Task">
  4.             <classpath location="nsisant-1.2.jar"/>
  5.         </taskdef>
  6.        
  7.         <nsis script="nsis/my_proggy.nsi" verbosity="4" out="nsis_build.log" noconfig="yes">
  8.             <define name="VERSION" value="1.0"/>
  9.             <define name="PRODUCT_NAME" value="My Proggy"/>
  10.             <define name="EXE_NAME" value="${ant.project.name}.exe"/>
  11.             <define name="SHORT_NAME" value="MyCoolProggy"/>
  12.             <define name="FOLDER" value="D:\launch4j\MyProggy"/>
  13.         </nsis>
  14.        
  15.         <copy file="${nsis}/${ant.project.name}.exe" tofile="${dist}/${ant.project.name}.exe"/>
  16.        
  17.         <delete file="${nsis}/${ant.project.name}.exe"/>
  18.        
  19.         <delete dir="${build}"/>
  20.         <delete dir="${html}"/>
  21.         <delete dir="${images}"/>
  22.         <delete dir="${lib}"/>
  23.        
  24.         <delete file="${ant.project.name}.jar"/>
  25.         <delete file="${ant.project.name}.exe"/>
  26.         <delete file="obfuscated_${ant.project.name}.jar" />
  27.        
  28.        
  29.     </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:

DOS:

  1. @echo off
  2. if "%ANT_HOME%"=="" goto noAntHome
  3. if "%JAVA_HOME%"=="" goto noJavaHome
  4.  
  5. call "%ANT_HOME%\bin\ant.bat" dist
  6. goto end
  7.  
  8. :noAntHome
  9. echo ANT_HOME environment variable is not set
  10. goto end
  11.  
  12. :noJavaHome
  13. echo JAVA_HOME environment variable is not set