Conveyor

A simpler and more elegant build tool for Kotlin – an idea and a work in progress project.

Motivation

Kotlin lacks good build tools.
Maven is overly verbose. Gradle is overly complex. Creating custom tasks in Maven is frustrating. Attempting to manage tasks in Gradle will have you ripping your hair out.

There isn't really a good option if you want a simple build tool for Kotlin. Initially, I set out to write a complete build tool on my own. The idea was to create a nice little build tool that uses YAML for its configuration. But build tools are complex, and the building parts of Gradle and Maven are completely fine, it's their configuration and task managements that are a chore.

And so I decided to start out by writing a tool that converts a YAML configuration into a valid Maven configuration. This worked pretty well and solved managing dependencies. But it brings in another step that the developer has to go through to compile their program, and it didn't fix the task management part. That's where I decided to bring in Task, to manage our tasks and provide some useful custom tasks to avoid having to directly interface with Maven.

The project (WIP)

The project ended up being simpler than expected, thanks to the project delegating all the hard work to Maven and Task. The project is essentially split into two main parts:

  1. The YAML to Maven converter
  2. The Taskfile

The YAML to Maven converter

This converter is the part that is doing all the hard work. It's more than just a converter. It's really the core of the project, and handles tasks such as creating new projects, checking if the configuration is valid, and so on.

The 'converter', or really the conveyor core, is built as a CLI which Task then uses (more on that in the Taskfile section). Its main responsibility is managing the conveyor file, conveyor.yaml. This is where our build configuration lives. The conveyor file acts as a single source of truth for how we should handle project settings and dependencies. On generating a new project, the following conveyor file is generated in the project root:

project:
  name: myProject
  group: com.example
  version: 0.0.1

  kotlinVersion: 2.0.0
  jvmTarget: 1.8  # Valid values: 1.8, 9, 10, ..., 21. Default 1.8
  mainClass: "com.example.myProject.MainKt"
  versions:

  plugins:

  dependencies:

  testDependencies:

The conveyor file is quite simple and the main sections are clearly separated. First we have metadata about our project, then we have some language and build configuration.
The versions block is supposed to allow us to define "version variables" to reuse the same version, for example a version variable for Ktor dependencies.
In the other blocks we can define our plugins and dependencies.

Here is an example of a more "full" conveyor file, that one might find in a normal project:

project:
  name: myProject
  group: com.example
  version: 0.0.1
  
  versions:
    kotlin: 2.0.0
    ktor: 2.3.12
    kotest: 5.9.1

  kotlinVersion: "$kotlin"
  jvmTarget: 21  # Valid values: 1.8, 9, 10, ..., 21. Default 1.8
  mainClass: "com.example.myProject.MainKt"
  
  plugins:
    - "kotlinx-serialization:org.jetbrains.kotlin:kotlin-maven-serialization:$kotlin"
    
  dependencies:
    - "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin"
    - "io.ktor:ktor-server-core:$ktor"
    - "io.ktor:ktor-server-netty:$ktor"
    - "ch.qos.logback:logback-classic:1.4.14"
    
  testDependencies:
    - "io.kotest:kotest-runner-junit5:$kotest"
    - "io.kotest:kotest-assertions-core:$kotest"
    

The Taskfile

Task handles all of our commands. It's what we use in most cases to interact with our CLI. By default, it provides us the following commands:

  1. Build task, to convert or conveyor file to a Maven pom.xml file
  2. Test task
  3. Compile task
  4. Run task
  5. Clean task

By a happy accident, Task also uses YAML for its configuration of the Taskfile, which keeps things consistent.