Sombriks Has A Plan



Build with bld and why it matters to the jvm ecosystem

In previous articles we discussed how to structure a java project and like this guide here we started naive and grew in complexity more and more.

But complexity is bad, it tends to just add up.

In this article we'll discuss a new tool, which uses both the state of the art and a very pragmatic way of thinking when it comes to the problem of manage a java/jvm project.

My friends, let's talk about bld.

What is bld anyway?

I was lurking in the internet when i hit this Adam Bien's podcast episode and got hooked: so that small command he was using wasn't a custom, personal, script to relieve the pain. It was a dedicated tool.

Curiosity grew and i decided to try it. Now here we are, writing advice for me, for new java adopters and for other old, seasoned and weary java architects out there.

In very short words, bld create and manage jvm-based projects.

The key difference, for example, from maven, it uses java language itself to manage the project construction and dependencies.

That puts bld closer to what gradle does, but unlike this one, bld is simpler, focused on the goal of just build things up and build it right.

What? how does it compares to ant?

Compared with ant, bld is simple and does more. Both has 3 letters.

How good is it?

Well, so far the single evidence that bld is cool and good is my word. so let't take a closer look:

Installation

For any of those tools mentioned here, there is a tool to help install them and it's called sdkman.

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

Once it's installed, proceed to install bld:

sdk install bld

Check if things are fine by simply calling bld:

bld
Welcome to bld 1.9.0.

The bld CLI provides its features through a series of commands that
perform specific tasks. The help command provides more information about
the other commands.

Usage: help [command]

The following commands are supported.

  create        Creates a new project from multiple choice
  create-app    Creates a new Java application project
  create-base   Creates a new Java baseline project
  create-lib    Creates a new Java library project
  create-rife2  Creates a new RIFE2 web application project
  help          Provides help about any of the other commands
  upgrade       Upgrades the bld wrapper to the latest version
  version       Outputs the version of the build system

  -?, -h, --help    Shows this help message
  -D<name>=<value>  Set a JVM system property
  -s, --stacktrace  Print out the stacktrace for exceptions

Notes on project creation

Create your project with:

bld create-app foo.bar app

And thats it. Get it?

If you're not shocked, see what is needed to achieve almost the same with maven:

mvn archetype:generate -DinteractiveMode=false \
                       -DgroupId=foo.bar \
                       -DartifactId=app \
                       -DarchetypeGroupId=org.apache.maven.archetypes \
                       -DarchetypeArtifactId=maven-archetype-quickstart \
                       -DarchetypeVersion=1.4

And this is what it takes to do the same on gradle:

gradle init \
  --type java-application \
  --dsl kotlin \
  --test-framework junit-jupiter \
  --package foo.bar \
  --project-name app  \
  --no-split-project  \
  --java-version 17

One may argue that although those are two very hideous command lines, it's not that important since we're not doing it so often.

On the other hand, this is how you create a go project nowadays:

mkdir app ; cd app; go mod init foo.bar/app

And this is the creation of a javascript project:

mkdir app ; cd app; npm init -y

The ability to create simple, fast and discardable projects without much effort tends to make us do it more, experiment more. That characteristic alone makes bld worthy enough to be taken seriously.

But how is it managing a project?

Simple hello world

Once created, you will find a very pragmatic structure:

Take this sample project as an example:

tree sample-htmx-rife2 -a
sample-htmx-rife2/
├── bld
├── bld.bat
├── .git
├── .github
│   └── workflows
│       └── bld.yml
├── .gitignore
├── .idea
│   ├── app.iml
│   ├── bld.iml
│   ├── dataSources
│   ├── dataSources.local.xml
│   ├── dataSources.xml
│   ├── .gitignore
│   ├── libraries
│   │   ├── bld.xml
│   │   ├── compile.xml
│   │   ├── runtime.xml
│   │   ├── standalone.xml
│   │   └── test.xml
│   ├── misc.xml
│   ├── modules.xml
│   ├── runConfigurations
│   │   ├── Run Main.xml
│   │   └── Run Tests.xml
│   ├── vcs.xml
│   └── workspace.xml
├── lib
│   └── bld
│       ├── bld-wrapper.jar
│       ├── bld-wrapper.properties
│       └── bld-wrapper.properties.hash
├── LICENSE
├── README.md
├── src
│   ├── bld
│   │   └── java
│   │       └── sample
│   │           └── htmx
│   │               └── TodoappBuild.java
│   ├── main
│   │   ├── java
│   │   │   └── sample
│   │   │       └── htmx
│   │   │           ├── elements
│   │   │           │   ├── DeleteTodoElement.java
│   │   │           │   ├── FindTodoElement.java
│   │   │           │   ├── IndexElement.java
│   │   │           │   ├── InsertTodoElement.java
│   │   │           │   ├── ListTodoElement.java
│   │   │           │   ├── processor
│   │   │           │   │   └── TemplateProcessor.java
│   │   │           │   └── UpdateTodoElement.java
│   │   │           ├── model
│   │   │           │   └── Todo.java
│   │   │           ├── service
│   │   │           │   └── TodoService.java
│   │   │           ├── TodoappSite.java
│   │   │           └── TodoappSiteUber.java
│   │   ├── resources
│   │   │   ├── simplelogger.properties
│   │   │   └── templates
│   │   │       ├── index.html
│   │   │       └── todos
│   │   │           ├── detail.html
│   │   │           ├── form.html
│   │   │           └── list.html
│   │   └── webapp
│   │       ├── css
│   │       │   └── style.css
│   │       ├── htmx-2.0.0-beta3.min.js
│   │       └── WEB-INF
│   │           └── web.xml
│   └── test
│       └── java
│           └── sample
│               └── htmx
│                   └── TodoappTest.java
└── .vscode
    ├── launch.json
    └── settings.json

Notice the .idea and the .vscode directories. You got those for free, but in the future the IDEs might detect that it's a bld project. I want to believe.

This is what you get when you run the wrapper script:

$ ./bld
Welcome to bld 1.9.0.

The bld CLI provides its features through a series of commands that
perform specific tasks. The help command provides more information about
the other commands.

Usage: help [command]

The following commands are supported.

  clean            Cleans the build files
  compile          Compiles the project
  dependency-tree  Outputs the dependency tree of the project
  download         Downloads all dependencies of the project
  help             Provides help about any of the other commands
  jar              Creates a jar archive for the project
  jar-javadoc      Creates a javadoc jar archive for the project
  jar-sources      Creates a sources jar archive for the project
  javadoc          Generates javadoc for the project
  precompile       Pre-compiles RIFE2 templates to class files
  publish          Publishes the artifacts of your web project
  publish-local    Publishes to the local maven repository
  purge            Purges all unused artifacts from the project
  run              Runs the project (take option)
  test             Tests the project with JUnit (takes options)
  uberjar          Creates an UberJar archive for the project
  updates          Checks for updates of the project dependencies
  version          Outputs the version of the build system
  war              Creates a war archive for the project

  -?, -h, --help    Shows this help message
  -D<name>=<value>  Set a JVM system property
  -s, --stacktrace  Print out the stacktrace for exceptions

IDE will help, but it's nice to have everything you need presented like that, upfront, ready to hit the road.

The sample project uses rife2 and htmx. It is part of an ongoing experiment involving htmx. You can see other articles about it later.

The ergonomics of such way to manage the java project are closer to what we get on node or go. Ask anyone who dealt with projects involving those languages and you will get positive feedback.

A look into the build file

As mentioned before, bld uses java to configure a java projet. this is what it looks like:

package sample.htmx;

import rife.bld.WebProject;

import java.util.List;

import static rife.bld.dependencies.Repository.*;
import static rife.bld.dependencies.Scope.*;
import static rife.bld.operations.TemplateType.*;

public class TodoappBuild extends WebProject {

    public TodoappBuild() {
        pkg = "sample.htmx";
        name = "Todoapp";
        mainClass = "sample.htmx.TodoappSite";
        uberJarMainClass = "sample.htmx.TodoappSiteUber";
        version = version(0, 1, 0);

        downloadSources = true;
        repositories = List.of(MAVEN_CENTRAL, RIFE2_RELEASES);
        scope(compile)
                .include(dependency("com.uwyn.rife2:rife2:1.7.3"))
                .include(dependency("org.slf4j:slf4j-simple:2.0.13"));
        scope(provided);
        scope(test)
                .include(dependency("org.jsoup:jsoup:1.17.2"))
                .include(dependency("org.junit.jupiter:junit-jupiter:5.10.2"))
                .include(dependency("org.junit.platform:junit-platform-console-standalone:1.10.2"));
        scope(standalone)
                .include(dependency("org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.8"))
                .include(dependency("org.eclipse.jetty.ee10:jetty-ee10:12.0.8"))
                .include(dependency("com.h2database:h2:2.2.224"));

        precompileOperation()
                .templateTypes(HTML);
    }

    public static void main(String[] args) {
        new TodoappBuild().start(args);
    }
}

Now tat's something.

Since we're talking about modern, enterprise java, there is no escape from maven central. But it's still more benefitial than troublesome, so we still have to declare maven coordinates correctly.

Pretty much like maven and gradle, you can put scopes on your dependencies, making things available in classpath depending on the context.

And of course you declare your own coordinates in this class as well, since bld is fully capable of publish packages too.

But does it really shine?

Short answer: YES.

Long one: let's take a look at this spring boot project.

I've added bld, maven and gradle as build systems to it.

This is the pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.5</version>
  <relativePath/> <!-- lookup parent from repository -->
 </parent>
 <groupId>sample.htmx</groupId>
 <artifactId>sample-htmx-spring</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>sample-htmx-spring</name>
 <description>Demo project for HTMX with Spring Boot / Thymeleaf</description>
 <properties>
  <java.version>17</java.version>
 </properties>
 <dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-webflux</artifactId>
  </dependency>
  <dependency>
   <groupId>org.liquibase</groupId>
   <artifactId>liquibase-core</artifactId>
  </dependency>
  <dependency>
   <groupId>org.webjars.npm</groupId>
   <artifactId>htmx.org</artifactId>
   <version>1.9.12</version>
  </dependency>

  <dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
   <scope>runtime</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>io.projectreactor</groupId>
   <artifactId>reactor-test</artifactId>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>org.hamcrest</groupId>
   <artifactId>hamcrest</artifactId>
   <scope>test</scope>
   <version>2.2</version>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
  </plugins>
 </build>

</project>

A reminder of XML isn't meant to be read by people but by parsers.

The gradle counterpart, the build.gradle file, looks like this:

plugins {
 id 'java'
 id 'org.springframework.boot' version '3.2.5'
 id 'io.spring.dependency-management' version '1.1.4'
}

group = 'sample.htmx'
version = '0.0.1-SNAPSHOT'

java {
 sourceCompatibility = '17'
}

repositories {
 mavenCentral()
}

dependencies {
 implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
 implementation 'org.springframework.boot:spring-boot-starter-webflux'
 implementation 'org.liquibase:liquibase-core'
 implementation 'org.webjars.npm:htmx.org:1.9.12'

 testImplementation 'org.springframework.boot:spring-boot-starter-test'
 testImplementation 'io.projectreactor:reactor-test'
 testImplementation 'org.hamcrest:hamcrest:2.2'

 runtimeOnly 'com.h2database:h2'
}

tasks.named('test') {
 useJUnitPlatform()
}

Better to read, since programming languages are meant to be easy to the programmer's eyes.

Then gradle parses it, spawns a daemon and performs several internal operations in order to compile your classes.

And this is our bld build class, from src/bld/java/sample/htmx/spring/SampleHtmxSpringBuild.java:

package sample.htmx.spring;

import rife.bld.BuildCommand;
import rife.bld.WebProject;
import rife.bld.extension.BootJarOperation;

import java.util.List;

import static rife.bld.dependencies.Repository.*;
import static rife.bld.dependencies.Scope.*;
import static rife.bld.operations.TemplateType.*;

public class SampleHtmxSpringBuild extends WebProject {

    public SampleHtmxSpringBuild() {

        pkg = "sample.htmx.spring";
        name = "SampleHtmxSpring";
        mainClass = "sample.htmx.spring.SampleHtmxSpringApplication";
        version = version(0, 0, 1, "SNAPSHOT");

        downloadSources = true;
        repositories = List.of(MAVEN_CENTRAL, RIFE2_RELEASES);

        scope(compile)
                .include(dependency("org.springframework.boot:spring-boot-starter-data-jpa:3.2.5"))
                .include(dependency("org.springframework.boot:spring-boot-starter-thymeleaf:3.2.5"))
                .include(dependency("org.springframework.boot:spring-boot-starter-webflux:3.2.5"))
                .include(dependency("org.webjars.npm:htmx.org:1.9.12"))
                .include(dependency("org.liquibase:liquibase-core:4.24.0"));

        scope(test)
                .include(dependency("org.junit.platform:junit-platform-console-standalone:1.10.2"))
                .include(dependency("org.springframework.boot:spring-boot-starter-test:3.2.5"))
                .include(dependency("io.projectreactor:reactor-test:3.6.5"))
                .include(dependency("org.hamcrest:hamcrest:2.2"));

        scope(standalone)
                .include(dependency("org.springframework.boot:spring-boot-loader:3.2.5"))
                .include(dependency("com.h2database:h2:2.2.224"));
    }

    @BuildCommand(summary = "Creates an executable JAR for the project")
    public void bootJar() throws Exception {
        new BootJarOperation()
                .fromProject(this)
                .execute();
    }

    public static void main(String[] args) {
        new SampleHtmxSpringBuild().start(args);
    }
}

We can argue that although groovy is cleaner, the developer is already wired in java syntax, so there is a smaller cognitive overhead.

And this is definitively easier to read than the maven's pom.xml.

Needless to say, all three build options in this sample are able to build a regular spring boot project.

Continuous integration

One last but not least important note: You can put bld to work for you on your continuous integration pipeline, just as you would do with maven or gradle. The sample project has three github workflows sampling how to use each of them.

The bld and maven are usually the fastest ones, gradle tries to spawn a daemon even in ci mode and it slows it down.

A quick note on bld workflow:

---
name: Java CI with bld
on:
  push:
    branches: [ "*" ]
  workflow_dispatch:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
      - uses: actions/cache@v4
        with:
          path: lib
          key: bld-${{ hashFiles('src/bld/java/**') }}
        id: cache
      - if: steps.cache.outputs.cache-hit != 'true'
        run: ./bld clean download
      - run: ./bld compile test

In order to keep it fast, you must tweak the build process by using the cache action. Maven and gradle actions uses cache too, so add it to your bld workflow as well.

Conclusion (or Why it matters)

For a long time, java, while language and platform, was stuck into a position lacking innovation and offering little developer ergonomics.

The recent years however where different: More frequent releases, decent LTS support, new features being added and as consequence new adopters, new tools and new ideas.

It is important and quite a feat to remain stable and very backwards compatible, but embrace the new and deliver good developer experience, like other stacks do, that keeps the jvm and its ecosystem as not only a safe choice, but also an exciting, joyful one.

You usually get nice results, nice products from happy, excited people.

And boy bld is for sure exciting, node and golang people can't mock me anymore!

Thanks for reading, happy hacking, bye bye!