Subsystem · resolver

Resolver & checksums

Curie talks to Maven Central directly — it resolves transitive closures, verifies checksums, and supports mirrors, credentials, and per-artifact repository routing. For declaring dependencies and BOM imports, see Adding dependencies.

Transitive resolution

Curie does a breadth-first walk of every dependency's POM, applying Maven's conflict-resolution rules:

Nearest-wins is silent for minor and patch differences, but a major-version mismatch (a discarded candidate whose major differs from the kept version) fails the build rather than silently shipping a possibly-incompatible JAR. Resolve it by upgrading, excluding the offending transitive dependency, or accepting it with allowVersionConflict = true on the dependency — see Major-version conflicts.

A pom-packaged (aggregator) dependency — one whose POM declares <packaging>pom</packaging>, such as org.codehaus.groovy:groovy-all — has no JAR of its own. Curie expands it for its <dependencies> and contributes no jar for the aggregator itself, whether it appears transitively or is declared directly in Curie.toml.

POMs are fetched serially in phase 1 (small, fast). JARs are then downloaded in parallel in phase 2 — up to eight concurrent workers — and arrive in the local cache in a stable, topologically ordered classpath.

Checksum verification

Every artifact Curie downloads is verified against its .sha256 sidecar from the same repository. The sidecar hash is then persisted next to the artifact in ~/.m2/repository/...artifact.jar.sha256, mirroring Maven Central's published layout. On the next build the cached hash is compared against the cached JAR — zero network calls, zero implicit trust.

A missing sidecar is a hard error. Every well-formed Maven repository publishes one — Maven's deploy plugin generates them, and Nexus / Artifactory generate them server-side on upload. A missing sidecar usually means a misconfigured proxy or a manually-uploaded artifact, and either way Curie refuses to install an unverifiable JAR.

Fallback to SHA-1

If a repository only publishes .sha1 sidecars (older mirrors, some private repos), Curie falls back to that. SHA-1 is cryptographically weak for authentication but still catches accidental corruption and unsophisticated substitution — adequate for the integrity-check use case.

Tamper detection

Edit a JAR in ~/.m2/repository by hand and the next build will fail with a checksum mismatch error citing the artifact and both digests. The local cache is now tamper-evident.

Offline mode

Pass --offline to forbid network calls entirely. Any cache miss is an immediate error. Useful for sealed CI environments, reproducibility checks, and incident-response builds where you want to be sure nothing on disk has changed since the last good build.

$ curie build --offline
Building greeter v0.1.0
  Compile         up to date
  Tests           up to date
  Package         up to date
  Done            target/greeter-0.1.0.jar

Pre-fetching dependencies

curie fetch downloads artifacts into ~/.m2/repository without compiling or testing anything — useful for warming the cache before --offline builds, in CI, or in air-gapped environments.

With no arguments, it downloads everything declared in Curie.toml: [dependencies], [test-dependencies], [annotation-processors], and [test-annotation-processors], with BOM-managed versions resolved.

$ curie fetch
  Dependencies    3 JAR(s)
  Done            3 JAR(s) cached

$ curie build --offline   # succeeds entirely from the local cache

Pass one or more group:artifact:version coordinates to fetch extra artifacts and their full transitive closures — without adding them to Curie.toml:

$ curie fetch org.apache.commons:commons-compress:1.27.1
  Done            4 artifact(s) cached

If a transitive dependency declares a Maven version range (e.g. [2.9.1,2.11)), curie fetch queries maven-metadata.xml, picks the exact version Maven would select (the highest published version satisfying the range), and proposes a ready-to-run command that pins it — a supplied coordinate overrides the matching transitive range:

$ curie fetch ch.epfl.scala:bsp4j:2.1.1
error: non-deterministic version ranges in dependency graph

  com.google.code.gson:gson
    [2.9.1,2.11)  →  2.10.1

Re-run with an explicit version for each ranged artifact:
  curie fetch ch.epfl.scala:bsp4j:2.1.1 com.google.code.gson:gson:2.10.1

(curie build, by contrast, always requires you to pin the range explicitly in Curie.toml — see Version range in dependency POM.)

Add --no-transitive to download just that one artifact's POM and JAR, skipping its dependencies:

$ curie fetch org.apache.commons:commons-compress:1.27.1 --no-transitive
  Done            ~/.m2/repository/org/apache/commons/commons-compress/1.27.1/commons-compress-1.27.1.jar

Bulk pre-fetch from a file

Pass --file <path> to read a list of coordinates from a file — one group:artifact:version per line. Blank lines and lines starting with # are ignored. The listed artifacts are resolved with parallel JAR downloads, making this much faster than fetching them one by one. If a transitive range is encountered, curie fetch names the coordinate line to add to the file (with the exact version computed from maven-metadata.xml); adding it pins the range so the file ends up listing the concrete versions you intend to use.

offline-deps.txt
# JUnit
org.junit.platform:junit-platform-console-standalone:6.1.0
org.junit.jupiter:junit-jupiter:6.0.3

# Jackson
com.fasterxml.jackson.core:jackson-databind:2.17.2

# Plugins
org.apache.maven.plugins:maven-surefire-plugin:3.5.6
$ curie fetch --file offline-deps.txt
  Done            47 artifact(s) cached

$ mvn clean verify   # now works without internet access

This is the recommended way to warm the Maven local cache for air-gapped builds. The project's [[repositories]] entries are automatically included when the command is run inside a project directory, so artifacts from custom repositories resolve correctly too.

Per-dependency repository selection

By default every artifact is fetched from Maven Central. To pull a specific artifact from a different repository, declare the repository with a unique id and reference it from the dependency:

Curie.toml
[[repositories]]
id  = "shibboleth"
url = "https://build.shibboleth.net/nexus/content/repositories/releases/"

[dependencies]
# fetched from shibboleth only; its transitive deps try Central + shibboleth
"net.shibboleth.oidc:oidc-common-crypto-api" = { version = "3.3.0", repository = "shibboleth" }
# fetched from Maven Central only (no repository field)
"com.fasterxml.jackson.core:jackson-databind" = "2.17.2"

The routing rules are precise: the explicitly-declared artifact is fetched from the named repository only; its transitive deps are tried in both Central and the named repository. Artifacts with no repository field never hit extra repositories.

Repository mirrors

Redirect artifact downloads through a corporate proxy or local cache without changing project config. Mirrors are per-machine and live in ~/.curie/config.toml:

~/.curie/config.toml
# Route Maven Central through an internal Nexus proxy.
[[mirrors]]
id        = "internal-nexus"
mirror_of = "central"
url       = "https://nexus.internal/repository/maven-central/"

# Use "*" to mirror every configured repository.
[[mirrors]]
id        = "catch-all"
mirror_of = "*"
url       = "https://nexus.internal/repository/all/"

Each repo gets its own independent mirror substitution. First matching mirror wins. Mirrors are transparent to the project — Curie.toml stays unchanged.

Repository credentials

Private repositories that require authentication are configured in ~/.curie/config.toml with indirection so secrets stay out of files:

~/.curie/config.toml
[[credentials]]
repo_id  = "internal-nexus"
username = ""
password = ""

The repo_id matches the id field in [[repositories]]. is expanded at credential-use time — the string is read from the environment variable, not stored as a literal. Literal values are accepted too (for local-only configs where the credential is not sensitive).

Dependency tree and conflict traces

After a successful build, inspect the resolved closure with curie deps:

$ curie deps
Dependencies for my-app v0.1.0
└─ com.fasterxml.jackson.core:jackson-databind:2.17.2
   ├─ com.fasterxml.jackson.core:jackson-annotations:2.17.2
   └─ com.fasterxml.jackson.core:jackson-core:2.17.2

Use --why group:artifact to explain why a specific artifact is at its chosen version — especially useful when nearest-wins resolved a conflict:

$ curie deps --why com.fasterxml.jackson.core:jackson-core
com.fasterxml.jackson.core:jackson-core:2.17.2  (depth 1)

  Introduced by:
    [declared] → jackson-databind:2.17.2 → jackson-core:2.17.2  (chosen — depth 1)

  No version conflicts.

Add --tests to inspect the test-dependency closure instead. See Dependency tree for the full reference.