Basics

Guides

API Reference

Menu

Basics

Guides

API Reference

Java Build Tools Usage Guide

TL;DR

An Aussom library that builds a Java project - compile, jar, javadoc, resolve dependencies, and native image - from a short build.auss script. For a standard project, that script is three lines:

include javabuild;

// (command-line args, sources path, make the jar executable)
runner = new BuildRunner(args, "src/main/java", true);
runner.run();

Then run abld in that directory to build it. See Getting Started: The Three-Line Build for the walk-through.

Overview

The Java Build Tools let you build a Java project from Aussom. They turn the everyday Java build steps - compile the sources, package a jar, generate Javadoc, resolve Maven dependencies, and produce a native app image - into a small set of Aussom objects you create and call. A standard project builds from a build.auss script only a few lines long; a project with special needs drops down to the same objects and wires the steps by hand.

This guide starts with the everyday case: a short build.auss and the commands to run it. It then builds up to dependencies, native images, custom build steps, and finally the low-level JavaTools calls the whole system sits on. The first half is what most projects need; the second half is reference material for when the easy path is not enough.

The tools are a library, not a framework. Nothing takes over your script. You create a runner, hand it the facts about your project, and call run(). Everything else is a normal Aussom object you can inspect and extend.

What It Is Good For

  • building a standard Java project (sources in, jar out) from a few lines
  • building modular projects (a module-info.java) with no extra setup
  • reading build options off the command line, so build.auss -c -p works
  • skipping steps whose inputs have not changed since the last build
  • resolving Maven dependencies and putting them on the compile classpath
  • installing your jar into the local Maven repository for other projects to use
  • producing a native app image or installer with jpackage
  • adding your own build steps alongside the standard ones
  • calling javac, jar, javadoc, jlink, and jpackage directly when you want full control

The Pieces

The system has a few parts. You will use the top two most of the time and rarely need to think about the rest.

Piece What it is
BuildRunner The build entry point. It reads command-line flags, holds the build steps, and runs them. This is what a build.auss creates.
build.auss Your project's build script. It creates a BuildRunner and calls run(). It runs in script mode (top-level statements, no class).
abld A command that finds and runs the build.auss in the current directory, forwarding your arguments. A shorter way to run a build.
Actions The build steps a run performs: clean, compile, package, docs, install, image, all. Flags select them.
BuildTask The base class for one build step. The standard steps extend it, and so can you for a custom step.
Standard tasks The built-in steps: resolve, compile, jar, javadoc, jpackage, install. The runner builds and wires these for you.
BuildLog The progress display. It shows a colored terminal UI when the output is a terminal and plain lines when it is piped.
JavaTools The low-level Java API underneath: one call each for compile, jar, javadoc, resolve, jlink, jpackage, and install.

The javabuild module gives you BuildRunner, BuildTask, the standard tasks, and BuildLog. It pulls in the javatools module, which gives you JavaTools and the option-builder classes. Including javabuild gets you everything:

include javabuild;

The Standard Project Layout

The easy path assumes a conventional project:

myapp/
  build.auss          # your build script
  src/main/java/      # your Java sources
  build/              # created by the build; safe to delete
    classes/          # compiled .class files
    dist/myapp.jar    # the packaged jar
    docs/             # generated Javadoc
    image/            # the native app image

The output name (myapp above) comes from the project directory name. Everything under build/ is generated, so clean can remove it and you can .gitignore it.

Getting Started: The Three-Line Build

For a standard project, the whole build.auss is three lines:

include javabuild;

// (command-line args, sources path, make the jar executable)
runner = new BuildRunner(args, "src/main/java", true);
runner.run();

That is a complete build script. args is the command-line argument list, which Aussom binds for you in script mode. The runner reads it to pick which action to run, builds the standard steps for a project rooted at src/main/java, and runs them.

The third argument, true, makes the jar executable: it gets a Main-Class header and the resolved dependencies bundled in, so the jar runs on its own. Pass false for a plain library jar.

Running the Build

There are three ways to run it, all equivalent:

abld                 # finds build.auss in this directory and runs it
./build.auss         # run the script directly
aussom build.auss    # run it through the aussom CLI

With no flags, the build runs the default package action: resolve dependencies, compile, and build the jar. When there is no build.auss in the current directory, abld prints build.auss not found in the current directory. and stops.

Choosing What to Build: Actions and Flags

Flags on the command line pick which action runs. Each flag has a short and a long form:

Flag Action What it does
-c / --clean clean Remove the build outputs.
-cp / --compile compile Resolve and compile only.
-p / --package package Resolve, compile, and build the jar. This is the default.
-d / --docs docs Resolve and generate Javadoc.
-i / --install install Build and install the jar into the local Maven repository.
-im / --image image Resolve, compile, jar, and build a native app image.
-a / --all all Package, docs, and image together.
-h / --help - Print the flag list and stop.

All short flags are lower case on purpose, so there is no -c versus -C confusion. Some short flags are two letters (-cp, -im) to stay distinct: -i is install, the common operation, and -im is image.

abld           # default: package
abld -cp       # compile only
abld -d        # docs only
abld -i        # build and install into the local Maven repo
abld -im       # build the native app image
abld -a        # package, docs, and image

Flag Order Never Matters

You can pass flags in any order and get the same build. This is different from mvn clean package, where order matters. The flags select a set of steps, and the runner always runs them in one correct order:

abld -c -p     # clean, then package
abld -p -c     # exactly the same: clean, then package

clean always runs first when it is selected. If clean is the only flag, the build cleans and stops:

abld -c        # clean only, no build

When you pass any action flag, exactly those actions run. The default package applies only when you pass no action flag at all.

Note that -cp (compile) is one flag, not -c -p clustered together. -c -p is clean plus package; -cp is compile.

Adding Dependencies and a Main Class

Most real projects need Maven dependencies and an entry point. Dependencies are a list of group:artifact:version coordinates. You can pass them as the fourth constructor argument or set them afterward; both do the same thing.

include javabuild;

runner = new BuildRunner(args, "src/main/java", true,
                         ["com.google.guava:guava:33.0.0-jre"]);
runner.setMainClass("com.example.Main");
runner.run();

The same build, written with setters:

include javabuild;

runner = new BuildRunner(args, "src/main/java", true);
runner.setDeps(["com.google.guava:guava:33.0.0-jre",
                "org.apache.commons:commons-lang3:3.14.0"]);
runner.setMainClass("com.example.Main");
runner.run();

setDeps feeds both the dependency resolver and the compile classpath, so your code compiles against the libraries and (for an executable jar) ships with them. setMainClass sets the jar's entry point and the app image's launcher class.

This is the boundary of the easy path: a standard Java build expressed as one constructor call and two setters. The library builds and orders the steps.

Watching the Build: Progress Output

The runner reports progress as it goes. It picks the display automatically:

  • In a terminal, it shows a colored progress line. Each step displays a live [2/5] compile ... line while it runs, replaced by a colored result when it finishes (green done, dim skipped, red failed), and a closing Build succeeded or Build failed.
  • When output is piped to a file or another program, it prints plain one-line-per-step logging with no color, so log files and CI output stay clean:
[1/3] compile ... done
[2/3] jar ... done
[3/3] jpackage ... done
Build succeeded (3 tasks, 0 skipped)

You can force either style. Add --plain to force plain logging (handy in a script even when you have a terminal) or --tui to force the terminal UI:

abld -p --plain
abld -p --tui

Incremental Builds: Skipping Unchanged Work

Every standard step is incremental. Before a step runs, the runner checks whether the step's outputs are newer than its inputs. If nothing changed, the step is skipped and reported as skipped. Run a build twice in a row and the second run does almost no work:

[1/3] resolve ... skipped
[2/3] compile ... skipped
[3/3] jar ... skipped
Build succeeded (3 tasks, 3 skipped)

Change one source file and only the steps that depend on it run again. You do not configure this; it is on by default. A clean (or abld -c) removes the outputs so the next build runs every step from scratch.

Modular Projects

If your project is a Java module - it has a module-info.java under the source path - the build detects that and puts the dependencies on the compile module path instead of the class path, so javac can resolve your module's requires. You do not configure anything; it is automatic:

include javabuild;

// A modular JavaFX library. Modular mode is auto-detected from
// src/main/java/module-info.java.
runner = new BuildRunner(args, "src/main/java", false,
                         ["org.openjfx:javafx-controls:23.0.1"]);
runner.run();

To override the detection in the rare case it guesses wrong, call setModular(true) or setModular(false). Note this is about your code: a non-modular project that merely depends on modular libraries needs no modular mode - those dependencies compile fine on the class path.

Installing to the Local Maven Repository

The install action (-i) builds the jar and installs it into your local Maven repository (~/.m2), so other projects on the same machine can resolve it as a dependency. It installs the jar, a generated POM, a sources jar, and a javadoc jar under the coordinates you set.

Install needs the Maven coordinates - the groupId, artifactId, and version - which you set with setCoordinates:

include javabuild;

runner = new BuildRunner(args, "src/main/java", false,
                         ["eu.hansolo.fx:countries:21.0.19"]);
runner.setCoordinates("com.example", "widgets", "1.4.2");
runner.run();
abld -i        # build and install into ~/.m2

The generated POM lists your dependencies, so a project that depends on com.example:widgets:1.4.2 also pulls its transitive dependencies. Install runs every time you ask for it, even when the version is unchanged - it does not skip, so you never have to bump a version or delete from ~/.m2 to re-install after a change.

Building a Native App Image

The image action (-im) produces a native app image with jpackage: your application plus a bundled Java runtime, launched by a native executable.

include javabuild;

runner = new BuildRunner(args, "src/main/java", true,
                         ["com.google.guava:guava:33.0.0-jre"]);
runner.setMainClass("com.example.Main");
runner.run();
abld -im       # build the app image under build/image

Set the main class so the launcher knows where to start, and make the jar executable (true) so the image bundles a runnable jar. The image action runs resolve, compile, jar, and jpackage in order.

The app image lands under build/image. To produce an installer instead of a plain app image (a .deb, .dmg, .msi, and so on), use the low-level JavaTools.jpackage call described later, which takes an explicit package type.

Going Beyond the Easy Path: Adding Your Own Steps

When the standard pipeline is not enough, you build the runner by hand. The no-argument constructor registers the flags but creates no steps. You add the steps you want - standard ones, your own, or a mix - and run:

include javabuild;

runner = new BuildRunner();
runner.add(new MyCustomTask());          // your own step
runner.setAction(buildAction.package);   // or pass args and let the CLI pick
runner.run();

add appends a step to the end of the list. setAction picks the action directly; if you pass args to the constructor instead, the command line picks it. The runner runs whatever steps it holds, in order.

Writing a Custom Task

A custom step extends BuildTask and overrides run(). That is the only required method. run() returns a result map with a success key.

/**
 * `StampTask` writes a build-stamp file each time the build runs.
 */
class StampTask : BuildTask {
    public pathValue = null;

    /**
     * Creates the stamp task.
     * @p Path is the file to write the stamp to.
     */
    public StampTask(string Path) {
        this.nameValue = "stamp";
        this.pathValue = Path;
        this.incrementalValue = false;    // always run
    }

    /**
     * Writes the stamp file.
     * @r A result map with success set to true.
     */
    public run() {
        file.write(this.pathValue, "built at " + sys.getMills() + "\n");
        return { "success": true };
    }
}

To make a custom step incremental, also override inputs() and outputs() to return the files it reads and writes. The runner then skips the step when its outputs are newer than its inputs, exactly like the standard steps. Set incrementalValue = false (as above) for a step that should always run.

The runner calls execute() on each step, never run() directly. execute() is where the incremental check lives, so it runs first and calls your run() only when the step is not up to date.

The Low Level: Calling JavaTools Directly

BuildRunner and the tasks all sit on JavaTools, which you can call directly for full control or a one-off task. Each operation is a single call that returns a result map with a success key.

include javatools;

jt = new JavaTools();

// Compile: sources in, .class files out.
res = jt.compile(["src/main/java"], "build/classes");
if (res.get("success") == true) {
    // Package a jar from the compiled classes.
    jt.jar("build/classes", "build/dist/app.jar", "com.example.Main");
}

The main operations:

Call What it does
jt.compile(sources, outputDir, classpath, release) Run javac. Returns diagnostics on failure.
jt.jar(inputDir, output, mainClass) Package a jar from compiled classes.
jt.javadoc(sources, outputDir, classpath) Generate Javadoc.
jt.resolve(coordinates, scope, transitive) Resolve Maven dependencies to a classpath.
jt.jlink(modules, output, modulePath) Build a custom Java runtime image.
jt.jpackage(type, name, inputDir, mainJar, mainClass) Build a native app image or installer.
jt.install(group, artifact, version, jarPath, pomPath, sourcesPath, javadocPath) Install the jar and its companions into the local Maven repository.

Only the first argument of each is required; the rest have sensible defaults (for example, compile writes to build/classes).

Option Builders for Full Control

The short calls above cover the common options. For everything else, each operation has a matching option-builder class with a method per option. You build it up and pass it to the operation. For example, JavaCompile covers the full javac surface:

include javatools;

comp = new JavaCompile();
comp.sources(["src/main/java"]);
comp.outputDir("build/classes");
comp.classpath(["build/lib/guava.jar"]);
comp.release(21);
comp.encoding("UTF-8");

jt = new JavaTools();
jt.compile(comp);

The builders are JavaCompile, JavaJar, JavaDoc, JavaLink (for jlink), and JavaPackage (for jpackage). Each has a method for every option its tool supports - classpath, module path, jar manifest entries, Javadoc titles, jpackage icons and installer types, and so on. Reach for a builder when the short call does not expose the option you need.

A Note on jlink

jlink builds a stripped-down custom runtime image. It is a best-effort, low-level feature: it is available through jt.jlink and the JavaLink builder but is not part of the image action, which uses jpackage. jlink needs a full JDK with a jmods directory present; the runtime bundled with the Aussom CLI does not include one, so jt.jlink reports a clear error when it cannot find the modules it needs. Point it at a full JDK install when you use it.

Reference

Action to Steps

Action Steps run, in order
clean Removes every step's outputs. Always runs first.
compile resolve, compile
package resolve, compile, jar
docs resolve, javadoc
install resolve, compile, jar, javadoc, install
image resolve, compile, jar, jpackage
all resolve, compile, jar, javadoc, jpackage

Selecting more than one action runs the union of their steps, and each shared step (like resolve) runs only once.

BuildRunner Methods

Method What it does
new BuildRunner(args, sourcesPath, executableJar, deps) Easy path. All arguments optional; a source path builds the standard steps.
new BuildRunner() Programmatic path. Registers flags, creates no steps.
setDeps(list) Set the Maven coordinates to resolve.
setMainClass(string) Set the jar entry point and image launcher.
setCoordinates(group, artifact, version) Set the Maven coordinates the jar is installed under. Required for install.
setModular(bool) Force modular mode on or off, overriding the module-info.java auto-detection.
setAction(buildAction) Set a single action, overriding the default and the CLI.
setLogMode(string) Force the progress display: "auto", "tui", or "plain".
add(task) Append a build step.
run() Run the selected actions. Returns a result map.

The Result of run()

run() returns a map with success (a bool) and results (a list, one entry per step). Each entry has a name and a status. The status is one of pending, skipped, done, or failed.

res = runner.run();
if (res.get("success") != true) {
    // a step failed; the runner already printed the reason
}

When a step fails, the runner stops (by default), prints the failure reason, and run() returns with success set to false.

Not Yet Supported

The tools install to your local Maven repository (the install action), but they do not publish to a remote one:

  • No publish or deploy. There is no way to upload your artifact to a remote Maven repository (a company Nexus or Artifactory) or to publish it to Maven Central. Those need remote transport, credentials, and - for Maven Central - GPG signing, which are not built in.

So the tools cover everything through a local install; pushing to a remote repository is a separate step you would run outside these tools for now (for example, with Maven itself).

See Also

  • design/java-build-tools.md - the Phase 1 design (the JavaTools API).
  • design/java-build-tools-phase-2.md - the Phase 2 design (tasks, the runner, incremental builds, and native images).