I’m back! We’ve decided to go head with Sunshower full time, so expect updates much more regularly here!
Last time, we looked at getting a simple build dockerized. Using the Go platform made this pretty simple for us from a dependency perspective, but a lot of you are using a dependency resolution tool like Gradle, Maven, Crate, or Ivy. This post will detail how to configure Maven and Gradle so that your dependencies are manageable and consolidated–a necessary prerequisite for any sane build/release process.
The Base Project
Sunshower.io has quite a few individual projects, each of which having at least several sub-projects. The first project that we need to build is sunshower-devops. This project contains
- Docker container definitions
- Bill-of-material POMs that are used by each of the sunshower.io projects
- Various scripts bundled with our Docker images.
Recall that last time, the first thing I recommended was for you to aggregate all of your dependencies. We needed that information because it allows us to build a bill-of-materials for our project. This is important because it allows us to understand clearly what our project pulls in. This in turn enables us to manage our dependencies in a revisionable and deterministic fashion. Let’s look at what one of our bills-of-material POMs looks like:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>io.sunshower.env</groupId> <artifactId>persist-dependencies</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <parent> <groupId>io.sunshower.env</groupId> <artifactId>env-aggregator</artifactId> <relativePath>../pom.xml</relativePath> <version>1.0.0-SNAPSHOT</version> </parent> <name>Sunshower Persistence Dependencies</name> <scm> <url>https://github.com/sunshower-io/sunshower.io</url> </scm> <properties> <hibernate.version>5.1.10.Final</hibernate.version> ... other properties </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.version}</version> </dependency> ... Other dependencies </dependencyManagement> </project>
Basically, this is just a standard Maven POM file with a structure that is convenient for declaring dependencies. The first thing to note is that dependencies are declared within a <dependencyManagement>
tag. This means that POM files that inherit from this, or import it, will not automatically depend on the dependencies declared within, only that if they explicitly declare a dependency from this POM, they will inherit its configuration as it appears in this declaration. For instance, if I import sunshower-env:persist-dependencies
, then if I declare org.hibernate:hibernate-entitymanager
in my importing POM, I will get the ${hibernate.version}
version declared in persist-dependencies
without having to redeclare it.
Basically, what we’re going for is this:
- We create bill-of-material (BOM) POMs for each category of dependency. This is optional, but I like it because these suckers can get huge otherwise.
-
If we have commonality between our BOM POMs (and we will), we pull it up into an aggregator POM.
-
We import each of our BOM POMs into our parent pom (sunshower-parent)
-
Every subproject in our system will have its own BOM POM that derives from sunshower-parent
- Each gradle file for each project uses the spring-maven-gradle plugin to import its BOM pom
- Viola! If we add a dependency, that addition is recorded in Git. We can see exactly what we’re pulling in for any release (and go back to a previous POM if we need to)
Visually:
Now, say I want to use hibernate-entitymanager
in sunshower-base:persist
:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>io.sunshower.base</groupId> <artifactId>bom</artifactId> <version>1.0.0-SNAPSHOT</version> <relativePath>../</relativePath> </parent> <groupId>io.sunshower.base</groupId> <artifactId>bom-imported</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <name>Sunshower.io Imported Bill-Of-Materials</name> <url>http://www.sunshower.io</url> <properties> <env.version>1.0.0-SNAPSHOT</env.version> </properties> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> </dependency> </dependencies> </project>
Then, I simply import that into my build.gradle
file for whichever project depends on hibernate-entitymanager
dependencyManagement { imports { mavenBom("io.sunshower.base:bom-imported:${version}") } }
Now, in that project (or any subproject thereof), I can just add hibernate-entitymanager
to the dependencies
block:
dependencies { implementation 'org.hibernate:hibernate-entitymanager' }
Conclusion
While this may seem like overkill, I like it because it scales quite well. It’s easy to audit (assuming you enforce the process), maintainable (dependencies are grouped together sensibly), and forces you to think about what you’re bringing in. Sometimes incompatibilities can be prevented simply by looking through the dependency lists and determining whether two versions are compatible. Finally, it gives a consistent view of the world to everyone in the project: if everyone contributing to the project follows the rules, you won’t get one component consuming one version of a dependency, and another component consuming another, which is a common source of bad builds IME.
1 comment