Featured image of post Bld: The Pure Java Build Experience

Bld: The Pure Java Build Experience

Do we need another build tool for our Java projects? Maybe. Could Bld be the one? Honestly, I’m not sure. What I can tell you, though, is that working with pure Java even with the building tasks is a refreshing experience. Whether it’s because I’m saving my brain from context switching or something else, I enjoyed staying in the flow while fiddling with my project. That’s something I’ve never experienced with Maven, my go-to build tool in the Java world. I can’t wait to share what I've learned - so let's Bld something!

🎒 Resources

I came across Bld while searching for the perfect build tool for monorepos (spoiler: I haven’t found it yet). Although Bld wasn’t exactly what I was looking for, I found it interesting enough to experiment with. As someone who values simplicity, I liked the idea of using the same language for both the project and the build process.

In this post, I’ll share my experience with Bld and guide you through a small project to see if it worth your time to explore further. Finally - as every rose has its thorn - I’ll discuss the trade-offs of using Bld instead of more established build tools like Maven or Gradle.

🤔 What is Bld Anyways?

bld was created because we’re not really interested in build tools. We use them because we have to, but we’d rather just get on with coding the real stuff.

Source: RIFE2

Bld is a lightweight build tool for the Java ecosystem. It approaches the build process differently than most other tools in the market: there is no Groovy-based DSL, no XML files to work with. Instead, you can define the entire build process using plain Java.

💡 If you know Java, you already know how to work with Bld.

It allows you to build faster by eliminating the burden of cognitive switching between your build logic and your project code. While it may not significantly reduce build times, it offers the advantage of using the same language for both your project and the build process. No more awkward or verbose file formats, outdated tools or unsupported environments - it is just Java: write once, run everywere, right?

If you dig a bit deeper you might come across the name RIFE2, which is a full-stack Java framework. I must admit that I’ve never heard about this project before. Anyways, Bld was started as an in-house tool for building RIFE2, but it has since evolved into a standalone project. Thank you, RIFE2 Team! 🙏

Alright. Let’s see how it works.

If you’re not interested in the technical details, but want to see an executive…ish summary, jump to the Conclusion section.

🚀 Getting Started with Bld

Before we jump into the details, I must mention that Bld requires Java 17 or later to run. After all, it’s always a good idea to keep up with the latest and greatest LTS version of Java.

Installation

You have more than one option to get Bld up and running. My suggestion depends on your intentions:

  • For a quick evaluation & new projects: Use the dedicated Bash script to scaffold a new project with Bld baked in. This is what we’ll do in this tutorial.

  • Install Bld globally: If you plan to go all-in with Bld, you can install it globally. I personally recommend using SDKMAN for this purpose: sdk install bld.

Creating a New Project With Bld

As I mentioned, we’ll scaffold a new project using Bld. To do this, run the following command:

1
bash -c "$(curl -fsSL https://rife2.com/bld/create.sh)"

You’ll be prompted to select the project type. Choose base (1) for now. Next, specify the package name and the project name. Here is what I’ve done for this tutorial:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Downloading bld v2.0.1...

Welcome to bld v2.0.1.
Please enter a number for the project type:
  1: base   (Java baseline project)
  2: app    (Java application project)
  3: lib    (Java library project)
  4: rife2  (RIFE2 web application)
1
Please enter a package name (for instance: com.example):
com.github.tcsizmadia.dognames
Please enter a project name (for instance: myapp):
backend
Downloading finished successfully.
The project was successfully created at '/Users/tamas/git/bld-sandbox/backend'.

Let’s see what we have now. Execute the bld tool:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
./bld
Downloading: https://repo1.maven.org/maven2/com/uwyn/rife2/bld/2.0.1/bld-2.0.1.jar ... done
Downloading: https://repo1.maven.org/maven2/com/uwyn/rife2/bld/2.0.1/bld-2.0.1-sources.jar ... done
Welcome to bld 2.0.1.

The bld CLI provides its features through a series of commands that
perform specific tasks.

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
  purge            Purges all unused artifacts from the project
  run              Runs the project (take option)
  test             Tests the project
  updates          Checks for updates of the project dependencies
  version          Outputs the version of the build system

The following bld arguments are supported:

  --offline         Works without Internet (only as first argument)
  -?, -h, --help    Shows the help
  -D<name>=<value>  Sets a JVM system property
  -s, --stacktrace  Prints out the stacktrace for exceptions

Great! We have a new project with Bld set up. Let’s see our build setup in action:

1
2
3
4
5
6
cd backend
./bld clean compile
Cleaning finished successfully.
Compilation finished successfully.
❯ ./bld run
Hello World!

👷 Bld in Action

But what’s inside? Let’s put aside the ‘Hello World!’ stuff for a moment and concentrate on the bld directory in src. What is in there? At first sight, it looks like a regular Java project structure:

1
2
3
4
5
6
7
8
src/bld
├── java
│   └── com
│       └── github
│           └── tcsizmadia
│               └── dognames
│                   └── BackendBuild.java
└── resources

Look Ma, No XML!

No XML - no DSL!

The BackendBuild.java file is where the magic happens: this is where you define your build process. As you can see, there are no pom.xml or build.gradle files anywhere, so we can start dissecting the BackendBuild.java file right away.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class BackendBuild extends BaseProject {
    public BackendBuild() {
        pkg = "com.github.tcsizmadia.dognames";
        name = "Backend";
        mainClass = "com.github.tcsizmadia.dognames.BackendMain";
        version = version(0,1,0);

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

        testOperation().mainClass("com.github.tcsizmadia.dognames.BackendTest");
    }

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

The pkg, name, mainClass, and version fields might look fishy at first in the constructor. These protected fields from the BaseProject class provide a concise way to set our project’s metadata such as the package name, the project’s name and its version. The repository setup is worth a look - as you can see -, Bld stands on the shoulders of Maven Central as the number one source of dependencies. Because Bld is pure Java, you can add a custom one hands-down if you have to: look at the Repository record for a hint.

No need to worry - for this tutorial, we are perfectly fine with all that Maven Central has to offer: we are going to build a tiny service powered by Spring Boot.

Adding Dependencies & Plugins

Bld offers a plugin system (here plugins called extensions) to extend its capabilities - like Maven or Gradle. For this tutorial we need the bld-spring-boot extension, which provides a simple way to build Spring Boot applications with Bld.

Open the lib/bld/bld-wrapper.properties file and add the following dependency using the Gradle shorthand notation:

1
bld.extensions=com.uwyn.rife2:bld-spring-boot:0.9.7

Now comes the most cumbersome part of working with Bld: you have to specify the dependencies in the BackendBuild.java file. For Maven or Gradle - you are in good hands with the Spring Initializr or you can rely on your IDE to help you out. But with Bld - at least for now - you have to do it manually.

So open the BackendBuild.java file and add the following changes:

  1. Change the BackendBuild class to extend WebProject instead of BaseProject (line 1).
  2. Add the spring-boot-starter and spring-boot-starter-web dependencies to the compile scope (line 18).
  3. Add the spring-boot-starter-test, junit-jupiter, and junit-platform-console-standalone dependencies to the test scope (line 25). This is necessary to run the unit tests.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class BackendBuild extends WebProject {
    public static final VersionNumber BOOT_VERSION = new VersionNumber(3,3,3);
    public static final VersionNumber JUPITER_VERSION = new VersionNumber(5,11,0);
    public static final VersionNumber JUNIT_PLATFORM_VERSION = new VersionNumber(1,11,0);
    public static final Logger logger = Logger.getLogger(BackendBuild.class.getName());

    public BackendBuild() {
        pkg = "com.github.tcsizmadia.dognames";
        name = "Backend";
        mainClass = "com.github.tcsizmadia.dognames.BackendMain";
        version = version(0,1,0);

        javaRelease = 17;

        autoDownloadPurge = true;
        repositories = List.of(MAVEN_CENTRAL);

        scope(Scope.compile)
                .include(dependency("org.springframework.boot", "spring-boot-starter", BackendBuild.BOOT_VERSION))
                .include(dependency("org.springframework.boot", "spring-boot-starter-web", BackendBuild.BOOT_VERSION));

        scope(Scope.standalone)
                .include(dependency("org.springframework.boot", "spring-boot-loader", BackendBuild.BOOT_VERSION));

        scope(Scope.test)
                .include(dependency("org.springframework.boot", "spring-boot-starter-test", BackendBuild.BOOT_VERSION))
                .include(dependency("org.junit.jupiter", "junit-jupiter", BackendBuild.JUPITER_VERSION))
                .include(dependency("org.junit.platform", "junit-platform-console-standalone", BackendBuild.JUNIT_PLATFORM_VERSION));
    }

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

I believe it’s important to provide further clarification on a few points. You may have noticed the VersionNumber constants in the code. It’s something like defining a <property> in Maven - but in a more idiomatic way. The javaRelease field may be familiar; think of <maven.compiler.source> and <maven.compiler.target> in Maven. The autoDownloadPurge field will save you from running ./bld download every time you’ve added a new dependency.

Here you can experience the satisfaction of working with Bld: we set up a lot of things in a single file using nothing but Java. Do not stop here! Let’s specify the project’s dependencies via a Fluent Interface - which is a nice touch in my opinion: you need to set a scope(), then just include() the dependency() as needed. Again, it’s Java, nothing else.

Now you know how to define your build process using pure code. While it may seem daunting at first, it becomes easier as you become familiar with Bld. Relying on the IDE’s auto-suggestions or an AI assistant can help you stay in the flow and bring some joy to the tedious task of defining build configurations. ✨

🐶 Defining the Sample Project

A robot dog - a very rare breed

I’ve prepared a small REST API to list dog names & breeds. Both feature has its own endpoint:

EndpointDescription
/dogs/namesReturns a list of dog names
/dogs/breedsReturns a list of dog breeds

Each endpoint has its own Controller and Service. The project is intended to be a demonstration of how to build a Spring Boot application with Bld, rather than a tutorial on creating Spring Boot microservices. Therefore, I’ll skip the detailed explanations for now. Please check out the code on the GitHub repository and bear with me:

1
2
git clone https://github.com/tcsizmadia/bld-sandbox.git
cd bld-sandbox/backend

Building & Running the Spring Boot Application

Let’s fetch all the dependencies, compile the code, and run the application:

1
./bld download compile run

Shortly, you will be able to access the service at http://localhost:8080/dogs/names and http://localhost:8080/dogs/breeds.

Let’s verify it:

1
curl http://localhost:8080/dogs/names

Here we go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[
  "Daisy",
  "Duke",
  "Max",
  "Lucy",
  "Rocky",
  "Bailey",
  "Molly",
  "Buddy",
  "Sadie",
  "Charlie"
]

…and for the dog breeds:

1
curl http://localhost:8080/dogs/breeds

Here I’ve formatted the response for brevity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[
  {
    "id": 1,
    "name": "Affenpinscher",
    "description": "The Affenpinscher is a terrier-like toy breed of dog."
  },
  {
    "id": 2,
    "name": "Afghan Hound",
    "description": "The Afghan Hound is an aristocrat, his whole appearance one of dignity and aloofness with no trace of plainness or coarseness."
  },
  {
    "id": 3,
    "name": "Airedale Terrier",
    "description": "The Airedale Terrier is the largest of all terrier breeds."
  },
  {
    "id": 4,
    "name": "Akita",
    "description": "The Akita is a large and powerful dog with a noble and intimidating presence."
  },
  {
    "id": 5,
    "name": "Alaskan Malamute",
    "description": "The Alaskan Malamute stands well over the pads, and this stance gives the appearance of much activity and a proud carriage, with head erect and eyes alert showing interest and curiosity."
  }
]

Building an Executable (“fat”) Jar

Now, stop the service and try to build a so-called “fat” jar - a JAR file that contains all the dependencies needed to run the application.

To make it happen, we need to wire the bld-spring-boot Extension’s BootJarOperation to our build logic: add this method to the BackendBuild.java file:

1
2
3
4
5
6
@BuildCommand(summary = "Creates an executable JAR for the project")
public void bootjar() throws Exception {
    new BootJarOperation()
            .fromProject(this)
            .execute();
}

This method is annotated with @BuildCommand, which indicates that it is an entry point for a custom command in Bld. We will discuss Custom Commands and Bld Operations soon, for now it’s enough to know that this method will create the executable JAR file of our Spring Boot application.

Let’s try it in action:

1
./bld bootjar

After that, run the generated JAR file:

1
java -jar build/dist/backend-0.1.0-boot.jar

The result should be the same service running with bld run. Congratulations! You have successfully created a fully functional, deployable microservice. 🎉

Now add an extra step to the build process: we are going to enrich our dog breed list with some AI-generated data.

🛠️ Extending Bld - Custom Commands and Extensions

As we know, Bld supports plugins, called extensions, to enhance its functionality. These extensions offer ready-to-use solutions for your tasks, so no need to write the logic yourself. For example, in the previous section, we used the bld-spring-boot extension.

However, there’s an even simpler way to customize the build: creating custom commands!

Custom Commands in Bld

Custom commands are essential for extending Bld. If you need to quickly add an extra task to your build process and are confident you won’t need it again, a custom command is your best friend. Here’s a secret: custom commands serve as the entry points for extensions, which is why we begin by writing one.

Let’s create a custom command to generate a CSV file with dog breeds. Instead of just spitting out a CSV file — which is boring — we’ll use Ollama and a locally running LLM to make it more interesting.

Sure, using an AI model to generate this list might seem a bit unusual, but it’s a great way to demonstrate how easily you can add new functionality to Bld.

Here is a class named BreedGenerator - very stupid name, I know - that will do the job:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class BreedGenerator {

    private static final Logger logger = Logger.getLogger(BreedGenerator.class.getName());

    private static final String PROMPT = """
            Create a CSV list of Dog breeds.
            Include exactly 10 entries.
            The first line must be the header.
            The header should contain these fields: id, name, description.
            Respond only with the CSV and nothing else.
            """;

    private static final String MODEL = "llama3.1";
    public static final String OLLAMA_API_URL = "http://localhost:11434/api/generate";

    private final Path destination;

    public BreedGenerator(Path destination) {
        this.destination = destination;
        logger.info("BreedGenerator initialized");
    }

    private String createJsonRequestPayload() throws IOException {
        var requestMap = new HashMap<String, Object>(3);
        requestMap.put("model", MODEL);
        requestMap.put("prompt", PROMPT);
        requestMap.put("stream", false);

        return JSON.std.asString(requestMap);
    }

    private String extractCsvFromResponse(String response) throws IOException {
        var mapFromResponse = JSON.std.mapFrom(response);
        return mapFromResponse.get("response").toString();
    }

    private Optional<String> fetchBreeds() {
        logger.info("Fetching breeds using Ollama");

        try {
            var client = HttpClient.newHttpClient();
            var request = HttpRequest.newBuilder()
                    .uri(URI.create(OLLAMA_API_URL))
                    .POST(HttpRequest.BodyPublishers.ofString(this.createJsonRequestPayload()))
                    .build();
            var response = client.send(request, HttpResponse.BodyHandlers.ofString());
            return Optional.of(this.extractCsvFromResponse(response.body()));
        } catch (IOException | InterruptedException e) {
            logger.severe("Error fetching breeds: " + e.getMessage());
            Thread.currentThread().interrupt();
            return Optional.empty();
        }
    }

    public void generate() {
        logger.info("Generating breeds using Ollama");

        this.fetchBreeds().ifPresent(csv -> {
            try {
                Files.writeString(destination, csv.replace("\\n", System.lineSeparator()));
            } catch (IOException e) {
                logger.severe("Error writing breeds to file: " + e.getMessage());
            }
        });
   }
}

Let me explain the details here. The PROMPT constant instructs our model (in this case llama3.1) to provide a CSV file with exactly 10 breed entries - including the header.

The public generate() method is the entry point of the command. This is what we are going to use in our custom command.

One important thing, before we move on: as you might have noticed, we are using an external dependency: jackson-jr. We need to add it to our project.

Open the lib/bld/bld-wrapper.properties file and add the following line somewhere:

1
bld.extensionsforcommands=com.fasterxml.jackson.jr:jackson-jr-objects:2.17.2

So what is this bld.extensionsforcommands property? According to the documentation:

Any property starting with bld.extension, can contain a comma-seperated list of extensions in the common dependency description format groupId:arfifactId:version.

I thought it would be a good idea to separate the dependencies for the custom commands from the extensions. Feel free to put all on the same line, if you prefer it that way.

Now as the dependencies are sorted, let’s create the custom command itself. Open the BackendBuild.java file and add the following code:

1
2
3
4
@BuildCommand(summary = "Generate dog breeds", value = "generate-breeds", help = BreedGeneratorHelp.class)
public void generateBreeds() {
    new BreedGenerator(Path.of("src/main/resources/dog-breeds.csv")).generate();
}

The generateBreeds() method is annotated with @BuildCommand, which indicates that it is an entry point for a custom command in Bld. The parameters are:

  • summary: A short description of the command that is displayed in the summary of all supported commands.
  • value: The actual name of the command, which in this case is generate-breeds.
  • help: A class that provides detailed information about the command, specified as BreedGeneratorHelp.class.

Finally, we need to create the BreedGeneratorHelp class. This class will provide more control over the help message displayed when the user runs ./bld help generate-breeds. Do not expect anything complicated here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class BreedGeneratorHelp implements CommandHelp {
    @Override
    public String getSummary() {
        return "Using a locally running instance of Ollama, this command generates a list of dog breeds.";
    }

    @Override
    public String getDescription(String topic) {
        return """
                This command generates a list of dog breeds using a locally running instance of Ollama.
                The list is saved to the file dog-breeds.csv in the resources directory.
                """;
    }
}

🤔 Unfortantely, I couldn’t find any documentation about the topic parameter in the getDescription() method. Its value is always the command’s name, so I think it is safe to ignore it. 🤷

Custom Command in Action

Now that we have everything in place, let’s see what’ve we done:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
./bld
Welcome to bld 2.0.1.

The bld CLI provides its features through a series of commands that
perform specific tasks.

The following commands are supported:

  bootjar          Creates an executable JAR for the project
  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
  generate-breeds  Generate dog breeds
  help             Provides help about any of the other commands
  [remaining output omitted]

Fantastic.

✅ Bld is now capable of producing a list of dog breeds with the generate-breeds command.

❓ And what about the detailed help message we provided via BreedGeneratorHelp?

1
2
3
4
5
./bld help generate-breeds
Welcome to bld 2.0.1.

This command generates a list of dog breeds using a locally running instance of Ollama.
The list is saved to the file dog-breeds.csv in the resources directory.

Okay, we’e seen enough, it’s time to generate the CSV file using Ollama:

1
2
3
4
5
6
7
./bld generate-breeds
Sep 10, 2024 10:23:03 AM com.github.tcsizmadia.dognames.BreedGenerator <init>
INFO: BreedGenerator initialized
Sep 10, 2024 10:23:03 AM com.github.tcsizmadia.dognames.BreedGenerator generate
INFO: Generating breeds using Ollama
Sep 10, 2024 10:23:03 AM com.github.tcsizmadia.dognames.BreedGenerator fetchBreeds
INFO: Fetching breeds using Ollama

Now you can check the src/main/resources/dog-breeds.csv file to see the list of dog breeds:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
id,name,description
1,Poodle,"A small to medium-sized dog with a curly or wavy coat."
2,Bulldog,"A short-legged dog with a distinctive wrinkled face and a calm temperament."
3,Beagle,"A small to medium-sized dog with a happy and curious nature."
4,Dalmatian,"A spotted dog known for its energetic and playful personality."
5,Shih Tzu,"A small dog with a friendly and outgoing personality."
6,Pug,"A short-legged dog with a flat face and a loyal temperament."
7,Rottweiler,"A large powerful dog with a confident and protective nature."
8,Corgi,"A short-legged herding dog known for its intelligence and loyalty."
9,Chihuahua,"A small dog with a bold and energetic personality."
10,Yorkie,"A small dog with a friendly and affectionate temperament."

Naturally, the list will differ on your machine since it’s generated by an AI model. 🤖

Before we proceed, let’s ensure our REST API actually uses the new data.

1
./bld compile run

Access the /dogs/breeds endpoint and - fingers crossed - you should see the new dog breeds in the response:

1
curl http://localhost:8080/dogs/breeds

The response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[
  {
    "id": 1,
    "name": "Poodle",
    "description": "A small to medium-sized dog with a curly or wavy coat."
  },
  {
    "id": 2,
    "name": "Bulldog",
    "description": "A short-legged dog with a distinctive wrinkled face and a calm temperament."
  },
  {
    "id": 3,
    "name": "Beagle",
    "description": "A small to medium-sized dog with a happy and curious nature."
  },
  {
    "id": 4,
    "name": "Dalmatian",
    "description": "A spotted dog known for its energetic and playful personality."
  },
  {
    "id": 5,
    "name": "Shih Tzu",
    "description": "A small dog with a friendly and outgoing personality."
  },
  {
    "id": 6,
    "name": "Pug",
    "description": "A short-legged dog with a flat face and a loyal temperament."
  },
  {
    "id": 7,
    "name": "Rottweiler",
    "description": "A large powerful dog with a confident and protective nature."
  },
  {
    "id": 8,
    "name": "Corgi",
    "description": "A short-legged herding dog known for its intelligence and loyalty."
  },
  {
    "id": 9,
    "name": "Chihuahua",
    "description": "A small dog with a bold and energetic personality."
  },
  {
    "id": 10,
    "name": "Yorkie",
    "description": "A small dog with a friendly and affectionate temperament."
  }
]

✅ The AI-powered dog breed list - provided by our build tool - is now available in our REST API.

Make an Extension Out of It!

Custom commands are great, but they have their limitations: Reusing them across different projects and teams can be cumbersome. So if your command is more than a one-time show, consider turning it into an extension.

Let’s turn our dog breed generator command into an extension. 🐶

Not interested in every detail? Let’s cut to the chase: you can check out the final result from the feature/breed-extension branch on GitHub & jump to the building part.

First, create a new project for the extension with the same command we used at the beginning of this tutorial:

1
bash -c "$(curl -fsSL https://rife2.com/bld/create.sh)"

☝️ Do not forget to cd out from the backend directory before running the command!

The process is very similar to what we did with the backend project. The only difference is that you should create a lib project, so choose 3, when asked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Welcome to bld v2.1.0.
Please enter a number for the project type:
  1: base   (Java baseline project)
  2: app    (Java application project)
  3: lib    (Java library project)
  4: rife2  (RIFE2 web application)
3
Please enter a package name (for instance: com.example):
com.github.tcsizmadia.dognames
Please enter a project name (for instance: mylib):
breedgenerator
...
Downloading finished successfully.
The project was successfully created at '/Users/tamas/git/bld-sandbox/breedgenerator'.

We need to take two actions to transform our command into an extension:

  1. 👷 Extract the code from the Custom Command into the newly created 2nd project.
  2. 📐 Refactor the logic to align better with Bld extensions’ structure.

It may seem like a big task, but as you’ll see, we’ll finish it in no time.

Extract the Code

Before we start, let’s remove all the Bld-made code from the breedgenerator project: BreedgeneratorLib.java and BreedgeneratorTest.java.

We need only these files and directories:

  • The src/main/java and src/main/resources directories will be populated with the code and resources of the new extension.

  • The src/bld/java/com/github/tcsizmadia/dognames directory will contain the build logic.

Start with the latter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class BreedgeneratorBuild extends Project {
    private static final VersionNumber JACKSON_JR_VERSION = new VersionNumber(2, 11, 0);
    private static final VersionNumber BLD_VERSION = new VersionNumber(2, 1, 0);

    public BreedgeneratorBuild() {
        pkg = "com.github.tcsizmadia.dognames";
        name = "Breedgenerator";
        version = version(0, 1, 0);

        downloadSources = true;
        repositories = List.of(MAVEN_CENTRAL, RIFE2_RELEASES);
        scope(Scope.compile)
                .include(dependency("com.uwyn.rife2", "bld", BLD_VERSION))
                .include(dependency("com.fasterxml.jackson.jr", "jackson-jr-objects", JACKSON_JR_VERSION));
    }

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

We already know the pkg, name, version and repositories fields and defining dependencies using the scope() method.

Because we are developing an extension for Bld, we need to include Bld itself as a dependency. Additionally, the jackson-jr library is also a build-time dependency. This means we no longer need the bld.extensionsforcommands property in the lib/bld/bld-wrapper.properties file in the backend project.

💡 It’s a good idea to have all the dependencies downloaded before we start coding.

Ask Bld to do it for you:

1
./bld download

👏 Much better! Missing dependencies and the lack of auto suggestions - solved!

Next, let’s move the BreedGenerator class from the backend module to the src/main/java/com/github/tcsizmadia/dognames directory.

We need to make a few changes to the class:

  1. We will no longer hardcode the model name, the base URL of the LLM Provider (that is Ollama) and the destination path. These values were based on my personal preference, but for a decent build experience, we should provide a way to configure them.

  2. The generate() method now returns a boolean value to indicate whether the generation was successful - or not. We will use this return value to make the build fail or succeed based on the result.

Here is the updated BreedGenerator class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class BreedGenerator {

    public static final String DEFAULT_OLLAMA_API_URL = "http://localhost:11434/api/generate";
    private static final Logger logger = Logger.getLogger(BreedGenerator.class.getName());
    private static final String PROMPT = """
            Create a CSV list of Dog breeds.
            Include exactly 10 entries.
            The first line must be the header.
            The header should contain these fields: id, name, description.
            Respond only with the CSV and nothing else.
            """;
    private static final String DEFAULT_MODEL = "llama3.1";
    private final Path destination;
    private final String model;
    private final String apiURL;

    public BreedGenerator(Path destination, String model, String apiURL) {
        this.destination = destination;
        this.model = model;
        this.apiURL = apiURL;
        logger.info("BreedGenerator initialized");
    }

    public BreedGenerator(Path destination) {
        this(destination, DEFAULT_MODEL, DEFAULT_OLLAMA_API_URL);
    }

    private String createJsonRequestPayload() throws IOException {
        var requestMap = new HashMap<String, Object>(3);
        requestMap.put("model", this.model);
        requestMap.put("prompt", PROMPT);
        requestMap.put("stream", false);

        return JSON.std.asString(requestMap);
    }

    private String extractCsvFromResponse(String response) throws IOException {
        var mapFromResponse = JSON.std.mapFrom(response);
        return mapFromResponse.get("response").toString();
    }

    private Optional<String> fetchBreeds() {
        logger.info("Fetching breeds using Ollama");

        try {
            var client = HttpClient.newHttpClient();
            var request = HttpRequest.newBuilder()
                    .uri(URI.create(this.apiURL))
                    .POST(HttpRequest.BodyPublishers.ofString(this.createJsonRequestPayload()))
                    .build();
            var response = client.send(request, HttpResponse.BodyHandlers.ofString());
            return Optional.of(this.extractCsvFromResponse(response.body()));
        } catch (IOException | InterruptedException e) {
            logger.severe("Error fetching breeds: " + e.getMessage());
            Thread.currentThread().interrupt();
            return Optional.empty();
        }
    }

    public boolean generate() {
        logger.info("Generating breeds using Ollama");
        var result = new AtomicBoolean(false);
        this.fetchBreeds().ifPresent(csv -> {
            try {
                Files.writeString(destination, csv.replace("\\n", System.lineSeparator()));
                result.set(true);
            } catch (IOException e) {
                logger.severe("Error writing breeds to file: " + e.getMessage());
            }
        });

        return result.get();
    }
}

Thank you for bearing with me. Now comes the fun part:

Implement an Operation

The last step is to expose our logic in an implementation of the AbstractOperation class. Here is the BreedGeneratorOperation class (still, I can’t name my classes properly):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class BreedGeneratorOperation extends AbstractOperation<BreedGeneratorOperation> {
    private BaseProject project;
    private String model = "llama3.1";
    private String ollamaHost = "localhost";
    private int ollamaPort = 11434;

    public BreedGeneratorOperation fromProject(BaseProject project) {
        this.project = project;
        return this;
    }

    public BreedGeneratorOperation model(String model) {
        this.model = model;
        return this;
    }

    public BreedGeneratorOperation ollamaHost(String ollamaHost) {
        this.ollamaHost = ollamaHost;
        return this;
    }

    public BreedGeneratorOperation ollamaPort(int ollamaPort) {
        this.ollamaPort = ollamaPort;
        return this;
    }

    @Override
    public void execute() throws Exception {
        if (null == this.project) {
            throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
        }

        var destination = Path.of(this.project.srcMainResourcesDirectory().getAbsolutePath(), "dog-breeds.csv");
        var generator = new BreedGenerator(destination, this.model, String.format("http://%s:%d/api/generate", this.ollamaHost, this.ollamaPort));
        var result = generator.generate();

        this.project.exitStatus(result ? ExitStatusException.EXIT_SUCCESS : ExitStatusException.EXIT_FAILURE);
    }
}

The fluent API might be familiar to you from the bld-spring-boot extension. We will work in the same way, beacuse we are good citizen of the great Bld realm.

  • The fromProject() method is essential as it allows the operation to access the project’s metadata and directory paths. We’ll use the provided BaseProject instance to ensure the generated CSV files are placed in the correct location: the project’s src/main/resources directory. I apologize for the unorthodox practice of putting generated files in the resources directory, but as it’s a demo project, I hope it’s acceptable. 🙏

  • The model(), ollamaHost(), and ollamaPort() methods enable our users to configure the GenAI-related things. These values are optional since we have provided default settings.

  • The execute() method is where the magic happens ✨. We create a new BreedGenerator instance with the configured values and call its generate() method. Based on the result, we set the project’s exit status.

The EXIT_SUCCESS and EXIT_FAILURE constants indicate whether the operation was successful or not (see the this.project.exitStatus() call at line 37).

Build & Publish the Extension

Now that we have everything in place, let’s build the extension:

1
./bld clean compile

The extension is ready to be used. Well, almost. ☝️ We need to publish it to the local Maven repository.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
./bld publish-local

...
The jar archive was created at '/Users/tamas/git/bld-sandbox/breedgenerator/build/dist/breedgenerator-0.1.0-javadoc.jar'
Publishing to '/Users/tamas/.m2/repository'
Storing: /Users/tamas/.m2/repository/com/github/tcsizmadia/dognames/breedgenerator/0.1.0/breedgenerator-0.1.0.jar ... done
Storing: /Users/tamas/.m2/repository/com/github/tcsizmadia/dognames/breedgenerator/0.1.0/breedgenerator-0.1.0-sources.jar ... done
Storing: /Users/tamas/.m2/repository/com/github/tcsizmadia/dognames/breedgenerator/0.1.0/breedgenerator-0.1.0-javadoc.jar ... done
Storing: /Users/tamas/.m2/repository/com/github/tcsizmadia/dognames/breedgenerator/0.1.0/breedgenerator-0.1.0.pom ... done
Storing: /Users/tamas/.m2/repository/com/github/tcsizmadia/dognames/breedgenerator/maven-metadata-local.xml ... done
Publishing finished successfully.

The process is very similar, if you need to publish the extension to a different repository (like Maven Central, for serious extension developers) - you should use the publish command.

Use the Extension

Now that we have published the extension, we can get rid of the custom command with all the related code in the backend project - and replace it with the extension.

Let’s switch back to the backend project and fix the dependencies in the lib/bld/bld-wrapper.properties file:

1
bld.extensions=com.uwyn.rife2:bld-spring-boot:0.9.7,com.github.tcsizmadia.dognames:breedgenerator:0.1.0

☝️ Please do not forget to remove the bld.extensionsforcommands property, as I pointed out earlier.

Now, we need to update the generateBreeds() method in the BackendBuild.java file to use our fine extension:

1
2
3
4
5
6
7
8
9
    @BuildCommand(summary = "Generate dog breeds", value = "generate-breeds", help = BreedGeneratorHelp.class)
    public void generateBreeds() throws Exception {
        new BreedGeneratorOperation()
                .fromProject(this)
                .model("llama3.1")
                .ollamaHost("localhost")
                .ollamaPort(11434)
                .execute();
    }

Nice and clean. ✨

Finally, remove the BreedGenerator.java and BreedGeneratorHelp.java classes from the src/bld/. directory. After fixing the import statements…

1
2
import com.github.tcsizmadia.dognames.breedgenerator.extension.BreedGeneratorHelp;
import com.github.tcsizmadia.dognames.breedgenerator.extension.BreedGeneratorOperation;

…it’s time to test the extension in action!

1
./bld generate-breeds

You should find your freshly generated dog breeds CSV file in the src/main/resources directory.

This time we used our brand new extension - in a nice and clean way. Fantastic job! Let’s finish this tutorial with a few closing thoughts.


🚀 Conclusion

“However, with Bld, authoring a custom command or even an extension feels so natural that you don’t even realize you’re writing one.”

Thank you for sticking with me through this - I must admit, quite long - tutorial. I hope you enjoyed the journey and learned something new about building your projects using Bld. Let’s summarize what we’ve done:

  1. We started with a simple Spring Boot application and built it using Bld, with Java code only.
  2. We added a custom command to extend the build process with an AI-generated dog breed list.
  3. We turned the custom command into an extension making it reusable across projects (and make our build process clean and neat).

After concluding our journey with a pure, Java-based build experience, it begs the question: is Bld the right tool? Should I replace Maven with it?

Well, I don’t think so. It’s not that Bld is a bad tool, but build tools aren’t like coffee brands; you can’t just switch from one to another on a whim.

I experimented with Bld for a few days before writing this tutorial, and I must say, I really enjoyed using Java files to describe, execute, and iterate over my builds.

I felt in control of every nuance of my builds and was tempted to write custom commands here and there to fix or enhance operations. I must confess, having never authored a Maven plugin, I can’t compare the two tools in terms of extensibility. However, with Bld, authoring a custom command or even an extension feels so natural that you don’t even realize you’re writing one.

This is something I’ve never experienced with Maven or any other build tool.

On the other hand, Bld is a newcomer, and isn’t as mature as Maven or Gradle — and it probably never will be. While you have the freedom to author your own extensions if you need a feature that isn’t available yet, you can’t expect to find a ready-to-use solution for every problem you encounter.

Yet again, Bld is faster than Maven and Gradle, but I’m afraid not significantly enough to be a decisive factor. Perhaps with a really large project, I might think differently. However, here’s the catch: would you start migrating your existing, likely mission-critical business projects to Bld?

Neither would I.

Also, like Maven or Gradle, Bld is not designed for building monorepos with mixed languages and technologies. You might be able to hack it using extensions, but you’ll never get the same experience as with Bazel, Buck or Pants (spoiler alert: one of them will be discussed here soon). Bld is for Java, period.

So Who Should Use Bld?

Should I use Bld?

✅ For hobbyists and professionals working on small to medium-sized projects without tight budget or time constraints, Bld can be a great choice. It’s a joy to use, and defining the build process with Java code is a unique experience.

✅ Additionally, it might be a good remedy for those who need to apply a lot of custom logic in their builds, especially for challenging tasks that lack a ready-to-use plugin in Maven or Gradle.

❌ For business-critical projects already using Maven or Gradle, I would not recommend migrating to Bld.

❌ If you plan to start an open-source project, you might still want to stick with Maven or Gradle, as they are more widely used and can attract more contributors. Of course, no one stops you from using Bld alongside your pom.xml or build.gradle files. However, expect confusion and a lot of extra effort to maintain both build setups.

🤔 As a potential solution, I would like to see a tool that can generate Bld configurations from Maven or Gradle projects. This might work for one-time migrations. However, maintaining both build configurations presents a chicken-and-egg problem: you need a build tool to generate the configuration for another build tool. So, why not just use the original build tool directly? 🤷 To make this a feasible solution, the third-party tool must be Bld itself.

All of this could change in the future. If Bld can add new features, such as more performance boosts, better support for multi-module projects, and seamless migration from other tools, it might become a serious contender.

If I’ve kindled your interest in Bld, I encourage you to give it a try and share your thoughts with me. I’m curious to hear about your experience with this tool!

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy