ProjectBuildStage
Stage 1 of the LST classpath-resolution pipeline: extract the project's compile classpath by invoking its own build tool (Maven or Gradle).
Why Stage 1? The most accurate way to obtain the exact JAR set the project actually compiles against is to ask the build tool itself. This accounts for BOM imports, version catalogs, dependency management, and plugin-contributed dependencies that are difficult to reproduce by static parsing alone.
Build tool detection: The presence of pom.xml signals a Maven project; any of build.gradle, build.gradle.kts, or settings.gradle(.kts) signals a Gradle project. If neither is found, extractClasspath returns null immediately.
Maven: Runs mvnw dependency:build-classpath (using the Maven wrapper if present, otherwise mvn). The classpath is written to a temp file via -Dmdep.outputFile and then parsed. The test scope is included (-DincludeScope=test) so test-only dependencies are available when recipes analyse test sources.
Gradle: Injects a temporary Gradle init script that registers a printClasspathForOpenRewrite task. The task resolves the project's testRuntimeClasspath (falling back through runtimeClasspath, testCompileClasspath, compileClasspath, and default in that order) and prints each JAR path to stdout. The Gradle wrapper (gradlew) is used when present; otherwise the system gradle command is used.
Failure behaviour: Any failure — non-zero exit code, process timeout, missing build tool, or an unexpected exception — is logged as a warning and causes extractClasspath to return null. The pipeline then falls through to DependencyResolutionStage (Stage 2).
Compilation: After a successful Stage 1, tryCompile is called if the project has no pre-compiled class directories. Compiled .class files are then appended to the classpath so that intra-project type references (e.g. wildcard imports across packages within the same project) resolve correctly instead of appearing as JavaType.Unknown during recipe execution.
Extensibility: The class is open with open methods so tests can subclass it to inject a fake classpath without spawning real processes.