Introduction into C++ Builds with Gradle

Welcome back to a new post on thoughts-on-cpp.com. In today’s post, I would like to give an introduction to the build system Gradle and how we can use it to build native applications and libraries. Gradle is originally coming from the Java world, but it’s also supporting native build toolchains for quite a while. Gradle is becoming more and more popular in the Java world and is on a good way to rule out the old bull Maven. This is because of two features which we can also benefit from in the native (C/C++, Objective-C/C++, Assembly, and Windows resources) build world. These features are Gradle’s easy to maintain and very expressive Groovy (or Kotlin if preferred) based DSL, and it’s capabilities of dependency resolving via online and on-premise library providers (such as maven-central, Artifactory, Bintray, etc.) or local repositories.

Let’s start with a multi-project example we already know from my post Introduction into an Automated C++ Build Setup with Jenkins and CMake. I have just slightly changed the example hello world application. This time the main function is printing out “Hello World!” onto console using a shared library, called greeter. The Greeter class itself is utilizing the external library {fmt} to print “Hello World” onto the screen. If you’ve wondered about the Gradle directory and files, those are provided by the Gradle wrapper which facilitates us to build the project without even installing Gradle upfront.

.
├── app
│   ├── build.gradle
│   └── src
│   └── main
│   └── cpp
│   └── main.cpp
├── build.gradle
├── gradle
│   └── wrapper
│   ├── gradle-wrapper.jar
│   └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── greeter
│   ├── build.gradle
│   └── src
│   └── main
│   ├── cpp
│   │   └── greeter.cpp
│   └── public
│   └── greeter.hpp
├── LICENSE
├── README.md
├── settings.gradle
└── testLib
   ├── build.gradle
   └── src
   └── test
   └── cpp
   └── greetertest.cpp
view raw gradleTree.txt hosted with ❤ by GitHub

The Gradle native build plugin is quite straight forward to configure. Every Gradle project needs a build.gradle file at its root directory as an entry point and one at each subproject. In most cases, we will do general configurations in a build.gradle file located at the root directory. But there is no need, it can also be empty. By default, Gradle is looking for sources in the directory src/main/cpp. For libraries, public headers are defined in src/main/public and in case they should be used only library internal (private) the default directory is src/main/headers. In case we define the headers also in src/main/cpp, the headers are treated as private as well. If we like to overwrite the default source directories we just need to define them according to this example. To be able to resolve dependencies between subprojects, we need to define a settings.gradle file which is including our subprojects include 'app', 'greeter', 'testLib'.

components.withType(ProductionCppComponent) {
//By convention, source files are located in the root directory/Sources/
source.from rootProject.file("Sources/${subproject.name.capitalize()}")
privateHeaders.from rootProject.file("Sources/${subproject.name.capitalize()}/include")
}
components.withType(CppLibrary) {
//By convention, public header files are located in the root directory/Sources//include
publicHeaders.from rootProject.file("Sources/${subproject.name.capitalize()}/include")
}

As build definition of our root directory we just simply define IDE support for each subproject. CLion, for example, has native Gradle support, so importing Gradle projects works smooth as silk. Therefore our root build.gradle file looks like the following.

allprojects {
apply plugin: 'xcode'
apply plugin: 'visual-studio'
}

The application build configuration is defined at the app directory starting with calling the cpp-application plugin which is generating an executable file which can be found and executed at app/build/install/main/{buildType}/{machine}. Project internal dependencies can be defined by the dependencies clause with the implementation of the dependency defined as a project and the given name of the dependency. By default, Gradle is assuming the current host as target machine. If we want to consider other target machines we have to declare them as we do in our example with the targetMachines statement.

plugins {
id 'cpp-application'
}
application {
dependencies {
implementation project(':greeter')
}
targetMachines = [
machines.windows.x86_64,
machines.macOS.x86_64,
machines.linux.x86_64
]
baseName = "app"
}
view raw appbuild.gradle hosted with ❤ by GitHub

The library build configuration is defined at the greeter directory starting with calling the cpp-library plugin and the type of linkage, which can be STATIC and SHARED. Gradle is assuming we want SHARED libraries as default. A bit special is the way of how we have to resolve the dependency to the header only library {fmt}. Unfortunately, Gradle is not supporting header only libraries out of the box, but we can accomplish a workaround by adding the include path to the includePathConfiguration of the resulting binary. All other dependencies can be defined as api, in case we want to share the external dependency api with all consumers of our own defined library, or implementation in case we only want to use the dependency api private with our own library. A good example can be found in Gradle’s example repository.

plugins {
id 'cpp-library'
}
library {
linkage = [Linkage.SHARED]
targetMachines = [
machines.windows.x86_64,
machines.macOS.x86_64,
machines.linux.x86_64
]
baseName = "greeter"
}
def fmtHeaders = file("$rootDir/../fmt/include")
components.main.binaries.whenElementFinalized { binary ->
project.dependencies {
if (binary.optimized) {
add(binary.includePathConfiguration.name, files(fmtHeaders))
} else {
add(binary.includePathConfiguration.name, files(fmtHeaders))
}
}
}
view raw greeterbuild.gradle hosted with ❤ by GitHub

With Gradle, we can not only build applications and libraries, but we can also execute tests to check the resulting artifacts. A test can be defined by the cpp-unit-test plugin which is generating a test executable. In principle, we could use any of the existing big test libraries, such as googletest, but in my opinion, the out of the box solution is pretty neat and lightweight and can be extended quite easily with external libraries.

plugins {
id 'cpp-unit-test'
}
unitTest {
dependencies {
implementation project(':greeter')
}
baseName = "greeterTest"
}
view raw testbuild.gradle hosted with ❤ by GitHub

With this project setup, we can build all artifacts by the command ./gradlew assemble and run tests by ./gradlew check. If we want to build and run all tests together we can invoke ./gradlew build. In case we need a list of all available tasks provided by Gradle and its plugins we can simply list them including their description by the command ./gradlew tasks. At GitHub you can find the resulting repository.

[bmahr@localhost gradleNative]$ ./gradlew tasks
> Task :tasks
————————————————————
Tasks runnable from root project
————————————————————
Build tasks
———–
assemble – Assembles the outputs of this project.
build – Assembles and tests this project.
clean – Deletes the build directory.
Build Setup tasks
—————–
init – Initializes a new Gradle build.
wrapper – Generates Gradle wrapper files.
Help tasks
———-
buildEnvironment – Displays all buildscript dependencies declared in root project 'gradleNativ'.
components – Displays the components produced by root project 'gradleNativ'. [incubating]
dependencies – Displays all dependencies declared in root project 'gradleNativ'.
dependencyInsight – Displays the insight into a specific dependency in root project 'gradleNativ'.
dependentComponents – Displays the dependent components of components in root project 'gradleNativ'. [incubating]
help – Displays a help message.
model – Displays the configuration model of root project 'gradleNativ'. [incubating]
projects – Displays the sub-projects of root project 'gradleNativ'.
properties – Displays the properties of root project 'gradleNativ'.
tasks – Displays the tasks runnable from root project 'gradleNativ' (some of the displayed tasks may belong to subprojects).
IDE tasks
———
cleanVisualStudio
cleanXcode – Cleans XCode project files (xcodeproj)
openVisualStudio – Opens the Visual Studio solution
openXcode – Opens the Xcode workspace
visualStudio
xcode – Generates XCode project files (pbxproj, xcworkspace, xcscheme)
Verification tasks
——————
check – Runs all checks.
runTest – Executes C++ unit tests.
Rules
—–
Xcode bridge tasks begin with _xcode. Do not call these directly.
Pattern: clean<TaskName>: Cleans the output files of a task.
To see all tasks and more detail, run gradlew tasks –all
To see more detail about a task, run gradlew help –task <task>
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
view raw gradleTasks.txt hosted with ❤ by GitHub
gradlebuild

UPDATE: Thanks to D. Lacasse I updated the source set configuration section to customize the folder structure of a project.

Did you like the post?

What are your thoughts?

Feel free to comment and share this post.

Processing…
Success! You're on the list.

11 thoughts on “Introduction into C++ Builds with Gradle

      1. For VS an Xcode there are plugins to generate project files as CMake does. CLion is supporting Gradle projects out of the box. I don’t know about linters (what are you using?) and Conan. But Conan is made by, like Bintray and Artifactory, jfrog. They normally provide Gradle plugins. CPPCheck has also a plugin available.

        Like

  1. Great post Benjamin! We will soon be releasing better documentation for those new plugins.

    Those are great comment Lesley and are all things we are considering to add support or at the very least show how to add support in Gradle.

    Like

  2. did you try the caching feature of gradle with C++? I’m hoping that it would solve me my two main problems – fast incremental builds on gates that switch branches often & incremental unit-testing (if the dependencies are correct, running the unit-tests should not be required if the files used in that unit-test were not built).

    Any experience with this?

    Like

    1. I never tried by intention but as far as i know gradle is doing chaching by default. Unit-Tests should be also changed as far as nothing changed on the dependency chain. But i never tried. I’ve you get experience in this topic i would be glad to here from.

      Like

      1. If you add org.gradle.caching=true to gradle.properties, rebuilds are very fast.
        Example:
        ./gradlew –no-build-cache clean build
        BUILD SUCCESSFUL in 1m 26s

        ibmadmin@ibmadmin-VirtualBox:~/repos/kohelder$ ./gradlew clean build

        BUILD SUCCESSFUL in 7s

        I do not represent IBM in this post.

        Like

  3. Thank you for your post. After digging pointlessly in Gradle’s documentation, following endless links getting nowhere, I found that it is the only post that gets straight to the point showing how to build C/C++ project in Gradle. Actually, your post convinced me finally, that Gradle should stay in Java world and C/C++ project should be built with other tools, such as make (first and preferred option), Meson, Bazel, CMake. In any way, your project requires C++11 support by default while GCC/G++ requires -std=c++11 flag to compile C++11 code. How can I change compiler flags in gradle without pain?

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.