Buscar este blog

sábado, 26 de diciembre de 2015

Java - Bundle JRE inside executable file - Launch4j and Inno Setup

This is the first of a serie of two posts about how to launch a desktop java application from a web browser. In this case I´ll work in a desktop app because it has to be able to access local resources in the user PC (for example, applets could do this, but they are far deprecated).

The requirements of the desktop application are as follow:
  1. It must be as isolated as be possible, i.e., it must not depend on the user JRE.
  2. It must be easy to install.
  3. It must be invoked from a web application, i.e, from a web browser.
  4. It must be easily upgradeable.

In this post I´ll explain how to convert a desktop java application in a exe file for windows, and how
to put a embedded JRE inside of it.

All java app needs a Java Runtime Environment in which it´s executed. JRE is a free software you can download from Oracle site and it is used to execute java applications, i.e, jars. But one user only can have one default JRE, and this might not be the last release or might not be the one you need.
In this cases there are two solutions, the obvious is to force the user to use the JRE you need, which imply upgrade/downgrade to the required version. In much cases this is not a valid option because the new JRE may break other applications.
So the recommended solution is that the application be shipped with its own JRE, and this one will be used only by this app. In order to achieve this we will need two programs:
  1. Launch4j. To put it simple, tis is a exe wrapper. You have a jar file and conver it to a exe file for windows with a specifyc JRE.
  2. Inno Setup. This is a installer builder. You have a exe file and some aditional resources, and you get a new full installer wizard.

Step 1, the desktop app

In this posts I´ll use a very simple app comprised of two files.

Main:
package es.sisifo.desktop;

import java.io.IOException;

public class Main {
 public static void main(String[] args) throws IOException {
  String[] textosAMostrar;
  if (args == null || args.length == 0) {
   textosAMostrar = new String[]{"No se han recibido argumentos"};
  }
  else {
   textosAMostrar = args;
  }
  
  SimpleJFrame simpleJFrame = new SimpleJFrame(textosAMostrar);
  simpleJFrame.setVisible(true);
 }
}

SimpleJFrame:
package es.sisifo.desktop;

import java.awt.GridLayout;
import java.io.IOException;

import javax.swing.JFrame;
import javax.swing.JLabel;

public class SimpleJFrame extends JFrame {
 private static final long serialVersionUID = -1153732581002339155L;

 public SimpleJFrame(String[] textosAMostrar) throws IOException {
  super("Simple App");
  setLayout(new GridLayout(textosAMostrar.length, 1));
  for(int i=0; i<textosAMostrar.length; i++) {
   JLabel texto = new JLabel("Texto " + i + ": " + textosAMostrar[i]);
   add(texto);
  }
  
  setBounds(500, 250, 300, 250);

  setResizable(true);
  setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 }
}

And, of course, the POM.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>es.sisifo</groupId>
 <artifactId>desktop-1</artifactId>
 <version>0.0.1-SNAPSHOT</version>

 <build>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.3</version>
    <configuration>
     <source>1.8</source>
     <target>1.8</target>
    </configuration>
   </plugin>


   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>2.6</version>
    <executions>
     <execution>
      <id>make-assembly</id>
      <phase>package</phase>
      <goals>
       <goal>single</goal>
      </goals>
      <configuration>
       <archive>
        <manifest>
         <mainClass>es.sisifo.desktop.Main</mainClass>
        </manifest>
        <manifestEntries>
         <Permissions>all-permissions</Permissions>
         <Application-Name>Example Desktop app</Application-Name>        
         <Built-By>Sisifo</Built-By>
        </manifestEntries>
       </archive>
       <descriptorRefs>
        <descriptorRef>jar-with-dependencies</descriptorRef>
       </descriptorRefs>
      </configuration>
     </execution>
    </executions>
   </plugin>
  </plugins>
 </build>
</project>

This app just receives a list of arguments a prints them in  JFrame.
In the pom.xml file, you specify to build the whole app in a single jar and that the main class (i.e. the default entry point) is es.sisifo.desktop.Main.

Step 2, build the exe

You need to download Launch4j. This is a standalone app.

There are a lot of configuration options to create your exe, but in this case we will need just to specify three things:
  1. The output exe file. This will be the exe generated by this program.
  2. The input jar. This is the desktop app created in step one.
  3. The JRE path. This is the bundled JRE the app will use to run.

The important part here is the JRE path. The app must not depend on the user JRE, so it will carry its own JRE. In this case I downloaded the 32 bits JRE from oracle and unzziped it in a bin folder.
I chose to use a 32 bits JRE because I don´t know if the final users will have a 64 bits system.

This is my folder structure:
  • work folder (any folder)
    • bin
      • jre1.8.0_66 (the JRE folder)
      • desktop-1-0.0.1-SNAPSHOT-jar-with-dependencies.jar (the app jar)



Note that the "Bundled JRE path", it is a relative path. You can not set an absolute path here, because you don´t know where the JRE will be in the final user PC.

Finally, you save your configuration file (I named it as "desktop.xml") and you build the exe by clicking in the gear icon.


Ok, now you have a exe file which launch your java app. But if you want to distribute this program as is, you have to ship the the exe file, the jar file and the JRE folder too. The final user will have to create a work folder, anywhere she wish, but this folder must have the structure presented previously, otherwise the exe will not found the jar or the JRE. Cumbersome.

Step 3, build the installer

Lets make the user life more ease and create a windows installer. You need to download Inno Setup.

Inno Setup is configured upon a script file. You can write your script manually, but it has a very useful script wizard, so you will not need to write any code.

Before we start with the installer, you need to reorganize your work folder. You need to create a new folder, for example resources, and put the JRE and the jar inside it. The result is as follow:
  • work folder (any folder)
    • resources
      • bin
        • jre1.8.0_66 (the JRE folder)
        • desktop-1-0.0.1-SNAPSHOT-jar-with-dependencies.jar (the app jar)
    • desktopApp.exe
As you can see next, you need a resources folder in order Inno Setup take all its content and package it inside the installer.


Now, Inno Setup. The first time you run Inno Setup you will see a screen to create your own script or build one from the wizard, In this case we will use the last one.

The wizard is quite simple, so most of the time you can press next without any further configuration.

The result is a classical exe installer placed in a output folder. When you run this installer a wizard will be launched with the options you configured previously. The result is a the new app installed in your system.



At this point you have a distributable installer of about 40 MB that you can send to any windows user, and you can be sure your app will work on its PC.


In the next post I will show how to make this application is launched from a web browser.

3 comentarios:

  1. I've stuck on the step No2: Launch4j compiles resources, links, wraps, sends warning message to enforce me to sign the executable.
    But generated .exe does nothing.

    Launch4j ver. 3.12
    JRE-1.8.0_221

    ResponderEliminar
  2. The problem is solved:
    issue was in manifest file.
    To whom probably it may concern:

    in generated .jar file find MANIFEST.MF and add stroke:
    Main-Class: my.package.MainClass

    pay attention to:
    1. Minimum one blank stroke must be in the end of manifest:

    Manifest-Version: 1.0
    Created-By: 1.8.0_221 (Oracle Corporation)
    Main-Class: MetricConverter
    (empty line)

    2.Do not add MainClass's .class exctention:

    Manifest-Version: 1.0
    Created-By: 1.8.0_221 (Oracle Corporation)
    Main-Class: MetricConverter.class //Wrong!
    (empty line)

    ResponderEliminar