Deploy java application JAR to Raspberry Pi using Maven and Eclipse

Software

Project (in Eclipse)

Create new Maven project or open a Java project and add Maven Nature to it.

When you add new dependencies to the project (in pom.xml), Eclipse automatically downloads all required libraries (in JAR files) into Maven repository and sets appropriate links to the files in the project build path.

Eclipse Maven dependenciesSo the application can be started directly from Eclipse in standard way: Use “Run as Java application” menu item for your main class.

We will not use “Export to runnable JAR file” function of the Eclipse to create a JAR for our application. We will create this file using Maven ‘install’ goal instead.

Use following plugin configuration for JAR manifest:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-jar-plugin</artifactId>
  	<version>2.6</version>
	<configuration>
		<archive>
			<manifest>
				<addClasspath>true</addClasspath>
				<classpathPrefix>lib/</classpathPrefix>
				<mainClass>com.gratchev.pi.scanboard.MainWindow</mainClass>
			</manifest>
		</archive>
	</configuration>
</plugin>
JAR plugin version may be important. For some configuration (old plugin versions) class path manifest parameters are being ignored and generated manifest doesn’t contain required information. See example below.

This plugin will generate JAR MANIFEST file in order to make our application JAR easily runnable:

  • ‘addClasspath’ parameter forces Maven to list all required libraries (JAR dependencies) in target JAR meta information, so it won’t be necessary to specify them in classpath when running our java application
  • ‘classpathPrefix’ is a string, which will be added (as prefix) to each JAR dependency, so we can place our required libraries in separate folder (relative to main application JAR file)
  • ‘mainClass’ parameter specifies, which runnable class (with ‘main’ method) is to be started when running JAR file

Here is an example of generated MANIFEST.MF (inside runnable JAR, in ‘META-INF’ folder):

Manifest-Version: 1.0
Built-By: Artem
Build-Jdk: 1.7.0_75
Class-Path: lib/pivot-wtk-terra-2.0.4.jar lib/pivot-wtk-2.0.4.jar lib/
 pivot-core-2.0.4.jar lib/pivot-charts-2.0.4.jar lib/guava-18.0.jar li
 b/slf4j-api-1.7.10.jar
Created-By: Apache Maven 3.2.1
Main-Class: com.gratchev.pi.scanboard.MainWindow
Archiver-Version: Plexus Archiver

Note, that dependencies in scope ‘provided’ will not be included into the list of JARs in manifest file. If you have such sub-dependencies, you can override the scope if you mention these dependencies in your top pom.xml with correct scope.

To run the application you need only the file name of main application JAR.

export DISPLAY=:0.0
java -jar java/Dashboard/Dashboard-0.0.1-SNAPSHOT.jar

How to deploy manually

  1. In Eclipse select all JAR files from ‘Maven Dependencies’ list in Project Navigator. Copy the selection into clipboard (using menu or keyboard shortcut)
  2. Using windows explorer:
    1. Create directory ‘lib’ anywhere
    2. In the directory, create new file ‘copy.bat’
    3. Open the file with text editor and paste the text from clipboard
    4. A list of full paths to all Maven dependencies JARs will be pasted into the file
    5. Add  ‘copy ‘ command to each line, before each JAR path
    6. Now the batch file contains commands to copy all required JAR to current directory
    7. Save and run this batch file ‘copy.bat’
    8. Directory must contain now all JARs, copied from local maven repository
  3. Start maven with goal ‘install’ to generate target JAR file
  4. Using WinSCP:
    1. Create new directory on Raspberry Pi for the application
    2. Copy application JAR file into this directory
    3. Copy complete ‘lib’ directory (all required JARs from Maven Dependencies) as subdirectory into application directory
  5. Now the Java application is ready to run on Raspberry Pi
  6. Use ‘java -jar’ command to run it (see above)
  7. Overwrite application JAR file if updating application code
  8. Overwrite/remove/add JAR files in ‘lib’ subdirectory if updating Maven dependencies

Automated deployment

Tools (frameworks) and ideas:

  • Ant task ‘scp’ (and ‘sshexec’)
    • Allows to copy generated JAR, and it’s dependencies (other JARs) over SCP (SSH connection) to Raspberry Pi directly
    • Use public key (for example, same as for WinSCP above) or password to authenticate
    • More complex file operation can be also performed (multiple file copy, directory creation, directory structure validation, etc.)
  • Maven plug-in ‘antrun’
    • Allows to execute ant scripts during Maven build phases
    • Use Maven environment variables to find out target JAR files paths
    • During ‘install’ phase, run Ant script above to deploy generated JARs to Raspberry Pi
  • Optionally
    • Detect if target program is already running on Pi
    • Shut down and restart program remotely on Pi using maven goals or profiles
    • Install program as service on Pi in order to run automatically at start up

Working example

Important! Target JAR file must be correctly generated. With all libraries referenced using ‘lib’ prefix. See above.
See also ‘Troubles’ section below.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-jar-plugin</artifactId>
	<configuration>
		<archive>
			<manifest>
				<addClasspath>true</addClasspath>
				<classpathPrefix>lib/</classpathPrefix>
				<mainClass>com.gratchev.pi.scanboard.MainWindow</mainClass>
			</manifest>
		</archive>
	</configuration>
</plugin>

Ant run plugin using SCP and SSHEXEC:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-antrun-plugin</artifactId>
	<version>1.8</version>
	<configuration>
		<tasks>
			<!-- https://maven.apache.org/plugins/maven-antrun-plugin/examples/classpaths.html -->
			<property name="compile_classpath" refid="maven.compile.classpath" />
			<property name="runtime_classpath" refid="maven.runtime.classpath" />
			<property name="test_classpath" refid="maven.test.classpath" />
			<property name="plugin_classpath" refid="maven.plugin.classpath" />

			<echo message="compile classpath: ${compile_classpath}" />
			<echo message="runtime classpath: ${runtime_classpath}" />
			<echo message="test classpath:    ${test_classpath}" />
			<echo message="plugin classpath:  ${plugin_classpath}" />

			<echo message="Copying runtime libraries to ${project.build.directory}/lib: ${runtime_classpath}" />

			<copy todir="${project.build.directory}/lib" flatten="true">
				<path path="${runtime_classpath}">
				</path>
			</copy>

			<echo message="Shut down remote application" />

			<sshexec command="pkill java" host="${pi.host}" username="${pi.user}" keyfile="${pi.publicKeyFile}"
				passphrase="" trust="true" failonerror="false"/>

			<!-- http://ant.apache.org/manual/Tasks/scp.html -->
			<scp todir="${pi.user}@${pi.host}:${pi.deployDirectory}/lib" keyfile="${pi.publicKeyFile}"
				passphrase="" trust="true" failonerror="true" verbose="true">
				<fileset dir="${project.build.directory}/lib"/>
			</scp>
			<scp file="${project.build.directory}/${project.build.finalName}.jar"
				todir="${pi.user}@${pi.host}:${pi.deployDirectory}" keyfile="${pi.publicKeyFile}"
				passphrase="" trust="true" failonerror="true" verbose="true">
			</scp>

			<sshexec command="export DISPLAY=:0.0;java -jar ${pi.deployDirectory}/${project.build.finalName}.jar >/dev/null 2>&amp;1 &amp; echo $!" host="${pi.host}" username="${pi.user}" keyfile="${pi.publicKeyFile}"
				passphrase="" trust="true" failonerror="true"/>
		</tasks>
	</configuration>
	<dependencies>
		<dependency>
			<groupId>org.apache.ant</groupId>
			<artifactId>ant-jsch</artifactId>
			<version>1.9.6</version>
		</dependency>
	</dependencies>
</plugin>

 

Details of ant task:

  • Extract list of all dependencies (JARs) for application runtime environment from Maven
    • See ‘runtime_classpath’ property. It contains a semicolon separated list of paths to all required JAR files in Maven repository.
  • Copy all library JAR files into local working ‘lib’ folder (normally under ‘target’ directory where all build files are being placed)
    • See ‘copy’ task with ‘path’
    • ‘path’ element does recognize Java Classpath-like file list (semicolon or colon separated)
    • ‘flatten’ attribute forces to copy all files into one directory without creating sub-folders.
  • Transfer all these JARs and target JAR to remote machine (Pi) using SCP into prepared deployment directory
    • See ‘scp’ tasks
    • First one uses ‘fileset’ element to copy all files from source folder
    • Second one copies one file by explicitly specified name (application JAR)
  • Execute Java remotely on Raspberry Pi
    • See ‘sshexec’ tasks
    • First task calls ‘pkill’ command to shut down (if active) a Java process. Assume, that our application is only Java program, which runs on Raspberry Pi. Otherwise, ‘pkill’ needs more parameters to distinguish different Java programs.
    • Second task starts Java with the copied application JAR and doesn’t wait for program exit (program stays running on Raspberry Pi).
      • Additionally a target display is being defined for program GUI
      • Last step – show process Id of the just started application (the Id can be used later on to stop the application)

Required configuration:

  • Deployment directory on Raspberry Pi
  • Private key file for Pi user

Troubles

SSH key exchange problem

After updating Raspbian release from Wheezy to Jessie, the deploy script couldn’t connect to Raspberry Pi over SSH anymore.
Following exception was thrown:

org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-antrun-plugin:1.8:run (default-cli) on project Dashboard: An Ant BuildException has occured: com.jcraft.jsch.JSchException: Algorithm negotiation fail
around Ant part ...<scp todir="pi@xx.xx.xx.xx:/home/pi/deployTest/lib" keyfile="F:\xx\xx.ppk" passphrase="" verbose="true" trust="true" failonerror="true">... @ 20:190 in F:\xx\build-main.xml
	at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:216)
	at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:153)
	at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:145)
	at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:116)
	at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:80)
	at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build(SingleThreadedBuilder.java:51)
	at org.apache.maven.lifecycle.internal.LifecycleStarter.execute(LifecycleStarter.java:128)
	at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:307)
	at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:193)
	at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:106)
	at org.apache.maven.cli.MavenCli.execute(MavenCli.java:862)
	at org.apache.maven.cli.MavenCli.doMain(MavenCli.java:286)
	at org.apache.maven.cli.MavenCli.main(MavenCli.java:197)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced(Launcher.java:289)
	at org.codehaus.plexus.classworlds.launcher.Launcher.launch(Launcher.java:229)
	at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(Launcher.java:415)
	at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:356)
Caused by: org.apache.maven.plugin.MojoExecutionException: An Ant BuildException has occured: com.jcraft.jsch.JSchException: Algorithm negotiation fail
around Ant part ...<scp todir="pi@xx.xx.xx.xx:/home/pi/deployTest/lib" keyfile="F:\xx\xx.ppk" passphrase="" verbose="true" trust="true" failonerror="true">... @ 20:190 in F:\xx\build-main.xml
	at org.apache.maven.plugin.antrun.AntRunMojo.execute(AntRunMojo.java:342)
	at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:134)
	at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:208)
	... 20 more
Caused by: F:\Projects\pi\Dashboard\target\antrun\build-main.xml:20: com.jcraft.jsch.JSchException: Algorithm negotiation fail
	at org.apache.tools.ant.taskdefs.optional.ssh.Scp.execute(Scp.java:277)
	at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:292)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:106)
	at org.apache.tools.ant.Task.perform(Task.java:348)
	at org.apache.tools.ant.Target.execute(Target.java:435)
	at org.apache.tools.ant.Target.performTasks(Target.java:456)
	at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1393)
	at org.apache.tools.ant.Project.executeTarget(Project.java:1364)
	at org.apache.maven.plugin.antrun.AntRunMojo.execute(AntRunMojo.java:313)
	... 22 more
Caused by: com.jcraft.jsch.JSchException: Algorithm negotiation fail
	at com.jcraft.jsch.Session.receive_kexinit(Session.java:582)
	at com.jcraft.jsch.Session.connect(Session.java:320)
	at com.jcraft.jsch.Session.connect(Session.java:183)
	at org.apache.tools.ant.taskdefs.optional.ssh.SSHBase.openSession(SSHBase.java:273)
	at org.apache.tools.ant.taskdefs.optional.ssh.Scp.upload(Scp.java:339)
	at org.apache.tools.ant.taskdefs.optional.ssh.Scp.execute(Scp.java:256)
	... 34 more

The problem is caused by incompatibility of JSCH library SSH connector with new Linux security policy. Some SSH key exchange algorithms were disabled (due to security reasons) in Jessie release.
See workarounds on stackoverflow:

I have enabled missing algorithms on Raspberry’s SSH daemon. Added following code to /etc/ssh/sshd_config (as root):

...

# See http://stackoverflow.com/questions/26424621/algorithm-negotiation-fail-ssh-in-jenkins
KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1

Then restarted sshd service.