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.
Coming from Maven or Gradle
Section titled “Coming from Maven or Gradle”| 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. |
Declaring a dependency
Section titled “Declaring a dependency”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.
Which properties take dependencies
Section titled “Which properties take dependencies”| 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.
How versions are chosen
Section titled “How versions are chosen”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 namesTo 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:
greeterdeps└── org.apache.commons/commons-text:1.11.0 └── org.apache.commons/commons-lang3:3.13.0When versions conflict
Section titled “When versions conflict”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.
Compile and runtime classpaths
Section titled “Compile and runtime classpaths”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'sCatalogs
Section titled “Catalogs”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-text1.11.0 asks forcommons-lang33.13.0, but the catalog lists 3.14.0, soappgets 3.14.0, though it doesn’t namecommons-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-textbeside@mvn:org.apache.commons:commons-lang3:3.14.0in 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 directlyChange 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.
What fabr reads from a POM
Section titled “What fabr reads from a POM”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.
Gradle Module Metadata
Section titled “Gradle Module Metadata”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.usagejava-apito compile against,java-runtimeto run; to compile against a module that publishes nojava-apivariant, itsjava-runtimeone), on the standard JVM, for a Java release no later thanJVM_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.
Classified jars
Section titled “Classified jars”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).
Files other than jars
Section titled “Files other than jars”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.gzSuch 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.
A dependency’s javadoc and sources
Section titled “A dependency’s javadoc and sources”Under the doc and sources operations a Maven reference is answered with the package’s
published -javadoc.jar or -sources.jar, unpacked:
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>' ./srcOnly 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.
Repositories
Section titled “Repositories”@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;}Combining repositories
Section titled “Combining repositories”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.
Credentials
Section titled “Credentials”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.
Mirrors and proxies
Section titled “Mirrors and proxies”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 amaven_repository @mvnof your own where it needs credentials; see Credentials. - A proxy comes from the standard environment variables,
https_proxy,http_proxyandno_proxy, in upper or lower case.
Not supported
Section titled “Not supported”- 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.modulepublishes 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_repositoryURL can’t be afile:URL.