Skip to content

Add incremental build context API and implementation - #12576

Draft
gnodet wants to merge 12 commits into
masterfrom
move-the-incremental-build-context-api-in-pr-1118
Draft

Add incremental build context API and implementation#12576
gnodet wants to merge 12 commits into
masterfrom
move-the-incremental-build-context-api-in-pr-1118

Conversation

@gnodet

@gnodet gnodet commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Port and modernize the incremental build context from PR #1118 for Maven 4.1.0. This gives mojo authors a first-class API for incremental builds — tracking which input files changed, managing input→output associations, skipping unnecessary work, and cleaning up stale outputs automatically.

Closes #1118.

What's in this PR

Public API (maven-api-core) — 17 types in 2 packages

org.apache.maven.api.build.context — mojo-facing API:

Type Role
BuildContext Main entry point — register inputs/outputs, check status, associate files
Input / Output Tracked input and output resources with status and metadata
InputSet Aggregation support (many inputs → one output, e.g., JAR, index)
Metadata<R> Lazy status inspection before committing to process
Resource Base for Input/Output — path, status, diagnostic messages
Status Change status enum: NEW, MODIFIED, UNMODIFIED, REMOVED
Severity Diagnostic severity: ERROR, WARNING, INFO
@Incremental Annotation for mojo fields — marks config parameters for change detection
BuildContextException Runtime exception (extends MavenException)

org.apache.maven.api.build.context.spi — integration/IDE SPI:

Type Role
Workspace Filesystem abstraction with 4 modes (NORMAL, DELTA, ESCALATED, SUPPRESSED)
CommittableBuildContext Extended BuildContext with commit(Sink) for state persistence
BuildContextEnvironment Construction parameters (state file, workspace, config, finalizer)
BuildContextFinalizer Batch-commits all contexts after mojo success
Sink Receives diagnostic messages during commit (bridges to IDE markers / Maven log)
FileState / Message Immutable value objects for file metadata and diagnostics

Implementation (maven-impl) — 12 classes + 10 test classes

Core incremental build engine in org.apache.maven.internal.build.context.impl:

  • DefaultBuildContext — full lifecycle: register inputs → detect changes → manage outputs → persist state
  • Change detection via timestamp/size comparison with configurable workspace modes
  • FileMatcher — Ant-style glob patterns with trie-based subdirectory optimization for targeted workspace walks
  • State serialization/deserialization with cross-build state persistence
  • 41 tests covering: status transitions across build cycles, input-output association cleanup, state serialization (including corrupt/incompatible files), include/exclude patterns, cross-platform paths (via Jimfs), configuration change escalation, delta workspace mode, diagnostic message persistence, aggregation lifecycle

Maven integration (maven-core) — 5 classes

Maven-specific wiring in org.apache.maven.internal.build.context.impl.maven:

  • MavenBuildContext@MojoExecutionScoped build context with Supplier-based delegation
  • MavenBuildContextFinalizerWeakMojoExecutionListener that batch-commits after mojo success
  • MavenBuildContextConfiguration — resolves state file location from MojoExecution
  • ProjectWorkspace — dispatches in-project paths to IDE workspace, out-of-project to filesystem
  • Digest utilities (ClasspathDigester, MojoConfigurationDigester, Digesters) — detect plugin classpath and mojo configuration changes

Key design decisions

  1. API in o.a.m.api.build.context (not o.a.m.api.build) — avoids package collision with the build report API in PR Build Report Foundation — structured report, console modes, warning control #12572

  2. Workspace abstraction — the same mojo code works in CLI Maven (NORMAL mode with full filesystem scan), IDEs (DELTA mode with file-watcher), recovery scenarios (ESCALATED), and read-only analysis (SUPPRESSED)

  3. Two-phase registrationregisterInputs() returns Metadata<Input> for status inspection; process() commits to work. registerAndProcessInputs() provides a one-step convenience

  4. Maven 4 DI — uses org.apache.maven.api.di.Inject/Named/Typed/MojoExecutionScoped and java.util.function.Supplier instead of javax.inject.* / org.eclipse.sisu.*

  5. No external dependencies — replaced plexus-utils MatchPatterns with regex-based antPatternToRegex(), replaced CachingOutputStream with BufferedOutputStream(Files.newOutputStream())

Maven 4 conventions followed

  • @Experimental on all API types
  • @Nonnull / @Nullable on all parameters and return types
  • @Immutable on value objects (FileState, Message)
  • @Consumer / @Provider on SPI interfaces
  • @since 4.1.0 on all public types
  • BuildContextException extends MavenException
  • Boolean accessor: isFailOnError() (not getFailOnError())
  • Package-private test classes and methods (no public)

Known limitations (documented in Javadoc)

  • No cross-module/reactor-level incremental coordination (build context is per-mojo-execution)
  • @Incremental annotation lives in build.context rather than plugin.annotations
  • Severity enum is separate from BuilderProblem.Severity (different semantics — build context messages persist across builds and support INFO level)

Test plan

  • 41 build context tests passing in maven-impl
  • mvn verify passes for maven-impl and maven-core (checkstyle, RAT, all tests)
  • CI build passes

🤖 Generated with Claude Code

Port the incremental build context from PR #1118 to Maven 4 conventions:
- API in org.apache.maven.api.build.context (avoids collision with build report API)
- Core impl in maven-impl using Maven 4 DI (org.apache.maven.api.di)
- Maven integration (MavenBuildContext, ProjectWorkspace, digest) stays in maven-core
- Replace plexus-utils dependencies (MatchPatterns, CachingOutputStream) with pure Java
- Add Maven 4 annotations (@experimental, @nonnull, @nullable, @SInCE 4.0.0)
- All 41 tests passing in maven-impl

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet
gnodet force-pushed the move-the-incremental-build-context-api-in-pr-1118 branch from e5a76c1 to 1e02718 Compare July 28, 2026 21:53
gnodet and others added 4 commits July 29, 2026 00:06
- Add package-info.java for both context and context.spi packages with
  architecture overview, use case examples, and SPI integration guide
- Enhance BuildContext Javadoc with three usage patterns: one-to-one
  transformation, two-pass status inspection, and aggregated builds
- Add code examples to Input, Output, InputSet, Metadata, Resource
- Add @see cross-references between all related types
- Enrich Incremental annotation with configuration parameter example
- Document SPI lifecycle: environment, workspace modes, finalizer,
  sink message flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…leMatcher package-private

- Convert all javax.inject.* and org.eclipse.sisu.* annotations to
  Maven 4 DI equivalents (org.apache.maven.api.di.*) in maven-core
  integration files
- Replace javax.inject.Provider with java.util.function.Supplier
- Remove unnecessary public modifiers from all test classes and methods
- Make FileMatcher package-private, add TODO noting overlap with
  PathSelector (createMatchers trie has no PathSelector equivalent)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix ClasspathDigester.digestZipOrFile() always re-throwing after
  non-ZIP fallback, and reset MessageDigest after partial update
- Fix ClasspathDigester.digestDirectory() passing subdirectories to
  digestFile() by filtering to regular files only
- Fix DefaultResource.equals() casting to wrong type (DefaultMetadata
  instead of DefaultResource)
- Rename CommitableBuildContext → CommittableBuildContext (correct
  English spelling for public API type)
- Fix commit() @nonnull@nullable on Sink parameter to match
  implementation behavior
- Rename getFailOnError() → isFailOnError() for boolean convention
- Fix FileMatcher.getCanonicalPath() NPE when path has no parent
- Fix readCollection() returning null for empty collections (NPE risk
  in invertMultimap)
- Replace bare NullPointerException with Objects.requireNonNull
- Clarify registerAndProcessInputs() Javadoc: returns all inputs, not
  just processed ones

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet gnodet mentioned this pull request Jul 28, 2026
@gnodet gnodet changed the title Move build context API to o.a.m.api.build.context Add incremental build context API and implementation (supersedes #1118) Jul 28, 2026
gnodet and others added 5 commits July 29, 2026 02:45
…V4 mojos

- Add MavenITgh12576IncrementalBuildContextTest with a real V4 plugin
  (IncrementalCopyMojo) that uses @Inject BuildContext for incremental
  file copying across 3 build scenarios: initial, no-change, modified
- Wire @MojoExecutionScoped scope, BuildContext impl bindings, and
  explicit BuildContext→MojoExecutionScopedBuildContext supplier in V4
  mojo injector (configureV4MojoInjector)
- Wrap V4 mojos to call MavenBuildContextFinalizer after execution since
  WeakMojoExecutionListener lifecycle doesn't apply to V4 mojos
- Fix ClasspathDigester to skip artifacts with unresolved paths instead
  of throwing NoSuchElementException

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace ConcurrentHashMap with HashMap in V4 @MojoExecutionScoped
  scope cache: ConcurrentHashMap.computeIfAbsent throws "Recursive
  update" when scoped beans depend on each other and hash to the same
  bucket; V4 mojo execution is single-threaded so HashMap is safe
- Add early return in MavenBuildContextFinalizer.afterMojoExecutionSuccess
  when no contexts are registered: prevents "Contexts FailOnError
  property have different values" error for V4 mojos that don't inject
  BuildContext (empty context list + null Sink triggered failBuild with
  an empty failOnError set)
- Fix IT to use per-build log files (setLogFileName) and clean on first
  build to avoid stale incremental state from prior test runs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the standalone FileMatcher class (373 lines) with the existing
PathSelector infrastructure, exposed through the PathMatcherFactory API.

- Add createSubdirectoryMatchers() to PathMatcherFactory API for targeted
  directory walks using trie-based pattern decomposition
- Port trie logic (IncludesTrie, subdirectory decomposition) into
  PathSelector.ofSubdirectories()
- Update DefaultBuildContext to use PathMatcherFactory instead of FileMatcher
- Move getCanonicalPath() from FileMatcher to DefaultBuildContext
- Delete FileMatcher.java entirely

This avoids duplicating Ant-to-glob pattern matching logic and centralizes
all path matching in PathSelector, the single implementation behind the
PathMatcherFactory service.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of hardcoding `new DefaultPathMatcherFactory()`, accept
PathMatcherFactory as a constructor parameter so it flows through
Maven's DI container. MojoExecutionScopedBuildContext now receives
the factory via @Inject alongside BuildContextEnvironment.

The 4-arg test constructor retains a null-safe fallback to
DefaultPathMatcherFactory for backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change the protected multi-arg constructors to public so that
downstream projects (maven-filtering, maven-resources-plugin) can
create test instances with a FilesystemWorkspace and no state file
to exercise incremental build context behavior in unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gnodet and others added 2 commits July 29, 2026 16:51
…tern

Expand the BuildContext API Javadoc to cover capabilities that were
implemented but not documented:

- Configuration change detection: explain that Maven automatically
  digests all mojo parameters and the plugin classpath, and that any
  change triggers escalation to a full build. Mojos do not need custom
  options-change detection.

- Escalation semantics: document when and why the build context treats
  all inputs as modified (missing state file, config change, missing
  outputs, workspace escalation).

- Two-pass processing pattern: add Use Case 5 showing how compilers and
  similar tools can register inputs, run the tool, then associate
  unpredictable outputs (inner classes) after processing.

- registerInput(Path): clarify its use for individual dependency JARs
  and other non-directory-tree inputs.

- BuildContextEnvironment.getParameters(): expand from a one-liner to
  a full explanation of the automatic mojo parameter digestion and
  plugin classpath hashing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ation

Add comprehensive Javadoc to the implementation classes that were
undocumented despite being key to the BuildContext's capabilities:

- DefaultBuildContext: document the class, the main constructor's
  configuration comparison and escalation logic, getConfigurationChanged()
  key-by-key comparison, and the three-pass finalizeContext() stale
  output cleanup algorithm.

- MojoConfigurationDigester: document the two-category digest (plugin
  classpath + mojo parameters), how each @parameter field is reflected
  and evaluated, and how unsupported types are reported as errors.

- ClasspathDigester: expand the one-liner to explain SHA-1 content
  hashing (not timestamps), sorted ZIP entry ordering, session-level
  caching, and immunity to SNAPSHOT timestamp changes.

- MavenBuildContextConfiguration: document how it wires together the
  digester, workspace, finalizer, and state file location
  (target/incremental/<groupId>_<artifactId>_<goal>_<executionId>).

- MavenBuildContext + MojoExecutionScopedBuildContext: document the
  scoping pattern and per-execution isolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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