/apr 13, 2015

Distribute Your Java App Via Brew

By Jason Nichols

For developers of desktop based applications, making sure your users can easily update to the latest version can be a pain. Expecting them to fetch out updates by going to your website only adds layers of complexity and slows adoption due to inertia. For SRC:CLR and our customers, we felt this pain as well. But thanks to Homebrew, sending our users URL links to download and install our applications is a thing of the past (at least for OS X users).

This post will discuss how to get your Java application configured to be installed via Homebrew.

Why Homebrew

Homebrew bills itself as the missing package manager for OS X, and it's a duty that it fulfills superbly. With Brew installed, installing a new application is as simple as:

brew install gimp

And boom! GIMP is now up and running. Let's walk through how to make this happen for your Java app. Remember, this really applies to user tools such as console and Swing/JavaFX based applications.

Using Taps

HomeBrew is based on the concept of Formulas, which are nothing more than instructions for installing a particular software application. Formulas are grouped together to form a Tap. Think of a Tap as a repository site, the Homebrew project has its official (built in) Tap at https://github.com/Homebrew/homebrew/tree/master/Library/Formula.

For our application, we're going to use Github to host our own Tap. This is because closed-source applications that aren't built by source as part of the installation process are not permitted under the Homebrew Acceptable Formulae Policy.

But have no fear! Creating a tap is easy. A Tap is nothing more than a Github repository with the name homebrew-<tapName> containing the Formula you wish to offer to users. Not attempting to use the default Homebrew Tap has several advantages:

  1. You don't have to rely on the Homebrew team accepting a pull request just to get your new formula published.
  2. You don't have to abide by third party rules for what application types you can and can't publish.

Installing a custom Tap into brew is extremely simple for users. Assume I have a repository at https://github.com/kickroot/homebrew-kickroot, installing the tap is done by running:

> brew tap kickroot/kickroot

And boom! You can install any Formulas defined in your tap just as any other:

> brew install kickroots-widget-maker-pro

Prepping your application

There are a few housekeeping requirements for your Java application that will make it considerably easier to setup via Homebrew. For these I'll use our SRC:CLR Console application as an example.

Application Configuration

Make sure your conf files for the application are stored in a well defined location (not relative to the application install location). Given that Homebrew applications install by default into /usr/local/Cellar, your user won't have write access and you probably don't want to share a single config between all users on the system anyway.

Logging

Logging to a logs/ folder relative to the application's jar file is no longer possible. For our apps, we log to the user's home folder in a dotfile, which keeps it out of the way, yet is easily retrieved for customer support purposes. Logback makes this a simple configuration option if you use a logback.xml file:

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${user.home}/.srcclr_console.log</file>
<append>false</append>
<encoder>
  <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>

Logback will automatically replace ${user.home} with the Java System Property "user.home".

Use an executable uber-Jar to keep things simple.

The Maven Shade plugin will do this. Below is our configuration with some customizations to allow Spring to function properly as a shaded jar:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.3</version>
    <executions>
      <execution>
        <phase>package</phase>
        <goals>
          <goal>shade</goal>
        </goals>
        <configuration>
          <transformers>
            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
              <mainClass>com.sourceclear.console.Console</mainClass>
            </transformer>
            <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
              <resource>META-INF/spring.handlers</resource>
            </transformer>
            <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
              <resource>META-INF/spring.schemas</resource>
            </transformer>
          </transformers>
        </configuration>
      </execution>
    </executions>
</plugin>

This jar is known as a 'fat jar', since it contains the classes of all its dependencies in addition to our own application classes. This simplifies distribution. Now you can run the application manually by doing java -jar myapp.jar.

Modify your assembly (zip/tar.gz/whatever) to simplify your Homebrew formula

With the SRC:CLR Console, I strove to make the zip file we distribute to our users the same zip that Homebrew uses for installation purposes. This zip file is automatically generated as part of the Maven Assembly plugin. Our distribution file is arranged as follows:

|-brew
  |--srcclr
|-srcclr
|-srcclr-console.jar
|-LICENSE, eulas, etc

Let's start with the srcclr file at the root of the zip file:

#!/bin/bash
# Runs the SourceClear Console
java -jar ##PREFIX##/srcclr-console.jar "$@"

This is just simple shell script that runs the console jar. We also have a srcclr file within the brew folder. This one is used by the brew formula and is similar:

#!/bin/bash
# Runs the SourceClear Console
java -jar ##PREFIX##/srcclr-console.jar "$@"

We have the ##PREFIX variable, which our formula will replace with the full path. This path is configurable by the end user so we can't hard code it into the script.

Formula Creation

With our new zip distribution file, we're ready to set up our Homebrew formula! Formulas use their file name as the formula name with an .rb extension. So here's our srcclr.rb formula:

require "formula"

class Srcclr < Formula
  homepage "https://srcclr.com"
  url "https://download.sourceclear.com/console/srcclr-console-0.7.0.zip"
  sha256 "6fdae5ff1adf92c5f99821cf1aceae8144686bf71350ccf0bbabb526da14de10"

  def install
      inreplace "brew/srcclr", "##PREFIX##", "#{prefix}"
      prefix.install "srcclr-console.jar"
      bin.install "brew/srcclr"
  end
end

Let's break this down by line number:

 

Line 9 replaces the hard-coded ##PREFIX## with the path of Brew's Cellar, allowing us to properly hard code the executable path to srcclr at install time. Line 10 places the jar file in its designated spot (currently in /usr/local/Cellar/srccclr<version>/). Line 11 install our executable script that we just modifed into /usr/local/bin as a symlink to /usr/local/Cellar/srcclr/0.7.0/bin/srcclr.

  • 1 - Include the required Formula class
  • 3 - All Formulas inherit from Formula
  • 4 - Your company URL
  • 5 - The actual URL of the assembly download
  • 6 - Always use a hash! On OS X you can get the SHA-2 256 value by running shasum -a 256 <myFile>
  • Lines 8-12 are the code run during the install phase

 

Push Code

Assuming we publish our Github repo and upload our distribution package, we can now run the SRC:CLR console with the following commands:

> brew tap srcclr/srcclr
> brew install srcclr
> srcclr

Upgrades are equally easy for the end user:

> brew update
> brew upgrade srcclr

With this in place, both your users and Product Manager will thank you!

Related Posts

By Jason Nichols