Friends of OpenJDK Today

Creating a JavaFX World Clock from Scratch (Part 4)

April 09, 2021


  • Carl Dea

    Carl Dea is a Senior Developer Advocate at Azul. He has authored Java books and has been developing software for 20+ years with many clients, from Fortune 500 companies to ... Learn more

"The tools we use have a profound (and devious!) influence on our thinking habits, and, therefore, on our thinking abilities." -- Edsger W. Dijkstra, 18 June 1975

JFX World Clock & JPackager

Hello and welcome to Part 4 of a series of blog entries on how I created a "sci-fi" looking world clock using JavaFX. If you are new to this series you can visit Part 12, & 3.

If you’ve been following me to this point take a moment to stand up, breath, take a bow and then pat yourself on the back. You are more than half way through the series.

In Part 4 I will be fast forwarding my progress of the JFX World Clock and jump right into how to build and create an installer that you can distribute. I will be using a new Java build tool called Bach by Christian Stein @sormuras. Later on, I will also show you my original build approach using the Maven build tool.

For the impatient go to check out the on the simple steps to build the app at


Install your favorite OpenJDK distribution.

  • JDK 16+ (with JavaFX)
  • Maven 3.6.3+ (optional)
  • SDKMan (optional)

Installing Java 16+ (with JavaFX)

Out of convenience I have been using the SDKMAN for easy installs and being able to switch between versions of the JDK on the command prompt (terminal).

Below I’m using SDKMan to install Azul’s Zulu (OpenJDK) distro containing the JavaFX modules.

$ sdk install java 16.0.0.fx-zulu
$ sdk use java 16.0.0.fx-zulu

For see more information on newer Java distros go to:

Clone JFX World Clock repo

Assuming you have Java 16 or greater installed do the following:

Linux/Mac OS

$ mkdir -p ~/projects && cd ~/projects
$ git clone
$ cd worldclock

Windows (Power shell)

# C:\Users\username\Documents
> cd ~/documents
> md projects
> cd projects
> git clone
> cd worldclock

Updating an upstream Repo

Whenever I make a change to the JFX World Clock repo your locally cloned repo can be stale (out of date). So, every now and then you could update from the upstream repo (mine). Below, is how to add the original repo as a remote upstream branch so you can do a pull to get the latest.

$ git remote add upstream
$ git remote -v
$ git pull upstream main

If you ever want to suggest a fix or change code locally (assuming you’ve forked), you’ll want to do a rebase, which will put your changes on top of the latest. That way you could create a pull request if you so choose. 😉

So, let’s build the JFX World Clock to be run.

Bach Build

Bach is a build tool that uses executable tools that comes with your installed Java 16+ JDK such as javac, jar, jlink, etc. Bach is a lightweight build tool but does the heavy lifting for you when building a Java based modular application.

After cloning the project, and sitting in the worldclock directory you can execute Bach’s build command as shown below:

$ ./bach/bin/bach build

You’re probably wondering, “I don’t remember installing Bach!”, Where did it come from? Well it was checked into the world clock git repo project. A little more on that later, but for now it just works when you are sitting in the worldclock directory.

What’s really cool about Bach is that you don’t need to use build tools like Maven or Gradle to handle dependencies and to generate artifacts such as jar, exe, dmg, etc.

So, what’s the catch? No catch, well except one thing…You gotta go modular! Bach is really designed for Java projects that are pure modular apps and libraries.

While there are ways to use non-modular libraries designed for the Java class path, it is NOT advisable (or not worth the time). What will happen is that you can get into weird mismatch problems within your IDE when building and running the application (inside the IDE) as opposed to being on the command line.

Tip: A word to the wise: Once you use Java modules, don’t look back. Meaning, you’ll need to look for a suitable modular library (equivalent) to replace your non-modular library. Also you might want to ask or encourage library owners to create a modular version (buy them a beverage). In the case with JFX World Clock I used the Jackson library's modular version for serialization (JSON).

Bach, is still relatively new and is always getting updates with new features, so keep an eye over at:

Oh, and let Christian know that Carl sent ya. 😉

Out of convenience you can set-up your PATH environment variable so you can perform the following (without having to fully qualify the path):

$ bach build

Setup Environment Variables

After building the world clock with the above command you will inevitably perform builds often, so let’s make it more convenient by setting your PATH environment.

Add to your .bashrc or .bash_profile as the following:

# Linux / MacOS
export PATH=$PATH:./bach/bin

On Windows you'll add to your environment variables as the following:

# Windows
set PATH=%PATH%;.bach\bin

Learning from Bach

After calling the bach build command you should see the following:

cdea$ bach build
Bach 17-ea+ce2b495 (Java 16+36, Mac OS X, /Users/cdea/projects/worldclock)
Build worldclock 17-bach
Verify external modules located in file:///Users/cdea/projects/worldclock/.bach/external-modules/
Verified 3 external modules
Build 1 main module: worldclock
  javac    --module worldclock --module-version 17-bach --module-source-path worldclock=src/main/java --module-path .bach/e[...]
  jar      --create --file .bach/workspace/modules/[email protected] -C .bach/workspace/classes-main-16/worldclock . -[...]
Assemble custom runtime image
  jlink    --add-modules worldclock --module-path .bach/workspace/modules:.bach/external-modules --launcher worldclock=worl[...]
Build took 3.739s
Logbook written to file:///Users/cdea/projects/worldclock/.bach/workspace/

Above you’ll notice Bach 17-ea+ce2b495, which is the version & build of Bach. It's neat to also know that Bach builds itself with Bach (yeah, I know right?!). This is nice to know whenever Bach gets new features or bug fixes, I can simply perform the following:

$ bach init 17-ea

I believe once Bach is GA (general availability) it’ll be bach init 17. Bach’s binaries (itself), resides in the .bach/bin directory. As the owner of the JFX World Clock repo(project), I would need to check-in these Bach binary files. This is why Bach is available without having to install Bach. Like other build tools I want to make sure I can exclude the build artifact such that they don’t get pushed in Git. But most importantly check-in these Bach binaries in as .bach/bin/.

I needed to ensure a few things in the .bach/.gitignore file I created. I made sure to exclude the /workspace/ directory and *.jar files that are generated from the build process. However, I do NOT exclude the .bach/bin directory as shown below:


$ cat .bach/.gitignore

Continuing our look at the above output of the command bach build you’ll see the steps that Bach had performed to build the JFX World Clock as a modular app. You’ll notice it used javac to compile, jar to create the module, and jlink to build a custom image of Java runtime. Lastly in the last line outputted is further details in a log file in a file called

Build took 3.739s
Logbook written to file:///Users/cdea/projects/worldclock/.bach/workspace/

Running JFX World Clock as a Module

After the application is built let’s run it as a Java module as shown below:

java --add-modules worldclock --module-path .bach/workspace/modules/:.bach/external-modules/ com.carlfx.worldclock.Launcher

To run an executable on the command line do the following:

# Linux/MacOS
$ .bach/workspace/image/bin/worldclock

# Windows 
$ .bach\workspace\image\bin\worldclock

How does Bach work?

To allow Bach to infer my project’s build intent I have to create a special Java file located at .bach/ Since it’s pure Java your IDE will be able to assist and warn if the file is invalid. The following is JFX World Clock’s Bach info definition:


    version = "17-bach",
    lookupExternals = @Externals(name = Name.JAVAFX, version = "16"),
    tools = @Tools(limit = {"javac", "jar", "jlink"}),
    tweaks = {
          tool = "jlink",
          option = "--launcher",
          value = "worldclock=worldclock/com.carlfx.worldclock.Launcher")
    lookupExternal = {
            @External(module = "com.fasterxml.jackson.core", via = "com.fasterxml.jackson.core:jackson-core:2.12.1"),
            @External(module = "com.fasterxml.jackson.annotation", via = "com.fasterxml.jackson.core:jackson-annotations:2.12.1"),
            @External(module = "com.fasterxml.jackson.databind", via = "com.fasterxml.jackson.core:jackson-databind:2.12.1")
module {
  requires com.github.sormuras.bach;

Bach’s API is in the form of Java annotations. A Bach annotation called @ProjectInfo allows me to specify what JDK tools to use and the project's external module dependencies. Any transitive dependencies would be resolved and placed into the external-modules directory. There are other attributes that I’m still learning about and so by the time you read this the documentation should be updated at Bach’s site ;-).

Now that you know how to build a modular app using Bach let’s create an installer to distribute to others.

Creating an Installer

Wouldn’t it be nice to distribute your application that is packaged into an installer program such as dmg, msi, or rpm?

Now you can.


New to Java 16 or greater contains the new and improved version of the jpackage application that lets you create native installers for your specific OS platform. Bach has plans to support this capability in the future but it doesn’t mean we can’t create one ourselves.

Since, Bach has already built my project using jlink, the jpackage tool is now able to create a native installer such as a dmg, pkg, msi, exe, file. Once packaged the distro contains the application and the custom Java runtime image (JRE 16) along with modules. When the end user double clicks the installer (dmg) you will get instructions to install the application.

To run jpackage, do the following:

$ jpackage --verbose \
      --name "JFX World Clock" \
      --description "JavaFX World Clock Application" \
      --vendor "Carl Dea" \
      --runtime-image .bach/workspace/image \
      --module worldclock/com.carlfx.worldclock.Launcher \
      --dest .bach/workspace/package

After, it is completed the output of the distro would be in the .bach/workspace/package directory as shown below:

JFX World Clock dmg

JFX World Clock dmg

On MacOS, it created a dmg, and the user would double click on the file and popup the install dialog as shown below:

JFX World Clock & JPackager

JFX World Clock & JPackage

Well, there you have it, a nice distributable application you can give to friends and relatives. There’s a lot more that I haven’t discussed, but this should be enough to get you started.

Topics not discussed regarding jpackage following:

  • Launchers
  • Signing Apps
  • Custom Icons
  • License File
  • Overriding resources

Maven Build and Run

This project originally used Maven to build the project. While Maven and Gradle are often the de-facto standard when it comes to building apps, I only went as far as building it and running it as a modular app in development. I did not use Maven to build a custom image and installer.

To run the JFX World Clock run the following:

$ mvn clean
$ mvn javafx:run


In Part 4, instead of working on the other features of the JFX World Clock I wanted to show you how to build the app using the Bach build tool and also how to use the jpackage tool from JDK 16 to create a native installer to be distributed. Lastly, I showed you how to build and run the app using Maven.

Next, in Part 5 we’ll be looking at Maps.

Happy coding!



  • Carl Dea

    Carl Dea is a Senior Developer Advocate at Azul. He has authored Java books and has been developing software for 20+ years with many clients, from Fortune 500 companies to ... Learn more

Comments (0)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:
Copied to the clipboard