Skip to content

Dependencies

A fabr project declares its Maven dependencies in the build file, on the targets that use them. Fabr reads each package’s pom.xml from the repository for what the package requires, downloads the jars a build needs when it needs them, and chooses versions by a rule that gives the same answer every time. This page covers declaring dependencies and how their versions are chosen.

With Maven or Gradle In fabr
<dependency> with <scope>compile</scope> / implementation, api A target’s deps.
<scope>runtime</scope> / runtimeOnly runtime_deps.
<scope>provided</scope> / compileOnly for a jar the runtime supplies provided_deps.
<optional>true</optional> optional_provided_deps.
compileOnly for a jar nothing needs at run time (annotations) compile_deps.
compileOnlyApi for a jar your public API names but nothing needs at run time compile_api_deps.
<scope>test</scope> / testImplementation test_deps.
annotationProcessor, <annotationProcessorPaths> processors; see Annotation processors.
A BOM in <dependencyManagement> with <scope>import</scope> / platform(…) The BOM in deps, test_deps or wherever its versions are used; see BOMs.
Versions in a parent POM’s <dependencyManagement> / a version catalog (libs.versions.toml) A catalog.
Nearest-wins conflict resolution The highest version anything requires; see How versions are chosen.
<classifier>linux-x86_64</classifier> / group:artifact:version:classifier @mvn:<group>:<artifact>/<classifier>:<version>; see Classified jars.
<type>zip</type> / group:artifact:version:classifier@zip @mvn:<group>:<artifact>/<classifier>@zip:<version>, or with no classifier <artifact>@zip; see Files other than jars.
<exclusions> in a dependency’s POM Read, as Maven reads them; see What fabr reads from a POM.
<exclusions> in your own build See Not supported.
<repositories> / repositories { maven { … } } A maven_repository; several combined with a repository_group.
<mirrors> in settings.xml MAVEN_REPOSITORY_URL; see Mirrors and proxies.
<proxies> in settings.xml The https_proxy environment variable; see Mirrors and proxies.
<servers> in settings.xml The same file; see Credentials.
mvn dependency:tree / gradle dependencies fabr deps <target>.
Gradle Module Metadata (.module) Read where the POM says it is published; see Gradle Module Metadata.
-SNAPSHOT versions Not supported, by design: fabr takes every version as never changing, and reads no snapshot metadata, so a snapshot’s latest build is never looked up. Depend on a release, or on the project’s own target.
The local repository, ~/.m2/repository (mvn install, mavenLocal()) Not read: what it holds depends on what was last installed on this machine. Depend on another module of the project directly, or publish the package to a repository fabr can read.

A dependency is written @mvn:<group>:<artifact>:<version>:

jar_package app {
srcs = src/main/java:**/*.java;
deps = @mvn:org.slf4j:slf4j-api:1.7.36 @mvn:com.google.guava:guava:33.2.1-jre;
}

@mvn is Maven Central unless you say otherwise; see Repositories. The version is a version (1.7.36), as Maven reads one, or a Maven version range in quotes: @mvn:com.example:lib:"[1.0,2.0)". Fabr shows a package’s name as <group>/<artifact>, as in org.slf4j/slf4j-api, and accepts it written that way too. Gradle’s dynamic versions (1.+, latest.release) and Maven’s LATEST and RELEASE are refused. Where a dependency’s own POM or .module states one, latest.release, LATEST and RELEASE are read as stating no lower bound, answered by the version the rest of the build selects, and 1.+ as the range of versions it prefixes, [1,2).

A dependency on another target in the same project is written by its name: deps = mylib;.

fabr ls @mvn:org.slf4j:slf4j-api lists the versions the repository publishes, newest first.

Property Used for Passed on to whatever depends on the jar
deps Packages the code uses Yes.
provided_deps Packages the code uses that whatever runs it supplies, such as the servlet API a container provides As a requirement the consumer must meet; the jar doesn’t carry them, and an installation of it holds them only where something else requires them.
optional_provided_deps As provided_deps, for packages whatever runs it may leave out Only where the consumer has the package anyway.
compile_deps Packages the code is compiled against that nothing needs when it runs, such as an annotations-only jar No.
compile_api_deps As compile_deps, for packages the jar’s own API names, such as the annotations on its public classes To what compiles against the jar; never installed.
runtime_deps Packages the code needs only when it runs, such as a JDBC driver To what runs the jar; never compiled against.
test_deps Packages only the tests use No.

The sources are compiled against all of them but runtime_deps and test_deps, and the tests against all but runtime_deps. The tests run with all but compile_deps and compile_api_deps on the classpath.

Fabr uses minimal version selection, the algorithm Go modules use. For each package it gathers every requirement in the build, yours and those of your dependencies’ POMs, and selects the highest of the versions they ask for. A plain version in Maven is a minimum (“this or later”), so this is the lowest version that satisfies everything.

Maven: the version declared nearest to your project wins, even when something deeper in the tree requires a later one, which can put a version on the classpath that a library was never built against. Gradle: the highest version requested wins, as in fabr. fabr: the highest version requested wins, and nothing depends on the order the tree was walked in. A range’s upper bound is checked against the version chosen, and a version that breaks one is an error rather than a silent choice.

deps = @mvn:org.apache.commons:commons-text:1.11.0; # also selects commons-lang3 3.13.0,
# the version commons-text's POM names

To use a later version of something a dependency requires, require it yourself: your own requirement is one of the ones the highest is taken from.

fabr deps <target> prints the tree each target resolves to:

greeter
deps
└── org.apache.commons/commons-text:1.11.0
└── org.apache.commons/commons-lang3:3.13.0

There is one version of each package on a classpath. When requirements can’t all be met by one version, because a range’s upper bound excludes the version something else requires, the build fails with Resolution requires multiple versions of the same package: and the two requirements that disagree, each with the path it came from. Choose the version yourself and mark it with ! to make every requirement on the package use it:

deps = @mvn:com.example:widget:2.3.0 @mvn:com.example:util:1.4.2!;

A ! needs an exact version.

Your compile classpath and your runtime classpath are resolved separately, as Gradle resolves its compileClasspath and runtimeClasspath: a jar_package compiles against the first, and its tests and a java_program run with the second, as does the WEB-INF/lib of a war_package. Where a dependency on only one of them asks for a later version of a package, the two classpaths can hold different versions of it, as Gradle’s do unless you turn on its consistent resolution. To use one version on both, pin it in your deps.

What goes on each follows from the scopes involved. Your compile_api_deps and compile_deps are on the compile classpath only, and your runtime_deps on the runtime classpath only; see Which properties take dependencies. A dependency’s own requirements are split the same way: its POM’s runtime scope dependencies are on your runtime classpath only, and where it publishes Gradle Module Metadata, its java-api variant gives your compile classpath and its java-runtime variant your runtime one. fabr deps <target> shows a target’s dependencies under the property each is written in. When you publish a jar, its POM records each property with the matching scope; see The generated POM.

A BOM (bill of materials) is a POM-packaged artifact whose <dependencyManagement> lists versions for a set of packages, such as junit-bom or spring-boot-dependencies. Put the BOM in the same property as the packages it manages, and write those without a version:

jar_package app {
srcs = src/main/java:**/*.java;
deps = @mvn:org.springframework.boot:spring-boot-dependencies:2.7.18
@mvn:org.springframework.boot:spring-boot-starter-web;
test_deps = @mvn:org.junit:junit-bom:5.11.4
@mvn:org.junit.jupiter:junit-jupiter
@mvn:org.junit.platform:junit-platform-launcher;
}

A BOM’s versions are minimums that apply only to packages something else requires: it adds nothing to the build itself. A versionless reference with no BOM to give it a version is an error that says the package is required ... without a version lower bound.

Written with !, a BOM’s versions are the only ones its packages can have, as with Gradle’s enforcedPlatform: each package it manages that something requires is that version, even where something requires a later one. A package also forced with its own ! keeps that version.

deps = @mvn:org.springframework.boot:spring-boot-dependencies:4.0.0!
@mvn:org.springframework:spring-core:7.0.10; # spring-core 7.0.1, the BOM's

A catalog is a named list of dependencies whose versions are chosen once, together, which targets then name without a version. It does for a multi-module project what a parent POM’s <dependencyManagement> or a Gradle version catalog does. A Maven package in a catalog is named by <group>/<artifact>:

catalog @lib {
deps = @mvn:com.google.guava:guava:33.2.1-jre
@mvn:org.apache.commons:commons-text:1.11.0
@mvn:org.apache.commons:commons-lang3:3.14.0
@mvn:org.junit:junit-bom:5.11.4
@mvn:org.junit.jupiter:junit-jupiter
@mvn:org.junit.platform:junit-platform-launcher;
}
jar_package app {
srcs = src/main/java:**/*.java;
deps = @lib:com.google.guava/guava @lib:org.apache.commons/commons-text;
tests = src/test/java:**/*.java;
test_deps = @lib:org.junit.jupiter/junit-jupiter @lib:org.junit.platform/junit-platform-launcher;
}
  • A BOM can be a member, and gives versions to the members written without one, as it does in a target’s deps; see BOMs. The BOM itself needn’t be named by the targets.

  • A catalog’s versions apply to what its members bring in. commons-text 1.11.0 asks for commons-lang3 3.13.0, but the catalog lists 3.14.0, so app gets 3.14.0, though it doesn’t name commons-lang3, as a <dependencyManagement> entry would give it.

  • Every target that uses a member gets the catalog’s versions. A target that asks for another version of one of them directly, such as @lib:org.apache.commons/commons-text beside @mvn:org.apache.commons:commons-lang3:3.14.0 in a catalog that selected 3.13.0, fails with a version conflict:

    Resolution requires multiple versions of the same package:
    org.apache.commons/commons-lang3 is required as '[3.13.0]' and as '3.14.0', and no one version is both
    '[3.13.0]' pinned by '@lib:org.apache.commons/commons-text'
    '3.14.0' required directly

    Change the version in the catalog instead, so that every target agrees.

The rest of what a catalog does, such as fetching a member only when a target uses it, is the same for every kind of package; see Catalogs. A published jar’s POM records each catalog dependency at the version the catalog states for it.

Fabr reads the POM of every package it selects, with its parent POMs, the properties they define (${project.version}, ${slf4j.version}), their <dependencyManagement> and the BOMs they import. Of a POM’s <dependencies>:

Declared in the POM What fabr does with it
compile scope A requirement of the package, on your compile classpath and your runtime classpath.
runtime scope A requirement of the package on your runtime classpath only, as in Maven and Gradle.
provided, test or system scope, or <optional>true</optional> Ignored, as Maven and Gradle ignore them.
A dependency with a <classifier> A requirement of the classified jar.
A test-jar dependency A requirement of the package’s tests jar.
A dependency of another type (zip, pom, …) Ignored.

A dependency’s <exclusions> leave the packages they name, * matching any group or artifact, out of what that dependency brings in, its own and its dependencies’ requirements alike, as in Maven and Gradle: a package is left out only where every path that reaches it excludes it, so one that something else requires is still installed. Exclusions written in a <dependencyManagement> entry apply to a dependency that declares none of its own, as in Maven. <profiles> are not read.

A package’s files are its one artifact, <artifact>-<version>.jar (.war, .ear and so on, from its <packaging>), checked against the .sha1 the repository publishes beside it. A POM-packaged artifact, such as a BOM, has no files unless the repository also publishes a jar for it. Its sources and javadoc jars are fetched only when asked for.

Libraries published by Gradle often publish a .module file beside their POM, and say so in the POM with the comment <!-- do_not_remove: published-with-gradle-metadata -->. Where it does, fabr reads the package from the .module instead of the POM, as Gradle does. The .module describes a version as variants, such as Guava’s jre and android flavours, each with its own dependencies and files. Like Gradle, fabr takes one variant for your compile classpath and one for your runtime classpath:

  • a library variant of the usage the classpath asks for (org.gradle.usage java-api to compile against, java-runtime to run; to compile against a module that publishes no java-api variant, its java-runtime one), on the standard JVM, for a Java release no later than JVM_TARGET’s, the latest such release where there are several; or, for a module that is a platform (a BOM), its platform variant;
  • not a variant providing another capability, such as the module’s test fixtures.

So a library’s compileOnlyApi dependencies (in its java-api variant only, such as JUnit’s apiguardian-api) are on your compile classpath and not installed, and its implementation dependencies (in its java-runtime variant only) are installed and not compiled against.

Of that variant:

In the .module What fabr does with it
dependencies Requirements of the package. A version requires is a floor, a strictly one a range, or an exact version as [1.2]; prefers where nothing else is stated. The versions it rejects are cut out of it: requiring 1.0 and rejecting 1.2 is [1.0,1.2),(1.2,).
dependencyConstraints Versions for the packages they name, where something requires them, as a BOM’s are.
A dependency’s excludes Read as a POM’s <exclusions> are.
A dependency on a platform A requirement of the platform, whose constraints give versions, as a BOM’s do. A library’s dependency on an enforced platform is read the same way: forcing versions is the decision of the project that installs the library, which writes ! on the BOM.
An artifactSelector on a dependency The classified jar or other file it names.
files The package’s files, checked against the SHA-1 the .module states.
available-at A requirement of the module named, which holds the variant: a Kotlin Multiplatform library’s root module answers with its -jvm module.

A package’s files always come from its java-runtime variant. The .module is read for a release no later than JVM_TARGET’s, so changing JVM_TARGET can select a different variant of a library that publishes one per release. Where no variant suits, it is the error … has no java-runtime variant for Java 17 (or java-api), with the variants there are.

Some packages publish further jars beside each version, told apart by a classifier: a native library for each platform (netty-transport-native-epoll-4.1.110.Final-linux-x86_64.jar), a tests jar. A reference names one with the classifier after the artifact:

deps = @mvn:io.netty:netty-transport-native-epoll/linux-x86_64:4.1.110.Final;

The version is chosen for the package, not for the classified jar: every requirement on netty-transport-native-epoll, classified or not, raises the one version, and each classified jar is of that version. So a native library always matches the classes that load it. Requiring …/linux-x86_64:4.1.100.Final beside a library that requires 4.1.110.Final gives netty-transport-native-epoll-4.1.110.Final-linux-x86_64.jar; fabr deps shows it as io.netty/netty-transport-native-epoll/linux-x86_64:4.1.100.Final -> 4.1.110.Final. A classified jar requires its package, and through it what the version’s POM requires: a classified jar has no POM of its own.

This is how Gradle resolves a classifier. Maven chooses a version for each classifier apart, by nearest wins, so a classified jar can end up at a different version from its package unless a BOM gives both, which is why Netty’s BOM lists each classifier.

Where the version chosen publishes no jar with the classifier, it is an error: io.netty:netty-transport-native-epoll:4.1.110.Final publishes no linux-sparc jar. Require a version that does.

Spelling the classifier after the artifact differs from Maven’s and Gradle’s own notation, which put it in a further : part; in fabr what follows the version selects files from the jar, as in any reference. A classified jar’s sources or javadoc are not read with <BUILD_OPERATION=sources> or doc: where a package publishes them, they are a classified jar of their own (/test-sources for a tests jar’s).

A version can also publish files that are not jars, such as a distribution: Tomcat publishes tomcat-9.0.85.zip and tomcat-9.0.85.tar.gz, and Apache Maven apache-maven-3.9.6-bin.zip. A POM doesn’t say which files a version publishes, so a reference names the extension after an @, as Gradle writes it:

@mvn:org.apache.tomcat:tomcat@zip:9.0.85 # tomcat-9.0.85.zip
@mvn:org.apache.maven:apache-maven/bin@tar.gz:3.9.6 # apache-maven-3.9.6-bin.tar.gz

Such a file is a package of its own, as a classified jar is, but goes on no classpath; use it where files are wanted, such as a generate target’s srcs or fabr cp. A version that publishes no such file is the error org.apache.tomcat:tomcat:9.0.85 publishes no rar.

A package’s own artifact needs no extension: it is the one its POM’s <packaging> names, a .war for a war.

Under the doc and sources operations a Maven reference is answered with the package’s published -javadoc.jar or -sources.jar, unpacked:

Terminal window
fabr ls '@mvn:org.slf4j:slf4j-api:1.7.36<BUILD_OPERATION=doc>' # index.html, …
fabr cp '@mvn:org.slf4j:slf4j-api:1.7.36<BUILD_OPERATION=sources>' ./src

Only the package named is fetched, not what it requires. A package that publishes no such jar is the error org.junit:junit-bom:5.11.4 publishes no javadoc jar.

@mvn is Maven Central. To send every @mvn package to a mirror, set MAVEN_REPOSITORY_URL:

MAVEN_REPOSITORY_URL = https://maven.example.com/repository/maven-public;

To use another repository beside Central, declare it with a name of your choosing, and write its packages under that name:

maven_repository @corp {
url = https://maven.corp.example.com/releases;
}
jar_package app {
deps = @corp:com.corp:billing:4.2.0 @mvn:org.slf4j:slf4j-api:1.7.36;
}

Maven and Gradle look for each package in every repository the build lists. Fabr looks for a package, and for everything its POM requires, in one repository only: the one its reference names. So @corp above must hold everything billing requires, as a repository manager that proxies Central (a Nexus or Artifactory group repository) does. Setting MAVEN_REPOSITORY_URL to such a repository is then all a build needs.

A repository that holds only some packages, such as Google’s, is combined with Central in a repository_group, which chooses the repository for each package by its name:

maven_repository @google {
url = https://maven.google.com;
}
repository_group @deps {
androidx.* = @google;
* = @mvn;
}
jar_package app {
srcs = src/main/java:**/*.java;
deps = @deps:androidx.annotation:annotation:1.8.0;
}

androidx.annotation comes from Google’s repository, and the Kotlin standard library it requires from Central. A key is a package name, <group>/<artifact>, or a prefix of one ending in *, or * alone for every other package; the longest matching key decides. A package is looked up only in the repository its name selects, never in another as a fallback, and versions are chosen across all of them together.

Where a repository lacks a package one of its packages requires, the error names the repository: org.jetbrains.kotlin:kotlin-stdlib:1.7.10 is not published at https://maven.google.com (required by androidx.annotation/annotation-jvm@1.8.0 < androidx.annotation/annotation@1.8.0). Combine it with Central in a repository_group, or use a repository manager that proxies both.

A repository that needs credentials gets them from your ~/.m2/settings.xml, as Maven does. A maven_repository uses the <server> its id names, for reading and for publishing. Without an id, it uses the server named exactly as the repository is, @corp:

maven_repository @corp {
url = https://maven.corp.example.com/releases;
id = corp-releases;
}
<settings>
<servers>
<server>
<id>corp-releases</id>
<username>deploy</username>
<password>${env.CORP_MAVEN_PASSWORD}</password>
</server>
</servers>
</settings>

For a mirror of Maven Central that needs credentials, declare @mvn yourself, which replaces the one fabr declares, with the mirror’s URL and server:

maven_repository @mvn {
url = https://maven.corp.example.com/central;
id = corp-mirror;
}

${env.NAME} reads an environment variable, as it does for Maven, which keeps the secret out of the file in CI. A server’s <configuration><httpHeaders> are sent too, for a repository that reads a token from a header, such as GitLab’s Private-Token. A password encrypted with Maven’s master password can’t be read; write it with ${env.NAME} instead.

Fabr reads only the <servers> of settings.xml. Its <mirrors>, <proxies>, <profiles> and <localRepository> are not read:

  • A mirror of Central is MAVEN_REPOSITORY_URL, or a maven_repository @mvn of your own where it needs credentials; see Credentials.
  • A proxy comes from the standard environment variables, https_proxy, http_proxy and no_proxy, in upper or lower case.
  • A project can’t write an exclusion of its own: a package a dependency requires can’t be left out, only overridden with !. Exclusions a dependency’s POM or .module publishes are read.
  • Relocations and POM profiles are not read.
  • Two packages providing one capability, such as Guava and google-collections, are both installed, where Gradle reports a conflict.
  • A module’s test fixtures can be required as its classified jar, @mvn:<group>:<artifact>/test-fixtures:<version>, but the dependencies Gradle publishes for them are not read: write them beside it.
  • A maven_repository URL can’t be a file: URL.