Resolver Ideas

This post is just “tinkering starter”, and will get updated as I progress with PoC, or discuss these ideas with others. I just wanted to share the ideas I had, and maybe get some feedback on them, or even better, get some help or hints where these features would be needed or would be useful.

Some time I was tinkering about some new features for Resolver, and I was thinking about how to implement them. So, let me lay down the ideas I had.

All these ideas are envisioned as SPI extension points, and by default (or without SPI implementations) would not alter anything of Resolver behavior. Moreover, in this post I am intentionally talking about Resolver only, and did not draw any parallel with Maven, as again, details would be implemented in specific SPI implementation.

Moreover, some of these features may not be feasible for Maven use cases at all, but as I said, this is about Resolver features only, and Maven is not the only Resolver integration out there, so users of library like MIMA can still benefit from these extra features, if they want to.

Note: these are just ideas and are locally implemented as “proof of concept” (PoC). Also, in many examples will use Maveniverse Toolbox, but only as tool, as same output would be produced by maven-dependency-plugin:tree as well.

Conditional dependencies

Condition dependencies are plain optional dependencies, that have activation condition associated, and based on condition evaluation, may be activated or not.

Example: consider following example (very same as in Quarkus guide): We have example artifact conditional-a, and it declares optional dependency on conditional-b example artifact.

In this case, the proof of concept (PoC) SPI is looking into POM properties, and conditional-b declares it: <activationCondition>artifactPresent(org.slf4j:slf4j-api)</activationCondition>. The PoC implements the rule artifactPresent(GAV) as “if artifact with given GAV is present in the resolved graph, then activate this dependency”.

Finally, we have an example project conditional that declares dependency on conditional-a. It also contains a profile satisfy, that when enabled, will add dependency on org.slf4j:slf4j-api.

Example run without profile:

$ mvn -f examples/conditional/ toolbox:tree
...
[INFO] --- toolbox:0.15.14:tree (default-cli) @ conditional ---
[INFO] org.cstamas.resolver.examples:conditional:jar:1.0.0-SNAPSHOT (origin: central)
[INFO] ╰─org.cstamas.resolver.artifacts:conditional-a:jar:1.0.0-SNAPSHOT [compile] (origin: central)

We see “as expected” tree. The optional dependency conditional-b is not present. Now, if we run with profile:

$ mvn -f examples/conditional/ -Psatisfy toolbox:tree
...
[INFO] --- toolbox:0.15.14:tree (default-cli) @ conditional
INFO] org.cstamas.resolver.examples:conditional:jar:1.0.0-SNAPSHOT (origin: central)
[INFO] ├─org.cstamas.resolver.artifacts:conditional-a:jar:1.0.0-SNAPSHOT [compile] (origin: central)
[INFO] │ ╰─org.cstamas.resolver.artifacts:conditional-b:jar:1.0.0-SNAPSHOT [compile, optional] (origin: central)
[INFO] │   ╰─org.slf4j:slf4j-simple:jar:2.0.18 [compile, optional] (origin: central)
[INFO] ╰─org.slf4j:slf4j-api:jar:2.0.18 [compile] (origin: central)

The presence of org.slf4j:slf4j-api activated the conditional dependency conditional-b, and it was present in the tree, along with its own dependencies.

Capabilities

Capabilities goal is to allow fulfillment checks on collected graph and provide early warning or errors, or even some “intervention” from SPI, like dynamic dependency injection.

The meaning of capabilities are left to SPI implementations (ideas: OSGi metadata, JPMS module-info module-name vs requires, or uses vs provides, ultimately it really depends on the domain SPI targets). The basic idea is that capabilities can, based on given domain, provide warnings, errors or even automatic dependency injection.

Example: consider following example: We have example artifact consumer-a and provider-a.

In this case, the PoC SPI is looking into POM properties, and consumer-a declares capability as <capabilityConsumer>slf4j-backend</capabilityConsumer> while provider-a declares <capabilityProvider>slf4j-backend</capabilityProvider>, and capabilities are simple string labels. Checking is simple label matching on consumer and provider side.

Finally, we have an example project capability that declares dependency on consumer-a. For example’s sake, we can invoke this project in three ways:

$ mvn -f examples/capability/ package
...
BUILD FAILURE

$ mvn -f examples/capability/ -Psatisfy package
...
BUILD SUCCESS

$ mvn -f examples/capability/ -Pdiscover package
...
BUILD SUCCESS

In first case, Resolver while collecting the graph will fail (“unsatisfied capability: no provider for capability slf4j-backend”) as there is no provider for declared consumer capability.

In second case, build succeeds, as producer and consumer are “matched”. Note: PoC performs only matching, but does not care about arity, but a SPI could enforce that capabilities match as 1:1 and fail in case of 1:N (multiple providers for same capability are present), but again, this is domain specific.

In third case, profile provides a “hint” for PoC SPI how to “discover” a provider for a capability, and possibly inject it as dependency, when required.

Platform matching

Platform matching would be a way for resolver to “align” certain artifact versions against given, known or discovered “platform”.

TBD.

Artifact mapping

Artifact mapping would be a way to map artifacts to alternative artifacts, for example using some lookup mechanism. It could happen within same repository (like relocation), or across repositories (a RemoteRepository that would be “redirector”).

TBD.

Example use case for mapping within same repository: Define a groupId (assume non-existing on Central) and a mapping for it. For example, define G as github.sormuras.modules and use provided mapping file.

Then users of SPI could declare dependencies as:

<dependency>
  <groupId>github.sormuras.modules</groupId>
  <artifactId>codes.rafael.asmjdkbridge</artifactId>
  <version>1.0</version>
</dependency>

and it would make resolver go for artifact

<dependency>
  <groupId>codes.rafael.asmjdkbridge</groupId>
  <artifactId>asm-jdk-bridge</artifactId>
  <version>1.0</version>
</dependency>

Whether artifact identity changes (to mapped) or remains virtual may be matter of configuration.

Implementation details

All these features are envisioned as SPI extension points, and by default (or without SPI implementations) would not alter anything of Resolver behavior. Still, given all these feature are able to dynamically alter the graph, the backing idea is shown by this pseudocode (showing what new code is wrapping existing graph = collect() call:

  boolean success = false
  while(!succes) { // + attempt limit
    if (resolverExtrasSPI != null) {
      resolverExtrasSPI.prepare(resolutionContext)
    }
    graph = collect()
    if (resolverExtrasSPI != null) {
      success = resolverExtrasSPI.check(graph, resolutionContext) << can return true, or ask for repeated collection (w/ modified context) or throw error
    } else {
      success = true
    }
  }

But, the thing is, and is not visible from pseudo-code, is that collector retains all the hot caches, so basically subsequent collection is not “full collection”, but only “re-collection” of modified graph. But, all steps like collection and subsequent conflict resolution (both performed by collect()) is a must, as for example dependency injection could change the winner(s) in the graph. Moreover, conditional dependency could trigger a provider injection, and so on.

Last modified June 3, 2026: Resolver Extras (add note) (bbf00ac)