Builds (Gradle)

Builds (Gradle)

For building our application, we are going to use Gradle, a modern build system that provides more functionality than make and is more suitable for complex projects. Gradle is popular in the Kotlin and Java ecosystems, and is the Google-endorsed build tool for Android projects.

Gradle is always setup in the context of a project: a specific directory structure, and a set of configuration files that define how your source code will be built. You create the Gradle project, and then your source code (and other assets) are added to that directory structure.

Setup

Gradle should be setup as the build system when you create your project in IntelliJ or Android Studio. Projects can be created with a variety of configurations, but for this course, you should choose Kotlin as your programming language, Gradle for your build system, and Kotlin for your DSL language.

New project wizard
Your dialog may look slightly different dependending on the version of IntelliJ and plugins you have installed.

The result of this process is a new project with a directory structure and configuration files that are ready to use with Gradle. Note that the project will be specific to the type of project that you chose e.g., Android, Desktop, Web Service.

The section below will describe the different styles of projects in more detail.

You will also need a JDK installed to proceed. We recommend JDK 21, which is the latest long-term release that is fully supported.

Wrapper

At the top-level of your project’s directory structure will be a script named gradlew (or gradlew.bat for Windows users). This is the Gradle wrapper: a script that you can use to run Gradle tasks without having to install Gradle on your machine. You can run it and pass it the same command-line arguments that you would normally pass to Gradle.

$ ./gradlew help
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :help

Welcome to Gradle 7.5.1.

To run a build, run gradlew <task> ...
To see a list of available tasks, run gradlew tasks
To see more detail about a task, run gradlew help --task <task>
To see a list of command-line options, run gradlew --help
For troubleshooting, visit https://help.gradle.org
When you run a task using the Gradle wrapper, it downloads and uses the appropriate version of Gradle. If you are running tasks from the command-line, you should always use the Gradle wrapper.

Gradle Tasks

To run Gradle tasks from the command line, use the Gradle wrapper with the appropriate the task name. We can run ./gradlew tasks to see a list of tasks that are supported in your project. Commonly used tasks include:

  • ./gradlew help will provide online help.
  • ./gradlew tasks will list all of the tasks that are available.
  • ./gradlew clean will remove temporary build files.
  • ./gradlew build will build your project.
  • ./gradlew run will usually run your project (depending on the platform).

IntelliJ IDEA and Android Studio also allow you to run Gradle tasks directly within the IDE, as long as the Gradle plugin is installed.

Gradle window
`View` > `Tool Windows` > `Gradle` to view the Gradle window in your IDE.

Build Configuration

When you create a new project with Gradle, it will create a directory structure for you. This directory structure is opinionated: Gradle requires a specific directory structure to work correctly, so you should work within the structure that it gives you.

The sections below describe the different types of projects that you can create.

Single-Target Desktop

The default Gradle project is meant to build a console or desktop application. All code resides in a single module e.g., app in the example below.

To create this style of project, launch IntelliJ IDEA.

  • From the Splash screen in IntelliJ IDEA, select New Project, and Kotlin as the project type.
  • Name is the name of the folder that will contain your source code. e.g., “DesktopApp”, and Location is the location of your Git working copy.
  • Click Create to proceed. If successful, IntelliJ IDEA will open into the main window to a starting project.

Your project should have this structure.

Empty Project Window

Explanation

  • app: the top-level code module.
    • build.gradle.kts: the main Gradle configuration file, specific to this module.
    • src/main: source code location
    • src/test: corresponding unit tests
  • gradle: the Gradle configuration files
    • libs.version.toml: the “version catalog” which contains dependency info (see below)
    • wrapper/gradle-wrapper.jar: bootstrap Gradle downloader
    • wrapper/gradle-wrapper.properties: information on Gradle version, download location
  • gradle.properties: optional environment variables
  • gradlew: Gradle wrapper (see above)
  • gradlew.bat: Gradle wrapper (see above)
  • settings.gradle.kts: top-level configuration file

Single-Target Android

If you have the Android plugin installed in IntelliJ, you will have the option to create a dedicated Android project.

You normally create an Android project in the IntelliJ New Project Wizard, and using one of the Android templates. I typically use the Empty Activity.

From the Splash screen in Android Studio:

  • Select New Project.
  • Select Empty Activity and Next.

A second screen will prompt you for project parameters. Fill in the following:

  • Name: Your application name
  • Package name: A unique package name.
  • Save location: Your working copy location.
  • Minimum SDK: API 26 or later.
  • Build configuration language: Kotlin DSL.

Click Finish and your project should be created. Your directory structure will be the same as the Desktop example, with a couple of differences:

  • You will have Test and AndroidTest for test source sets, instead of a single folder.
  • You will have a Manifest file in the root of your project which contains Android specific configuration information.
  • Your application will launch from a MainActivity.kt file instead of Main.kt.

Multi-Project (App+Lib)

What do you do if you want to build a more complex project e.g., a combination of desktop, Android, web service and so on? In Gradle, this is known as a multi-project build, and it allows you to build multiple projects from a single top-level project.

This structure is useful when you have multiple projects that depend on each other, or when you want to share code between projects.

There is a specific structure that you need to follow to create a multi-project build. In the example below, we have subprojects application, models and server which represent different projects.

You would typically create this project by using the IntelliJ New Project Wizard to create a starting project and then manually adding the others. Creating this structure isn’t directly supported by the IDE.

.
├── application
│   ├── build.gradle.kts
│   └── src
├── gradle
│   └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
├── models
│   ├── build.gradle.kts
│   └── src
├── server
│   ├── build.gradle.kts
│   └── src
└── settings.gradle.kts

Notice that the top-level modules have been named application, models, server. Each of these represents a different type of project, with it’s own build.gradle.kts file that defines how that particular module should be built. e.g.,

  • application is a console application, so its build.gradle.kts will contain a plugin for application. If you build that sub-project, you will get that style of application produced.
  • models is a shared library, meaning that it contains classes that are imported by both the application and server projects. e.g., data classes.
  • server is a server, which runs standalone on a web server. It has specific server-like dependencies e.g., Ktor. If you build this project, you get a JAR file that can be installed on a web server.

To create a multi-project build, you need to create subprojects for each project that you want to manage. Each subproject should have its own build.gradle.kts file, and a src directory containing the source code for that project. The top-level settings.gradle.kts describes common dependencies and the overall project structure.

Kotlin Multiplatform (KMP)

This project-type is suitable for targetting two or more targets with your application e.g., Desktop/JVM + Android. If you don’t mind the folder structure, it can also be used for any single project as well!

From the Splash screen in IntelliJ IDEA:

  • Select New Project.
  • Choose Kotlin Multiplatform from the list of project types.

Provide the following:

  • Name is the name of the folder that will contain your source code. e.g., “DesktopApp”.
  • Location is the location of your working copy (from Step 1 above).
  • Group is a reverse-DNS name, used for the top-level package name.
  • Artifact is the single-word name of the project.
  • JDK should point to your JDK installation (see toolchain).

Select the platforms that you wish to target, and enable Share UI (which will default to using Compose for all platforms).

Click Create to proceed. If successful, IntelliJ IDEA will open into the main window to an empty project.

Empty Project Window

You should be able to expand the folders to an individual target under the composeApp folder, and click the Run button in the toolbar to execute it.

FAQ

How do I specify the version of Gradle?

The Gradle project configuration (gradle/gradle-wrapper.properties) lists the version of Gradle to be used for your project.

To change the version of Gradle being used, update the gradle-wrapper.properties file, and change the distributionURL line to the correct version e.g., Gradle 8.0.2 below.

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

The gradle-wrapper.properties file should be stored in Git, as part of your build configuration scripts.

Check that the Kotlin and Gradle versions are compatible. You can find this information in the Kotlin compatibility matrix. You can probably just use the version that the project wizard selected for you when the project was created.

How can I tell Gradle what version of the JDK to use?

Gradle will use the JDK in the class path on your system to execute tasks. If you are using IntelliJ IDEA, you can also specify the version to use in the IDE settings: Settings > Build Tools > Gradle.

Often you want to tell Gradle to compile your code with a specific JDK version. You can specify this in your project configuration files. For example, here’s how you specify JDK 21.

// add this to your build.gradle.kts
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}
// add this to your settings.gradle.kts so Gradle can download the JDK
plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0")
}

How do I create an installer for my program?

For a Compose desktop application, there is a set of Gradle packaging tasks: Gradle > application > Tasks > compose desktop > package. You probably want to packageDistributionForCurrentOS which will build a regular installer for the OS that you’re currently using.

For Android, it’s acceptable to build an APK file (Build > Generate APK) instead of a full installer. This file can be drag-dropped onto a running AVD to run the program.

There are also third-party installation tools if you want something more sophisticated. e.g. Conveyor.

How can I fix an installer that fails?

Here are some common errors to watch out for (all related to the build.gradle.kts configuration file):

  1. You need to specify that you want to include all modules in the installer.
nativeDistributions {
        includeAllModules = true
        targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
        packageName = "cs346-project"
        packageVersion = "1.0.0"
}
  1. The version property defaults to “1.0-SNAPSHOT”. You MUST change this to the format “1.0.0” - see Semantic Versioning. You should probably be incrementing this with each software release/demo.
group = "com.example"
   version = "1.0.0"
  1. It often takes some time for libraries to be updated to the “newest” JDK release. Best-practice is to use a JDK that has been stable for some time to avoid incompatibilty issues (see required software). Make sure to set the JDK in the project settings (File > Project Structure > Project) and the Gradle project settings (IntelliJ > Settings > Build > Gradle)

If your installer fails when you execute it, try checking some of the other related tasks in the Gradle Menu in IntelliJ. e.g. compose desktop > runDistributable. The Gradle tasks in this section represent the subtasks that the installer builds and if you run them in the IDE, you will get more detailed feedback than you would from running the installer manually.

Final Word

It’s staggering how much software is available through package repositories…

XKCD Dependency
https://xkcd.com/2347