Skip to content

Dependency Management

For Jeka, a dependency is something that can be resolved to a set of files by a JkDependencyResolver. Generally a dependency resolves to 1 file (or folder) but it can also be 0 or many.

Compared to mainstream build tools, Jeka offers a simpler and more flexible model to deal with multiple dependency configurations required to build a project. See project dependencies

Types of Dependency

A dependency is always an instance of JkDependency.

Jeka distinguishes between 3 main types of dependency :

  • Arbitrary files located on the file system (represented by JkFileSystemDependency class). These files are assumed to be present on the file system while the build is running.
  • Files produced by a computation (represented by JkComputedDependency class). These files may be present on file system, or not. If they are not present, the computation is run in order to produce the missing files. Usually the computation stands for the build of an external project.
  • Coordinate pointing to a remote artifact (represented by JkCoordinateDependency) hosted in a binary repository (Ivy or Maven for instance) : Jeka can consume and transitively resolve any artifact located in a repository as you would do with Maven, Ivy, or Gradle.

Lastly, Jeka uses Ivy 2.5.0 under the hood. Jeka jar embeds Ivy and executes it in a dedicated classloader to be hidden for client code.

image

Coordinate dependency

This type of dependency is represented by a JkCoordinateDependency class. It stands for a Maven/Ivy dependency expressed with coordinates (e.g. _group:module:version).

This is for declaring a dependency on a module hosted in the Maven or Ivy repositories. Basically you instantiate a JkCoordinateDependency from its group, name, and version.

JkDependencySet.of()
    .and(JkPopularModule.GUAVA, "18.0")
    .and("com.orientechnologies:orientdb-client:[2.0.8, 2.1.0[")
    .and("mygroup:mymodule:myclassifier:0.2-SNAPSHOT");

Many string formats are accepted to specify a module coordinate :

  • group:name
  • group:name:version
  • group:name:classifier:version
  • group:name:classifier:extension:version

Classifier can be either :

  • an empty string to specify the default classifier
  • a simple string as 'linux' to specify and retrieve a single classifier variant

Note

By default, dependencies specifying a classifier or an extension are not considered to be transitive. Although, transitivity can be explicitly configured.

Version can be either :

  • a static version number, as 1.0.2
  • a snapshot version, as 1.0.2-SNAPSHOT
  • a version range, as [2.0.8, 2.1.0[

Examples :

  • com.sun.jersey:jersey-server : specifies artifact without version
  • com.sun.jersey:jersey-server:1.19.4 : specifies artifact with version
  • org.lwjgl:lwjgl:natives_linux:3.1.0 : specifies artifact having natives_linux classifier and 3.1.0 version
  • org.springframework.boot:spring-boot-dependencies::pom:2.5.6 specifies artifact having .pom extension (to retrieve a BOM)

Note

  • A version ending by -SNAPSHOT has a special meaning : Jeka will consider it "changing". This means that it won't cache it locally and will download the latest version from repository.
  • As Jeka relies on Ivy under the hood, it accepts dynamic versions as mentioned here.
  • Dependency files are downloaded in [USER HOME]/.jeka/cache/repo

Additionally, it's possible to define the transitivity of the dependency using :

JkModuleDependency.of("group:name:sources:zip:version").withTransitivity(JkTransitivity.NONE);

By default, Jeka uses the most relevant transitivity according to the declaration context, so users don't need to specify it unless they want a specific one.

See later for more details about transitivity.

File System Dependencies

This type of dependency is represented by the JkFileSystemDependency class.

Just mention the path of one or several files. If one of the files does not exist at the time of resolution (when the dependency is actually retrieved), build fails.

JkDependencySet.of().andFiles("libs/my.jar", "libs/my.testingtool.jar");

Computed Dependencies

This type of dependency is represented by the JkComputedDependency class.

It is typically used for multi-modules or multi-techno projects.

The principle is that if the specified files are not present, the computation is run in order to generate the missing files. If some files still missing after the computation has run, the build fails.

This mechanism is quite simple yet powerful as it addresses the following use cases :

  • Dependencies on files produced by an artifact producer (JkArtifactProducer). A JkProject is an artifact producer.
  • Dependencies on files produced by external build tools (Ant, Maven, Gradle, SBT, Android SDK, Make, npm ...).
  • ... In other words, files produced by any means.

The generic way is to construct this kind of dependency using a java.lang.Runnable.

The following snippet constructs a set of dependencies on two external projects : one is built with Maven, the other with Jeka.

Path mavenProject = Paths.get("../a-maven-project");
JkProcess mavenBuild = JkProcess.of("mvn", "clean", "install")
        .withWorkingDir(mavenProject);
Path mavenProjectJar = mavenProject.resolve("target/maven-project.jar");
JkJavaProject externalProject = 
        JkJavaProject.ofSimple(Paths.get("../a-jeka-project")); 
JkDependencySet deps = JkDependencySet.of()
    .and(JkComputedDependency.of(mavenBuild, mavenProjectJar))
    .and(externalProject);

Dependency Set

A dependency set (JkDependencySet) is an ordered bunch of dependencies used for a given purpose (compilation, war packaging, testing, ...). It can contain any kind of JkDependency. See here

dependencySet also defines :

  • A version provider to define which version of a module should be used in case it is not explicitly mentioned.
  • A set of transitive dependency exclusion rules.

It is designed as an immutable object where we can apply set theory operations for adding, removing, or merging with other dependencies and dependencySet.

Example

JkDependencySet deps = JkDependencySet.of()
    .and("com.google.guava") 
    .and("org.slf4j:slf4j-simple")
    .and("com.orientechnologies:orientdb-client:2.0.8")
    .andFile("../libs.myjar")
    .withVersionProvider(myVersionProvider);

Note

  • Module versions and scopes can be omitted when declaring dependencies. Versions can be provided by a JkVersionProvider.
  • Instances of JkDependencySet can be combined in order to construct a large dependencySet from smaller ones.

Transitivity

For each dependency, mainstream build tools use a single concept (scope or configuration) to determine :

  1. which part of the build needs the dependency
  2. which transitive dependencies to fetch along the dependency
  3. with which transitivity the dependency must be published

This confusion leads to dependency management systems that are bloated, difficult to reason with, and not quite flexible. Gradle comes with a proliferation of 'configurations' to cover most use case combinations, while Maven narrows 'scopes' to a smaller number, but with limitations and not-so-clear transitivity/publish rules.

In the opposite, Jeka distinguishes clearly the following three purposes :

  1. Jeka uses distinct dependencySet instances for each part of the build (compile, runtime, test,...). Each can be defined relatively to another using set theory operations.
  2. For each dependency, we can decide its transitivity, that is, the transitive dependencies fetched along the dependency.
  3. For publishing, we can optionally re-define a specific dependencySet, exposing exactly what we want.

Jeka defines by default, three levels of transitivity :

  • NONE : Not transitive
  • COMPILE : Also fetches transitive dependencies declared with scope 'compile' in the dependency published pom.
  • RUNTIME : Also fetches transitive dependencies declared with any scope in the dependency published pom.

Notes

On Maven repositories, published poms can declare only two scopes for transitive dependencies : 'compile' and 'runtime'.

For Ivy repositories, it is possible to declare a specific transitivity that maps to a slave 'configuration'.

The below example shows a JkJavaProject declaration using explicit transitivity.

JkJavaProject.of().simpleFacade()
    .configureCompileDeps(deps -> deps
            .and("com.google.guava:guava:23.0", JkTransitivity.NONE)
            .and("javax.servlet:javax.servlet-api:4.0.1"))
    .configureRuntimeDeps(deps -> deps
            .and("org.postgresql:postgresql:42.2.19")
            .withTransitivity("com.google.guava:guava", JkTransitivity.RUNTIME)
            .minus("javax.servlet:javax.servlet-api"))
    .configureTestDeps(deps -> deps
            .and(Hint.first(), "org.mockito:mockito-core:2.10.0")
    )
It results in :
Declared Compile Dependencies : 2 elements.
  com.google.guava:guava:23.0 transitivity:NONE
  javax.servlet:javax.servlet-api:4.0.1

Declared Runtime Dependencies : 2 elements.
  com.google.guava:guava:23.0 transitivity:RUNTIME
  org.postgresql:postgresql:42.2.19

Declared Test Dependencies : 4 elements.
  org.mockito:mockito-core:2.10.0
  com.google.guava:guava:23.0 transitivity:RUNTIME
  org.postgresql:postgresql:42.2.19
  javax.servlet:javax.servlet-api:4.0.1
Dependencies without any transitivity specified will take default transitivity for their purpose, namely COMPILE for compile dependencies, and RUNTIME for runtime and test dependencies.

The API allows you to redefine the transitivity declared in a upper dependency set.

Note that transitivity can only apply to JkModuleDependency (like com.google.guava:guava:23.0) and JkLocalProjectDependency.

Resolve Dependencies

The JkDependencyResolver class is responsible JkDependencyResolver.of(JkRepo.ofMavenCentral());to resolve dependencies by returning JkResolveResult from a JkdependencySet.

JkDependencySet deps = JkDependencySet.of()
                            .and("org.apache.httpcomponents:httpclient:4.5.3")
                            .andFile("libs/my.jar");

// Here, module dependencies are fetched from Maven central repo
JkDependencyResolver resolver = 
        JkDependencyResolver.of(JkRepo.ofMavenCentral());  
JkResolveResult result = resolver().resolve(deps);

From the result you can :

  • Navigate in the resolved dependency tree as :
JkDependencyNode slfjApiNodeDep = result.getDependencyTree()
        .getFirst(JkModuleId.of("org.slf4j:slf4j-api"));
System.out.println(slfjApiNode.getModuleInfo().getResolvedVersion());
  • Get the direct list of artifact files
JkPathSequence sequence = result.getFiles();  
sequence.forEach(System.out::println); // print each files part of the result

Publication

Jeka is able to publish on both the Maven and Ivy repositories. This includes repositories such as Sonatype Nexus.

Maven and Ivy have different publication models, so Jeka proposes specific APIs according to whether you want to publish on a Maven or Ivy repository.

Publish on a Maven repository

Jeka proposes a complete API to pubish on Maven repository. POM files will be generated by Jeka according to provided elements.

Find an example here

Notice that Jeka allows you to :

  • Publish more than one artifact.
  • Produce & publish checksum files for each published artifact.
  • Mention using unique snapshots (What is it ?).
  • Feed generated pom with data necessary to publish on central repository.
  • Sign published artifacts with PGP
  • Publish to multiple repositories by creating the publisher using a JkRepoSet instead of a JkRepo.

To sign with PGP, there is no need to have PGP installed on Jeka machine. Jeka uses Bouncy Castle internally to sign artifacts.

Publish to a Ivy repository

Publishing on Ivy repo is pretty similar to publishing on Maven, though there are options specific to Ivy.

See also here

Common Classes

  • JkRepo and JkRepoSet represent both download and upload repositories.

  • JkRepoFromPropertiesprovides configured repositories according to global or project scopes properties.

  • JkDependencySet represents a set of dependencies.

  • JkDependencyResolver resolves dependencies to classpaths and resolution results that allow resolution exploration.

  • JkModuleFileProxy provides an smart way to get a file from its coordinates.