Skip to content

Maven BOM support: import BOMs, and write dependencies without a version - #685

Merged
oyvindberg merged 7 commits into
masterfrom
bom
Sep 4, 2026
Merged

Maven BOM support: import BOMs, and write dependencies without a version#685
oyvindberg merged 7 commits into
masterfrom
bom

Conversation

@oyvindberg

Copy link
Copy Markdown
Owner

Adds first-class Maven BOM (dependencyManagement import) support — the one piece of Maven the docs used to call out as missing.

What you can now do

Declare BOMs on a project and write dependencies without a version; the BOM supplies it:

projects:
  app:
    boms:
      - com.fasterxml.jackson:jackson-bom:2.17.2
    dependencies:
      - com.fasterxml.jackson.core:jackson-databind   # no version — the BOM supplies it
  • boms: applies Maven dependencyManagement-import semantics to every resolution the project participates in: transitives are pinned, sibling BOMs are first-wins, and a version you write yourself still wins over the BOM.
  • Version-less dependenciesorg:name (Java) / org::name (Scala) — round-trip through the model and resolve from the BOM. Dep.parse/repr handle the new form; coursier's addBoms already fills a version-less request.
  • BOMs travel along dependsOn: a platform BOM on a shared library governs every consumer, so one line pins an app's whole dependency universe (how Quarkus/Spring-Boot platforms work).
  • bleep import-maven extracts <dependencyManagement> scope=import entries into boms: (read from the raw pom chain, since effective-pom flattens them away) and emits any dependency the pom wrote version-less as version-less in bleep — so the imported build reads like the Maven original instead of freezing BOM-owned versions.
  • Clear failure: a version-less dependency with no BOM imported fails up front with an actionable message naming the coordinate and the fix, instead of coursier retrying an empty-version fetch. When BOMs are present, coursier is the authority on what they manage (we don't second-guess it and risk false rejections).

Also in this PR

  • Schema: boms: is documented, and the DependencyShort pattern now allows the version-less form (previously a org:name dep would show an IDE validation error).
  • Docs + site: every "no BOM / not yet / inlined at import" claim flipped across the docs and landing pages; docs/concepts/dependencies.mdx gains a proper BOMs and version-less dependencies section including the downstream-dependsOn flow.
  • BomIT (3 tests, against a real published BOM): version-less resolves to the BOM's version, a library's BOM constrains a consumer that declares none, and the no-BOM case fails with the actionable error.

Full build green.

🤖 Generated with Claude Code

oyvindberg and others added 5 commits September 3, 2026 23:27
Completes the BOM feature. A dependency can now be written without a version
(org:name, org::name); the version is supplied by a Maven BOM the project (or a
project it dependsOn) imports. Dep.parse/repr round-trip the version-less form;
coursier's addBoms already fills a version-less request, so resolution needed
nothing. The Maven importer now preserves the author's intent: a dependency
written version-less in the raw pom (BOM-managed) is emitted version-less rather
than frozen at the effective-pom version, so an imported build reads like the
Maven one.

BomIT pins both halves against a real published BOM (jackson-bom): a version-less
dependency resolves to the BOM's version, and that constraint travels along
dependsOn so a library's BOM governs a consumer that declares none.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtKgAYQtNSjkyDWVb4UyTr
Flips every 'no BOM / dependencyManagement not yet / inlined at import'
claim across the docs and site, and gives dependencies.mdx a proper
'BOMs and version-less dependencies' section: the boms: field, writing
dependencies without a version, first-wins/self-declared-wins rules, and
how a BOM travels along dependsOn to govern a whole subtree. The import
demo now says BOMs are extracted to boms: with managed versions stripped;
the Spring Boot tutorial shows the spring-boot-dependencies BOM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtKgAYQtNSjkyDWVb4UyTr
The DependencyShort pattern required a trailing :version, so a version-less
org:name / org::name (now valid, resolved from boms:) failed IDE schema
validation. Make the :version optional and document why.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtKgAYQtNSjkyDWVb4UyTr
…front

A version-less dependency (org:name) resolves only if a BOM supplies its
version. When NO BOM is imported, nothing possibly can, so catch that up front
and fail with an actionable message naming the coordinates and the fix, instead
of letting coursier retry an empty-version fetch and report a cryptic
`...:"" not found`.

Deliberately only the no-BOM case. The authority on what a BOM manages is
coursier's own addBoms, not bleep's BomPins re-derivation (which can miss, e.g.,
a version written through a property it cannot interpolate) — so when BOMs are
present we defer to coursier rather than risk falsely rejecting a valid build.
BomIT covers the no-BOM failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtKgAYQtNSjkyDWVb4UyTr
When collapsing an exploded build back to YAML, a cross-project whose config
is fully inherited from an extended cross-template could be written as an
explicit empty `cross: {jvm211: {}}` entry — noise for a cross id the template
already provides. The boms-support model changes shifted a collapse result
just enough to surface this on the scalameta rewrite snapshot (23 such
entries), which is why it was band-aided with a snapshot regeneration.

Fix it at the source: in BuildRewrite.withProjects, drop an empty cross entry
when an extended template (transitively) already provides that cross id. This
is round-trip safe — the project still cross-builds the id at the template's
config, so no cross id is ever lost — and the rewrite output is now
byte-identical to before the boms field, so the snapshot regeneration is
reverted.

Also add TemplateTest assertions for the template-collapse bug family: a
dependency shared across two projects lands in a common template (not dropped,
not force-inlined) and survives the round-trip; a single-project dependency
stays put; cross-projects sharing config collapse without empty cross entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtKgAYQtNSjkyDWVb4UyTr
CoursierResolver.Params gained a `boms` field with a forProduct5 codec that
made it required on decode. Every resolve cache written before the field — all
the committed snapshot-test caches, and any user's on-disk cache — then failed
to load with "DecodingFailure ... params.boms: Missing required field".

Decode absent-as-empty: an entry written before the field carried no boms,
which is exactly the empty set. The encoder still writes the field, so new
caches round-trip. No cache regeneration needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtKgAYQtNSjkyDWVb4UyTr
@oyvindberg
oyvindberg merged commit 9a5728a into master Sep 4, 2026
10 checks passed
@oyvindberg
oyvindberg deleted the bom branch September 4, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant