This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new f0b5291f9d2 SOLR-17473: Introduce build tools formatting (#2739)
f0b5291f9d2 is described below

commit f0b5291f9d228833dcf52b6f8b145e2709f982b6
Author: Christos Malliaridis <[email protected]>
AuthorDate: Sat Oct 5 05:17:38 2024 +0200

    SOLR-17473: Introduce build tools formatting (#2739)
    
    * Migrate buildSrc to a composite included build
    Migrate buildSrc into a composite included build (build-infra).
    Expose a plugin with buildinfra extension.
    * Configure tidy to run on infra classes
    * Run tidy
    * Add missing annotations
---
 {buildSrc => build-tools/build-infra}/build.gradle |  37 ++-
 .../build-infra/settings.gradle                    |  16 +-
 .../java/org/apache/lucene/gradle/Checksum.java    |  13 +-
 .../lucene/gradle/ErrorReportingTestListener.java  | 288 ++++++++++++++++++
 .../lucene/gradle/GradlePropertiesGenerator.java   |   2 +-
 .../org/apache/lucene/gradle/PrefixedWriter.java   |  11 +-
 .../org/apache/lucene/gradle/ProfileResults.java   |  71 +++--
 .../java/org/apache/lucene/gradle/SpillWriter.java |   7 +-
 .../org/apache/lucene/gradle/StdOutTeeWriter.java  |  94 ++++++
 .../apache/lucene/gradle/WrapperDownloader.java    |  56 ++--
 .../lucene/gradle/buildinfra/BuildInfraPlugin.java |  60 ++++
 .../missing-doclet}/build.gradle                   |  24 +-
 .../missing-doclet}/settings.gradle                |   0
 .../apache/lucene/missingdoclet/MissingDoclet.java | 321 +++++++++++----------
 {buildSrc => build-tools}/scriptDepVersions.gradle |   1 +
 build.gradle                                       |  18 +-
 .../lucene/gradle/ErrorReportingTestListener.java  | 275 ------------------
 .../org/apache/lucene/gradle/StdOutTeeWriter.java  |  93 ------
 dev-tools/README.txt                               |   1 -
 buildSrc/build.gradle => gradle/conventions.gradle |  31 +-
 gradle/testing/defaults-tests.gradle               |   3 +-
 gradle/testing/failed-tests-at-end.gradle          |   4 +-
 gradle/testing/profiling.gradle                    |   4 +-
 gradle/validation/check-environment.gradle         |   1 +
 gradle/validation/jar-checks.gradle                |   4 +-
 gradle/validation/owasp-dependency-check.gradle    |   2 +-
 gradle/validation/rat-sources.gradle               |   6 +-
 gradle/validation/spotless.gradle                  |  23 +-
 gradlew                                            |   4 +-
 gradlew.bat                                        |   4 +-
 settings.gradle                                    |   4 +-
 solr/distribution/build.gradle                     |   5 +-
 32 files changed, 804 insertions(+), 679 deletions(-)

diff --git a/buildSrc/build.gradle b/build-tools/build-infra/build.gradle
similarity index 57%
copy from buildSrc/build.gradle
copy to build-tools/build-infra/build.gradle
index 91f88741322..0d78cce49bf 100644
--- a/buildSrc/build.gradle
+++ b/build-tools/build-infra/build.gradle
@@ -15,20 +15,43 @@
  * limitations under the License.
  */
 
+plugins {
+  id "java-gradle-plugin"
+  id 'com.diffplug.spotless' version '6.5.2' apply false
+}
+
 repositories {
   mavenCentral()
 }
 
-ext {
-  // Minimum Java version required to compile buildSrc.
-  minJavaVersion = JavaVersion.VERSION_11
-}
+group = "org.apache"
 
 // Make sure the build environment is consistent.
-apply from: file('../gradle/validation/check-environment.gradle')
+apply from: file('../../gradle/conventions.gradle')
+apply from: file('../../gradle/validation/check-environment.gradle')
+
+// Add spotless/ tidy.
+tasks.register("checkJdkInternalsExportedToGradle") {}
+apply from: file('../../gradle/validation/spotless.gradle')
+
+// Load common script dependencies.
+apply from: file("../scriptDepVersions.gradle")
 
-// Load common buildSrc and script deps.
-apply from: file("scriptDepVersions.gradle")
+java {
+  sourceCompatibility = scriptDepVersions['min-java-version']
+  targetCompatibility = scriptDepVersions['min-java-version']
+}
+
+gradlePlugin {
+  automatedPublishing = false
+
+  plugins {
+    buildInfra {
+      id = 'solr.build-infra'
+      implementationClass = 
'org.apache.lucene.gradle.buildinfra.BuildInfraPlugin'
+    }
+  }
+}
 
 dependencies {
   implementation gradleApi()
diff --git a/dev-tools/solr-missing-doclet/build.gradle 
b/build-tools/build-infra/settings.gradle
similarity index 68%
copy from dev-tools/solr-missing-doclet/build.gradle
copy to build-tools/build-infra/settings.gradle
index e85f0a037e4..350b781b7f4 100644
--- a/dev-tools/solr-missing-doclet/build.gradle
+++ b/build-tools/build-infra/settings.gradle
@@ -15,18 +15,4 @@
  * limitations under the License.
  */
 
-plugins {
-  id 'java-library'
-}
-
-version = "1.0.0-SNAPSHOT"
-group = "org.apache.solr.tools"
-description = 'Doclet-based javadoc validation'
-
-sourceCompatibility = JavaVersion.VERSION_11
-targetCompatibility = JavaVersion.VERSION_11
-
-tasks.withType(JavaCompile) {
-  options.compilerArgs += ["--release", targetCompatibility.toString()]
-  options.encoding = "UTF-8"
-}
+rootProject.name = 'build-infra'
diff --git a/buildSrc/src/main/java/org/apache/lucene/gradle/Checksum.java 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/Checksum.java
similarity index 99%
rename from buildSrc/src/main/java/org/apache/lucene/gradle/Checksum.java
rename to 
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/Checksum.java
index 0dab9dc7f05..a1d5c09586f 100644
--- a/buildSrc/src/main/java/org/apache/lucene/gradle/Checksum.java
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/Checksum.java
@@ -27,6 +27,11 @@
 
 package org.apache.lucene.gradle;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Locale;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.gradle.api.DefaultTask;
 import org.gradle.api.GradleException;
@@ -39,16 +44,10 @@ import org.gradle.api.tasks.TaskAction;
 import org.gradle.work.Incremental;
 import org.gradle.work.InputChanges;
 
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.util.Locale;
-
 public class Checksum extends DefaultTask {
   private FileCollection files;
   private File outputDir;
-  private Algorithm algorithm;
+  private Algorithm algorithm = Checksum.Algorithm.SHA512;
 
   public enum Algorithm {
     MD5(new DigestUtils(DigestUtils.getMd5Digest())),
diff --git 
a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/ErrorReportingTestListener.java
 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/ErrorReportingTestListener.java
new file mode 100644
index 00000000000..c1fb7b83983
--- /dev/null
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/ErrorReportingTestListener.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.gradle;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+import org.gradle.api.internal.tasks.testing.logging.FullExceptionFormatter;
+import org.gradle.api.internal.tasks.testing.logging.TestExceptionFormatter;
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+import org.gradle.api.tasks.testing.TestDescriptor;
+import org.gradle.api.tasks.testing.TestListener;
+import org.gradle.api.tasks.testing.TestOutputEvent;
+import org.gradle.api.tasks.testing.TestOutputListener;
+import org.gradle.api.tasks.testing.TestResult;
+import org.gradle.api.tasks.testing.logging.TestLogging;
+
+/**
+ * An error reporting listener that queues test output streams and displays 
them on failure.
+ *
+ * <p>Heavily inspired by Elasticsearch's ErrorReportingTestListener (ASL 2.0 
licensed).
+ */
+public class ErrorReportingTestListener implements TestOutputListener, 
TestListener {
+  private static final Logger LOGGER = 
Logging.getLogger(ErrorReportingTestListener.class);
+
+  private final TestExceptionFormatter formatter;
+  private final Map<TestKey, OutputHandler> outputHandlers = new 
ConcurrentHashMap<>();
+  private final Path spillDir;
+  private final Path outputsDir;
+  private final boolean verboseMode;
+
+  public ErrorReportingTestListener(
+      TestLogging testLogging, Path spillDir, Path outputsDir, boolean 
verboseMode) {
+    this.formatter = new FullExceptionFormatter(testLogging);
+    this.spillDir = spillDir;
+    this.outputsDir = outputsDir;
+    this.verboseMode = verboseMode;
+  }
+
+  @Override
+  public void onOutput(TestDescriptor testDescriptor, TestOutputEvent 
outputEvent) {
+    handlerFor(testDescriptor).write(outputEvent);
+  }
+
+  @Override
+  public void beforeSuite(TestDescriptor suite) {
+    // noop.
+  }
+
+  @Override
+  public void beforeTest(TestDescriptor testDescriptor) {
+    // Noop.
+  }
+
+  @Override
+  public void afterSuite(final TestDescriptor suite, TestResult result) {
+    if (suite.getParent() == null || suite.getName().startsWith("Gradle")) {
+      return;
+    }
+
+    TestKey key = TestKey.of(suite);
+    try {
+      OutputHandler outputHandler = outputHandlers.get(key);
+      if (outputHandler != null) {
+        long length = outputHandler.length();
+        if (length > 1024 * 1024 * 10) {
+          LOGGER.warn(
+              String.format(
+                  Locale.ROOT,
+                  "WARNING: Test %s wrote %,d bytes of output.",
+                  suite.getName(),
+                  length));
+        }
+      }
+
+      boolean echoOutput = Objects.equals(result.getResultType(), 
TestResult.ResultType.FAILURE);
+      boolean dumpOutput = echoOutput;
+
+      // If the test suite failed, report output.
+      if (dumpOutput || echoOutput) {
+        Files.createDirectories(outputsDir);
+        Path outputLog = outputsDir.resolve(getOutputLogName(suite));
+
+        // Save the output of a failing test to disk.
+        try (Writer w = Files.newBufferedWriter(outputLog, 
StandardCharsets.UTF_8)) {
+          if (outputHandler != null) {
+            outputHandler.copyTo(w);
+          }
+        }
+
+        if (echoOutput && !verboseMode) {
+          synchronized (this) {
+            System.out.println("");
+            System.out.println(
+                suite.getClassName()
+                    + " > test suite's output saved to "
+                    + outputLog
+                    + ", copied below:");
+            try (BufferedReader reader =
+                Files.newBufferedReader(outputLog, StandardCharsets.UTF_8)) {
+              char[] buf = new char[1024];
+              int len;
+              while ((len = reader.read(buf)) >= 0) {
+                System.out.print(new String(buf, 0, len));
+              }
+              System.out.println();
+            }
+          }
+        }
+      }
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    } finally {
+      OutputHandler handler = outputHandlers.remove(key);
+      if (handler != null) {
+        try {
+          handler.close();
+        } catch (IOException e) {
+          LOGGER.error("Failed to close output handler for: " + key, e);
+        }
+      }
+    }
+  }
+
+  private static Pattern SANITIZE = Pattern.compile("[^a-zA-Z .\\-_0-9]+");
+
+  public static String getOutputLogName(TestDescriptor suite) {
+    return SANITIZE.matcher("OUTPUT-" + suite.getName() + 
".txt").replaceAll("_");
+  }
+
+  @Override
+  public void afterTest(TestDescriptor testDescriptor, TestResult result) {
+    // Include test failure exception stacktrace(s) in test output log.
+    if (result.getResultType() == TestResult.ResultType.FAILURE) {
+      if (result.getExceptions().size() > 0) {
+        String message = formatter.format(testDescriptor, 
result.getExceptions());
+        handlerFor(testDescriptor).write(message);
+      }
+    }
+  }
+
+  private OutputHandler handlerFor(TestDescriptor descriptor) {
+    // Attach output of leaves (individual tests) to their parent.
+    if (!descriptor.isComposite()) {
+      descriptor = descriptor.getParent();
+    }
+    return outputHandlers.computeIfAbsent(TestKey.of(descriptor), (key) -> new 
OutputHandler());
+  }
+
+  public static class TestKey {
+    private final String key;
+
+    private TestKey(String key) {
+      this.key = key;
+    }
+
+    public static TestKey of(TestDescriptor d) {
+      StringBuilder key = new StringBuilder();
+      key.append(d.getClassName());
+      key.append("::");
+      key.append(d.getName());
+      key.append("::");
+      key.append(d.getParent() == null ? "-" : d.getParent().toString());
+      return new TestKey(key.toString());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o != null && o.getClass() == this.getClass() && 
Objects.equals(((TestKey) o).key, key);
+    }
+
+    @Override
+    public int hashCode() {
+      return key.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return key;
+    }
+  }
+
+  private class OutputHandler implements Closeable {
+    // Max single-line buffer before automatic wrap occurs.
+    private static final int MAX_LINE_WIDTH = 1024 * 4;
+
+    private final SpillWriter buffer;
+
+    // internal stream.
+    private final PrefixedWriter sint;
+    // stdout
+    private final PrefixedWriter sout;
+    // stderr
+    private final PrefixedWriter serr;
+
+    // last used stream (so that we can flush it properly and prefixes are not 
screwed up).
+    private PrefixedWriter last;
+
+    public OutputHandler() {
+      buffer =
+          new SpillWriter(
+              () -> {
+                try {
+                  return Files.createTempFile(spillDir, "spill-", ".tmp");
+                } catch (IOException e) {
+                  throw new UncheckedIOException(e);
+                }
+              });
+
+      Writer sink = buffer;
+      if (verboseMode) {
+        sink = new StdOutTeeWriter(buffer);
+      }
+
+      sint = new PrefixedWriter("   > ", sink, MAX_LINE_WIDTH);
+      sout = new PrefixedWriter("  1> ", sink, MAX_LINE_WIDTH);
+      serr = new PrefixedWriter("  2> ", sink, MAX_LINE_WIDTH);
+      last = sint;
+    }
+
+    public void write(TestOutputEvent event) {
+      write(
+          (event.getDestination() == TestOutputEvent.Destination.StdOut ? sout 
: serr),
+          event.getMessage());
+    }
+
+    public void write(String message) {
+      write(sint, message);
+    }
+
+    public long length() throws IOException {
+      return buffer.length();
+    }
+
+    private void write(PrefixedWriter out, String message) {
+      try {
+        if (out != last) {
+          last.completeLine();
+          last = out;
+        }
+        out.write(message);
+      } catch (IOException e) {
+        throw new UncheckedIOException("Unable to write to test output.", e);
+      }
+    }
+
+    public void copyTo(Writer out) throws IOException {
+      flush();
+      buffer.copyTo(out);
+    }
+
+    public void flush() throws IOException {
+      sout.completeLine();
+      serr.completeLine();
+      buffer.flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+      buffer.close();
+    }
+  }
+}
diff --git 
a/buildSrc/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java
 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java
similarity index 96%
rename from 
buildSrc/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java
rename to 
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java
index db4f804f12e..5436afe70f8 100644
--- 
a/buildSrc/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java
@@ -67,6 +67,6 @@ public class GradlePropertiesGenerator {
       fileContent = fileContent.replace(entry.getKey(), 
String.valueOf(entry.getValue()));
     }
     Files.writeString(
-            destination, fileContent, StandardCharsets.UTF_8, 
StandardOpenOption.CREATE_NEW);
+        destination, fileContent, StandardCharsets.UTF_8, 
StandardOpenOption.CREATE_NEW);
   }
 }
diff --git 
a/buildSrc/src/main/java/org/apache/lucene/gradle/PrefixedWriter.java 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/PrefixedWriter.java
similarity index 91%
rename from buildSrc/src/main/java/org/apache/lucene/gradle/PrefixedWriter.java
rename to 
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/PrefixedWriter.java
index 7281d496001..3dc663e8332 100644
--- a/buildSrc/src/main/java/org/apache/lucene/gradle/PrefixedWriter.java
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/PrefixedWriter.java
@@ -20,12 +20,13 @@ import java.io.IOException;
 import java.io.Writer;
 
 /**
- * Prefixes every new line with a given string, synchronizing multiple streams 
to emit consistent lines.
+ * Prefixes every new line with a given string, synchronizing multiple streams 
to emit consistent
+ * lines.
  */
 public class PrefixedWriter extends Writer {
   Writer sink;
 
-  private final static char LF = '\n';
+  private static final char LF = '\n';
   private final String prefix;
   private final StringBuilder lineBuffer = new StringBuilder();
   private final int maxLineLength;
@@ -45,7 +46,7 @@ public class PrefixedWriter extends Writer {
       sink.write(LF);
 
       lineBuffer.setLength(0);
-      if (c != LF) { 
+      if (c != LF) {
         lineBuffer.append((char) c);
       }
     } else {
@@ -70,9 +71,7 @@ public class PrefixedWriter extends Writer {
     throw new UnsupportedOperationException();
   }
 
-  /**
-   * Complete the current line (emit LF if not at the start of the line 
already).
-   */
+  /** Complete the current line (emit LF if not at the start of the line 
already). */
   public void completeLine() throws IOException {
     if (lineBuffer.length() > 0) {
       write(LF);
diff --git 
a/buildSrc/src/main/java/org/apache/lucene/gradle/ProfileResults.java 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/ProfileResults.java
similarity index 79%
rename from buildSrc/src/main/java/org/apache/lucene/gradle/ProfileResults.java
rename to 
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/ProfileResults.java
index 60def1a89d1..15e0f11c56e 100644
--- a/buildSrc/src/main/java/org/apache/lucene/gradle/ProfileResults.java
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/ProfileResults.java
@@ -20,13 +20,12 @@ package org.apache.lucene.gradle;
 import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.AbstractMap.SimpleEntry;
-import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-
 import jdk.jfr.consumer.RecordedClass;
 import jdk.jfr.consumer.RecordedEvent;
 import jdk.jfr.consumer.RecordedFrame;
@@ -36,15 +35,12 @@ import jdk.jfr.consumer.RecordedThread;
 import jdk.jfr.consumer.RecordingFile;
 
 /**
- * Processes an array of recording files (from tests), and prints a simple 
histogram.
- * Inspired by the JFR example code.
- * Whole stacks are deduplicated (with the default stacksize being 1): you can 
drill deeper
- * by adjusting the parameters.
+ * Processes an array of recording files (from tests), and prints a simple 
histogram. Inspired by
+ * the JFR example code. Whole stacks are deduplicated (with the default 
stacksize being 1): you can
+ * drill deeper by adjusting the parameters.
  */
 public class ProfileResults {
-  /**
-   * Formats a frame to a formatted line. This is deduplicated on!
-   */
+  /** Formats a frame to a formatted line. This is deduplicated on! */
   static String frameToString(RecordedFrame frame, boolean lineNumbers) {
     StringBuilder builder = new StringBuilder();
     RecordedMethod method = frame.getMethod();
@@ -84,29 +80,32 @@ public class ProfileResults {
 
   /**
    * Driver method, for testing standalone.
+   *
    * <pre>
    * java -Dtests.profile.count=5 
buildSrc/src/main/java/org/apache/lucene/gradle/ProfileResults.java \
    *   ./lucene/core/build/tmp/tests-cwd/somefile.jfr ...
    * </pre>
    */
   public static void main(String[] args) throws IOException {
-    printReport(Arrays.asList(args),
-                System.getProperty(MODE_KEY, MODE_DEFAULT),
-                Integer.parseInt(System.getProperty(STACKSIZE_KEY, 
STACKSIZE_DEFAULT)),
-                Integer.parseInt(System.getProperty(COUNT_KEY, COUNT_DEFAULT)),
-                Boolean.parseBoolean(System.getProperty(LINENUMBERS_KEY, 
LINENUMBERS_DEFAULT)));
+    printReport(
+        Arrays.asList(args),
+        System.getProperty(MODE_KEY, MODE_DEFAULT),
+        Integer.parseInt(System.getProperty(STACKSIZE_KEY, STACKSIZE_DEFAULT)),
+        Integer.parseInt(System.getProperty(COUNT_KEY, COUNT_DEFAULT)),
+        Boolean.parseBoolean(System.getProperty(LINENUMBERS_KEY, 
LINENUMBERS_DEFAULT)));
   }
 
   /** true if we care about this event */
   static boolean isInteresting(String mode, RecordedEvent event) {
     String name = event.getEventType().getName();
-    switch(mode) {
+    switch (mode) {
       case "cpu":
-        return (name.equals("jdk.ExecutionSample") || 
name.equals("jdk.NativeMethodSample")) &&
-            !isGradlePollThread(event.getThread("sampledThread"));
+        return (name.equals("jdk.ExecutionSample") || 
name.equals("jdk.NativeMethodSample"))
+            && !isGradlePollThread(event.getThread("sampledThread"));
       case "heap":
-        return (name.equals("jdk.ObjectAllocationInNewTLAB") || 
name.equals("jdk.ObjectAllocationOutsideTLAB")) &&
-            !isGradlePollThread(event.getThread("eventThread"));
+        return (name.equals("jdk.ObjectAllocationInNewTLAB")
+                || name.equals("jdk.ObjectAllocationOutsideTLAB"))
+            && !isGradlePollThread(event.getThread("eventThread"));
       default:
         throw new UnsupportedOperationException(event.toString());
     }
@@ -119,7 +118,7 @@ public class ProfileResults {
 
   /** value we accumulate for this event */
   static long getValue(RecordedEvent event) {
-    switch(event.getEventType().getName()) {
+    switch (event.getEventType().getName()) {
       case "jdk.ObjectAllocationInNewTLAB":
         return event.getLong("tlabSize");
       case "jdk.ObjectAllocationOutsideTLAB":
@@ -144,15 +143,17 @@ public class ProfileResults {
 
   /** fixed width used for printing the different columns */
   private static final int COLUMN_SIZE = 14;
+
   private static final String COLUMN_PAD = "%-" + COLUMN_SIZE + "s";
+
   private static String pad(String input) {
     return String.format(Locale.ROOT, COLUMN_PAD, input);
   }
 
-  /**
-   * Process all the JFR files passed in args and print a merged summary.
-   */
-  public static void printReport(List<String> files, String mode, int 
stacksize, int count, boolean lineNumbers) throws IOException {
+  /** Process all the JFR files passed in args and print a merged summary. */
+  public static void printReport(
+      List<String> files, String mode, int stacksize, int count, boolean 
lineNumbers)
+      throws IOException {
     if (!"cpu".equals(mode) && !"heap".equals(mode)) {
       throw new IllegalArgumentException("tests.profile.mode must be one of 
(cpu,heap)");
     }
@@ -178,14 +179,13 @@ public class ProfileResults {
             StringBuilder stack = new StringBuilder();
             for (int i = 0; i < Math.min(stacksize, trace.getFrames().size()); 
i++) {
               if (stack.length() > 0) {
-                stack.append("\n")
-                     .append(framePadding)
-                     .append("  at ");
+                stack.append("\n").append(framePadding).append("  at ");
               }
               stack.append(frameToString(trace.getFrames().get(i), 
lineNumbers));
             }
             String line = stack.toString();
-            SimpleEntry<String,Long> entry = histogram.computeIfAbsent(line, u 
-> new SimpleEntry<>(line, 0L));
+            SimpleEntry<String, Long> entry =
+                histogram.computeIfAbsent(line, u -> new SimpleEntry<>(line, 
0L));
             long value = getValue(event);
             entry.setValue(entry.getValue() + value);
             totalEvents++;
@@ -195,12 +195,20 @@ public class ProfileResults {
       }
     }
     // print summary from histogram
-    System.out.printf(Locale.ROOT, "PROFILE SUMMARY from %d events (total: 
%s)\n", totalEvents, formatValue(sumValues));
+    System.out.printf(
+        Locale.ROOT,
+        "PROFILE SUMMARY from %d events (total: %s)\n",
+        totalEvents,
+        formatValue(sumValues));
     System.out.printf(Locale.ROOT, "  tests.profile.mode=%s\n", mode);
     System.out.printf(Locale.ROOT, "  tests.profile.count=%d\n", count);
     System.out.printf(Locale.ROOT, "  tests.profile.stacksize=%d\n", 
stacksize);
     System.out.printf(Locale.ROOT, "  tests.profile.linenumbers=%b\n", 
lineNumbers);
-    System.out.printf(Locale.ROOT, "%s%sSTACK\n", pad("PERCENT"), 
pad(mode.toUpperCase(Locale.ROOT) + " SAMPLES"));
+    System.out.printf(
+        Locale.ROOT,
+        "%s%sSTACK\n",
+        pad("PERCENT"),
+        pad(mode.toUpperCase(Locale.ROOT) + " SAMPLES"));
     List<SimpleEntry<String, Long>> entries = new 
ArrayList<>(histogram.values());
     entries.sort((u, v) -> v.getValue().compareTo(u.getValue()));
     int seen = 0;
@@ -209,7 +217,8 @@ public class ProfileResults {
         break;
       }
       String percent = String.format("%2.2f%%", 100 * (c.getValue() / (float) 
sumValues));
-      System.out.printf(Locale.ROOT, "%s%s%s\n", pad(percent), 
pad(formatValue(c.getValue())), c.getKey());
+      System.out.printf(
+          Locale.ROOT, "%s%s%s\n", pad(percent), 
pad(formatValue(c.getValue())), c.getKey());
     }
   }
 }
diff --git a/buildSrc/src/main/java/org/apache/lucene/gradle/SpillWriter.java 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/SpillWriter.java
similarity index 92%
rename from buildSrc/src/main/java/org/apache/lucene/gradle/SpillWriter.java
rename to 
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/SpillWriter.java
index f89977c2503..e9783d070b5 100644
--- a/buildSrc/src/main/java/org/apache/lucene/gradle/SpillWriter.java
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/SpillWriter.java
@@ -24,9 +24,10 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.function.Supplier;
+import org.jetbrains.annotations.NotNull;
 
 public class SpillWriter extends Writer {
-  private final static int MAX_BUFFERED = 2 * 1024;
+  private static final int MAX_BUFFERED = 2 * 1024;
   private final StringWriter buffer = new StringWriter(MAX_BUFFERED);
 
   private final Supplier<Path> spillPathSupplier;
@@ -38,7 +39,7 @@ public class SpillWriter extends Writer {
   }
 
   @Override
-  public void write(char[] cbuf, int off, int len) throws IOException {
+  public void write(char @NotNull [] cbuf, int off, int len) throws 
IOException {
     getSink(len).write(cbuf, off, len);
   }
 
@@ -58,7 +59,7 @@ public class SpillWriter extends Writer {
   }
 
   @Override
-  public void write(String str, int off, int len) throws IOException {
+  public void write(@NotNull String str, int off, int len) throws IOException {
     getSink(len).write(str, off, len);
   }
 
diff --git 
a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/StdOutTeeWriter.java
 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/StdOutTeeWriter.java
new file mode 100644
index 00000000000..71901259ea2
--- /dev/null
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/StdOutTeeWriter.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.gradle;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.Writer;
+import org.jetbrains.annotations.NotNull;
+
+class StdOutTeeWriter extends Writer {
+  private final Writer delegate;
+  private final PrintStream out = System.out;
+
+  public StdOutTeeWriter(Writer delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void write(int c) throws IOException {
+    delegate.write(c);
+    out.write(c);
+  }
+
+  @Override
+  public void write(char @NotNull [] cbuf) throws IOException {
+    delegate.write(cbuf);
+    out.print(cbuf);
+  }
+
+  @Override
+  public void write(@NotNull String str) throws IOException {
+    delegate.write(str);
+    out.print(str);
+  }
+
+  @Override
+  public void write(@NotNull String str, int off, int len) throws IOException {
+    delegate.write(str, off, len);
+    out.append(str, off, len);
+  }
+
+  @Override
+  public Writer append(CharSequence csq) throws IOException {
+    delegate.append(csq);
+    out.append(csq);
+    return this;
+  }
+
+  @Override
+  public Writer append(CharSequence csq, int start, int end) throws 
IOException {
+    delegate.append(csq, start, end);
+    out.append(csq, start, end);
+    return this;
+  }
+
+  @Override
+  public Writer append(char c) throws IOException {
+    delegate.append(c);
+    out.append(c);
+    return this;
+  }
+
+  @Override
+  public void write(char @NotNull [] cbuf, int off, int len) throws 
IOException {
+    delegate.write(cbuf, off, len);
+    out.print(new String(cbuf, off, len));
+  }
+
+  @Override
+  public void flush() throws IOException {
+    delegate.flush();
+    out.flush();
+  }
+
+  @Override
+  public void close() throws IOException {
+    delegate.close();
+    // Don't close the actual output.
+  }
+}
diff --git 
a/buildSrc/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java
similarity index 78%
rename from 
buildSrc/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java
rename to 
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java
index e6930af7c74..f7e07eb164a 100644
--- a/buildSrc/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java
@@ -16,32 +16,26 @@
  */
 package org.apache.lucene.gradle;
 
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
-import java.net.URLConnection;
-import java.nio.channels.Channels;
-import java.nio.channels.FileChannel;
-import java.nio.channels.ReadableByteChannel;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import java.util.EnumSet;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 
-import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
-import static java.nio.file.StandardOpenOption.APPEND;
-
 /**
  * Standalone class that can be used to download a gradle-wrapper.jar
- * <p>
- * Has no dependencies outside of standard java libraries
+ *
+ * <p>Has no dependencies outside of standard java libraries
  */
 public class WrapperDownloader {
   public static void main(String[] args) {
@@ -62,18 +56,21 @@ public class WrapperDownloader {
   public static void checkVersion() {
     int major = Runtime.getRuntime().version().feature();
     if (major < 11 || major > 21) {
-      throw new IllegalStateException("java version must be between 11 and 21, 
your version: " + major);
+      throw new IllegalStateException(
+          "java version must be between 11 and 21, your version: " + major);
     }
   }
 
   public void run(Path destination) throws IOException, 
NoSuchAlgorithmException {
-    Path checksumPath = 
destination.resolveSibling(destination.getFileName().toString() + ".sha256");
+    Path checksumPath =
+        destination.resolveSibling(destination.getFileName().toString() + 
".sha256");
     if (!Files.exists(checksumPath)) {
       throw new IOException("Checksum file not found: " + checksumPath);
     }
     String expectedChecksum = Files.readString(checksumPath, 
StandardCharsets.UTF_8).trim();
 
-    Path versionPath = 
destination.resolveSibling(destination.getFileName().toString() + ".version");
+    Path versionPath =
+        destination.resolveSibling(destination.getFileName().toString() + 
".version");
     if (!Files.exists(versionPath)) {
       throw new IOException("Wrapper version file not found: " + versionPath);
     }
@@ -92,7 +89,11 @@ public class WrapperDownloader {
       }
     }
 
-    URL url = new URL("https://raw.githubusercontent.com/gradle/gradle/v"; + 
wrapperVersion + "/gradle/wrapper/gradle-wrapper.jar");
+    URL url =
+        new URL(
+            "https://raw.githubusercontent.com/gradle/gradle/v";
+                + wrapperVersion
+                + "/gradle/wrapper/gradle-wrapper.jar");
     System.err.println("Downloading gradle-wrapper.jar from " + url);
 
     // Zero-copy save the jar to a temp file
@@ -108,7 +109,8 @@ public class WrapperDownloader {
         } catch (IOException e) {
           if (retries-- > 0) {
             // Retry after a short delay
-            System.err.println("Error connecting to server: " + e + ", will 
retry in " + retryDelay + " seconds.");
+            System.err.println(
+                "Error connecting to server: " + e + ", will retry in " + 
retryDelay + " seconds.");
             Thread.sleep(TimeUnit.SECONDS.toMillis(retryDelay));
             continue;
           }
@@ -120,7 +122,12 @@ public class WrapperDownloader {
           case HttpURLConnection.HTTP_BAD_GATEWAY:
             if (retries-- > 0) {
               // Retry after a short delay.
-              System.err.println("Server returned HTTP " + 
connection.getResponseCode() + ", will retry in " + retryDelay + " seconds.");
+              System.err.println(
+                  "Server returned HTTP "
+                      + connection.getResponseCode()
+                      + ", will retry in "
+                      + retryDelay
+                      + " seconds.");
               Thread.sleep(TimeUnit.SECONDS.toMillis(retryDelay));
               continue;
             }
@@ -131,13 +138,15 @@ public class WrapperDownloader {
       }
 
       try (InputStream is = connection.getInputStream();
-           OutputStream out = Files.newOutputStream(temp)){
+          OutputStream out = Files.newOutputStream(temp)) {
         is.transferTo(out);
       }
 
       String checksum = checksum(digest, temp);
       if (!checksum.equalsIgnoreCase(expectedChecksum)) {
-        throw new IOException(String.format(Locale.ROOT,
+        throw new IOException(
+            String.format(
+                Locale.ROOT,
                 "Checksum mismatch on downloaded gradle-wrapper.jar (was: %s, 
expected: %s).",
                 checksum,
                 expectedChecksum));
@@ -146,8 +155,12 @@ public class WrapperDownloader {
       Files.move(temp, destination, REPLACE_EXISTING);
       temp = null;
     } catch (IOException | InterruptedException e) {
-      throw new IOException("Could not download gradle-wrapper.jar (" +
-          e.getClass().getSimpleName() + ": " + e.getMessage() + ").");
+      throw new IOException(
+          "Could not download gradle-wrapper.jar ("
+              + e.getClass().getSimpleName()
+              + ": "
+              + e.getMessage()
+              + ").");
     } finally {
       if (temp != null) {
         Files.deleteIfExists(temp);
@@ -165,7 +178,8 @@ public class WrapperDownloader {
       }
       return sb.toString();
     } catch (IOException e) {
-      throw new IOException("Could not compute digest of file: " + path + " (" 
+ e.getMessage() + ")");
+      throw new IOException(
+          "Could not compute digest of file: " + path + " (" + e.getMessage() 
+ ")");
     }
   }
 }
diff --git 
a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/buildinfra/BuildInfraPlugin.java
 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/buildinfra/BuildInfraPlugin.java
new file mode 100644
index 00000000000..415922a1916
--- /dev/null
+++ 
b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/buildinfra/BuildInfraPlugin.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.gradle.buildinfra;
+
+import java.nio.file.Path;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.lucene.gradle.Checksum;
+import org.apache.lucene.gradle.ErrorReportingTestListener;
+import org.apache.lucene.gradle.ProfileResults;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.tasks.testing.TestDescriptor;
+import org.gradle.api.tasks.testing.logging.TestLogging;
+
+public class BuildInfraPlugin implements Plugin<Project> {
+  @Override
+  public void apply(Project project) {
+    project.getExtensions().create(BuildInfraExtension.NAME, 
BuildInfraExtension.class);
+  }
+
+  public static class BuildInfraExtension {
+    public static final String NAME = "buildinfra";
+
+    public ErrorReportingTestListener newErrorReportingTestListener(
+        TestLogging testLogging, Path spillDir, Path outputsDir, boolean 
verboseMode) {
+      return new ErrorReportingTestListener(testLogging, spillDir, outputsDir, 
verboseMode);
+    }
+
+    public DigestUtils sha1Digest() {
+      return new DigestUtils(DigestUtils.getSha1Digest());
+    }
+
+    public String getOutputLogName(TestDescriptor suite) {
+      return ErrorReportingTestListener.getOutputLogName(suite);
+    }
+
+    public Class<?> checksumClass() {
+      return Checksum.class;
+    }
+
+    public Class<?> profileResultsClass() {
+      return ProfileResults.class;
+    }
+  }
+}
diff --git a/dev-tools/solr-missing-doclet/build.gradle 
b/build-tools/missing-doclet/build.gradle
similarity index 61%
rename from dev-tools/solr-missing-doclet/build.gradle
rename to build-tools/missing-doclet/build.gradle
index e85f0a037e4..2525f50d79b 100644
--- a/dev-tools/solr-missing-doclet/build.gradle
+++ b/build-tools/missing-doclet/build.gradle
@@ -17,16 +17,34 @@
 
 plugins {
   id 'java-library'
+  id 'com.diffplug.spotless' version '6.5.2' apply false
+}
+
+repositories {
+  mavenCentral()
 }
 
 version = "1.0.0-SNAPSHOT"
 group = "org.apache.solr.tools"
 description = 'Doclet-based javadoc validation'
 
-sourceCompatibility = JavaVersion.VERSION_11
-targetCompatibility = JavaVersion.VERSION_11
+// Make sure the build environment is consistent.
+apply from: file('../../gradle/conventions.gradle')
+apply from: file('../../gradle/validation/check-environment.gradle')
+
+// Add spotless/ tidy.
+tasks.register("checkJdkInternalsExportedToGradle") {}
+apply from: file('../../gradle/validation/spotless.gradle')
+
+// Load common script dependencies.
+apply from: file("../scriptDepVersions.gradle")
+
+java {
+  sourceCompatibility = scriptDepVersions['min-java-version']
+  targetCompatibility = scriptDepVersions['min-java-version']
+}
 
-tasks.withType(JavaCompile) {
+tasks.withType(JavaCompile).configureEach {
   options.compilerArgs += ["--release", targetCompatibility.toString()]
   options.encoding = "UTF-8"
 }
diff --git a/dev-tools/solr-missing-doclet/settings.gradle 
b/build-tools/missing-doclet/settings.gradle
similarity index 100%
rename from dev-tools/solr-missing-doclet/settings.gradle
rename to build-tools/missing-doclet/settings.gradle
diff --git 
a/dev-tools/solr-missing-doclet/src/main/java/org/apache/lucene/missingdoclet/MissingDoclet.java
 
b/build-tools/missing-doclet/src/main/java/org/apache/lucene/missingdoclet/MissingDoclet.java
similarity index 61%
rename from 
dev-tools/solr-missing-doclet/src/main/java/org/apache/lucene/missingdoclet/MissingDoclet.java
rename to 
build-tools/missing-doclet/src/main/java/org/apache/lucene/missingdoclet/MissingDoclet.java
index 36c0e4fe4c8..44c9c411a9e 100644
--- 
a/dev-tools/solr-missing-doclet/src/main/java/org/apache/lucene/missingdoclet/MissingDoclet.java
+++ 
b/build-tools/missing-doclet/src/main/java/org/apache/lucene/missingdoclet/MissingDoclet.java
@@ -16,6 +16,9 @@
  */
 package org.apache.lucene.missingdoclet;
 
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.ParamTree;
+import com.sun.source.util.DocTrees;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -24,7 +27,6 @@ import java.util.Locale;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.ExecutableElement;
@@ -35,24 +37,19 @@ import javax.lang.model.type.TypeKind;
 import javax.lang.model.util.ElementFilter;
 import javax.lang.model.util.Elements;
 import javax.tools.Diagnostic;
-
-import com.sun.source.doctree.DocCommentTree;
-import com.sun.source.doctree.ParamTree;
-import com.sun.source.util.DocTrees;
-
 import jdk.javadoc.doclet.Doclet;
 import jdk.javadoc.doclet.DocletEnvironment;
 import jdk.javadoc.doclet.Reporter;
 import jdk.javadoc.doclet.StandardDoclet;
 
 /**
- * Checks for missing javadocs, where missing also means "only whitespace" or 
"license header".
- * Has option --missing-level (package, class, method, parameter) so that we 
can improve over time.
- * Has option --missing-ignore to ignore individual elements (such as split 
packages). 
- *   It isn't recursive, just ignores exactly the elements you tell it.
- *   This should be removed when packaging is fixed to no longer be split 
across JARs.
- * Has option --missing-method to apply "method" level to selected packages 
(fix one at a time).
- *   Matches package names exactly: so you'll need to list subpackages 
separately.
+ * Checks for missing javadocs, where missing also means "only whitespace" or 
"license header". Has
+ * option --missing-level (package, class, method, parameter) so that we can 
improve over time. Has
+ * option --missing-ignore to ignore individual elements (such as split 
packages). It isn't
+ * recursive, just ignores exactly the elements you tell it. This should be 
removed when packaging
+ * is fixed to no longer be split across JARs. Has option --missing-method to 
apply "method" level
+ * to selected packages (fix one at a time). Matches package names exactly: so 
you'll need to list
+ * subpackages separately.
  */
 public class MissingDoclet extends StandardDoclet {
   // checks that modules and packages have documentation
@@ -70,121 +67,124 @@ public class MissingDoclet extends StandardDoclet {
   Elements elementUtils;
   Set<String> ignored = Collections.emptySet();
   Set<String> methodPackages = Collections.emptySet();
-  
+
   @Override
   public Set<Doclet.Option> getSupportedOptions() {
     Set<Doclet.Option> options = new HashSet<>();
     options.addAll(super.getSupportedOptions());
-    options.add(new Doclet.Option() {
-      @Override
-      public int getArgumentCount() {
-        return 1;
-      }
+    options.add(
+        new Doclet.Option() {
+          @Override
+          public int getArgumentCount() {
+            return 1;
+          }
 
-      @Override
-      public String getDescription() {
-        return "level to enforce for missing javadocs: [package, class, 
method, parameter]";
-      }
+          @Override
+          public String getDescription() {
+            return "level to enforce for missing javadocs: [package, class, 
method, parameter]";
+          }
 
-      @Override
-      public Kind getKind() {
-        return Option.Kind.STANDARD;
-      }
+          @Override
+          public Kind getKind() {
+            return Option.Kind.STANDARD;
+          }
 
-      @Override
-      public List<String> getNames() {
-        return Collections.singletonList("--missing-level");
-      }
+          @Override
+          public List<String> getNames() {
+            return Collections.singletonList("--missing-level");
+          }
 
-      @Override
-      public String getParameters() {
-        return "level";
-      }
+          @Override
+          public String getParameters() {
+            return "level";
+          }
 
-      @Override
-      public boolean process(String option, List<String> arguments) {
-        switch (arguments.get(0)) {
-          case "package":
-            level = PACKAGE;
-            return true;
-          case "class":
-            level = CLASS;
-            return true;
-          case "method":
-            level = METHOD;
-            return true;
-          case "parameter":
-            level = PARAMETER;
-            return true;
-          default:
-            return false;
-        }
-      }
-    });
-    options.add(new Doclet.Option() {
-      @Override
-      public int getArgumentCount() {
-        return 1;
-      }
+          @Override
+          public boolean process(String option, List<String> arguments) {
+            switch (arguments.get(0)) {
+              case "package":
+                level = PACKAGE;
+                return true;
+              case "class":
+                level = CLASS;
+                return true;
+              case "method":
+                level = METHOD;
+                return true;
+              case "parameter":
+                level = PARAMETER;
+                return true;
+              default:
+                return false;
+            }
+          }
+        });
+    options.add(
+        new Doclet.Option() {
+          @Override
+          public int getArgumentCount() {
+            return 1;
+          }
 
-      @Override
-      public String getDescription() {
-        return "comma separated list of element names to ignore (e.g. as a 
workaround for split packages)";
-      }
+          @Override
+          public String getDescription() {
+            return "comma separated list of element names to ignore (e.g. as a 
workaround for split packages)";
+          }
 
-      @Override
-      public Kind getKind() {
-        return Option.Kind.STANDARD;
-      }
+          @Override
+          public Kind getKind() {
+            return Option.Kind.STANDARD;
+          }
 
-      @Override
-      public List<String> getNames() {
-        return Collections.singletonList("--missing-ignore");
-      }
+          @Override
+          public List<String> getNames() {
+            return Collections.singletonList("--missing-ignore");
+          }
 
-      @Override
-      public String getParameters() {
-        return "ignoredNames";
-      }
+          @Override
+          public String getParameters() {
+            return "ignoredNames";
+          }
 
-      @Override
-      public boolean process(String option, List<String> arguments) {
-        ignored = new HashSet<>(Arrays.asList(arguments.get(0).split(",")));
-        return true;
-      }
-    });
-    options.add(new Doclet.Option() {
-      @Override
-      public int getArgumentCount() {
-        return 1;
-      }
+          @Override
+          public boolean process(String option, List<String> arguments) {
+            ignored = new 
HashSet<>(Arrays.asList(arguments.get(0).split(",")));
+            return true;
+          }
+        });
+    options.add(
+        new Doclet.Option() {
+          @Override
+          public int getArgumentCount() {
+            return 1;
+          }
 
-      @Override
-      public String getDescription() {
-        return "comma separated list of packages to check at 'method' level";
-      }
+          @Override
+          public String getDescription() {
+            return "comma separated list of packages to check at 'method' 
level";
+          }
 
-      @Override
-      public Kind getKind() {
-        return Option.Kind.STANDARD;
-      }
+          @Override
+          public Kind getKind() {
+            return Option.Kind.STANDARD;
+          }
 
-      @Override
-      public List<String> getNames() {
-        return Collections.singletonList("--missing-method");
-      }
+          @Override
+          public List<String> getNames() {
+            return Collections.singletonList("--missing-method");
+          }
 
-      @Override
-      public String getParameters() {
-        return "packages";
-      }
+          @Override
+          public String getParameters() {
+            return "packages";
+          }
 
-      @Override
-      public boolean process(String option, List<String> arguments) {
-        methodPackages = new 
HashSet<>(Arrays.asList(arguments.get(0).split(",")));
-        return true;
-      }
-    });
+          @Override
+          public boolean process(String option, List<String> arguments) {
+            methodPackages = new 
HashSet<>(Arrays.asList(arguments.get(0).split(",")));
+            return true;
+          }
+        });
     return options;
   }
 
@@ -205,10 +205,8 @@ public class MissingDoclet extends StandardDoclet {
 
     return super.run(docEnv);
   }
-  
-  /**
-   * Returns effective check level for this element
-   */
+
+  /** Returns effective check level for this element */
   private int level(Element element) {
     String pkg = 
elementUtils.getPackageOf(element).getQualifiedName().toString();
     if (methodPackages.contains(pkg)) {
@@ -217,24 +215,24 @@ public class MissingDoclet extends StandardDoclet {
       return level;
     }
   }
-  
-  /** 
-   * Check an individual element.
-   * This checks packages and types from the doctrees.
-   * It will recursively check methods/fields from encountered types when the 
level is "method"
+
+  /**
+   * Check an individual element. This checks packages and types from the 
doctrees. It will
+   * recursively check methods/fields from encountered types when the level is 
"method"
    */
   private void check(Element element) {
-    switch(element.getKind()) {
+    switch (element.getKind()) {
       case MODULE:
         // don't check the unnamed module, it won't have javadocs
-        if (!((ModuleElement)element).isUnnamed()) {
+        if (!((ModuleElement) element).isUnnamed()) {
           checkComment(element);
         }
         break;
       case PACKAGE:
         checkComment(element);
         break;
-      // class-like elements, check them, then recursively check their 
children (fields and methods)
+        // class-like elements, check them, then recursively check their 
children (fields and
+        // methods)
       case CLASS:
       case INTERFACE:
       case ENUM:
@@ -242,17 +240,18 @@ public class MissingDoclet extends StandardDoclet {
         if (level(element) >= CLASS) {
           checkComment(element);
           for (var subElement : element.getEnclosedElements()) {
-            // don't recurse into enclosed types, otherwise we'll double-check 
since they are already in the included docTree
-            if (subElement.getKind() == ElementKind.METHOD || 
-                subElement.getKind() == ElementKind.CONSTRUCTOR || 
-                subElement.getKind() == ElementKind.FIELD || 
-                subElement.getKind() == ElementKind.ENUM_CONSTANT) {
+            // don't recurse into enclosed types, otherwise we'll double-check 
since they are
+            // already in the included docTree
+            if (subElement.getKind() == ElementKind.METHOD
+                || subElement.getKind() == ElementKind.CONSTRUCTOR
+                || subElement.getKind() == ElementKind.FIELD
+                || subElement.getKind() == ElementKind.ENUM_CONSTANT) {
               check(subElement);
             }
           }
         }
         break;
-      // method-like elements, check them if we are configured to do so
+        // method-like elements, check them if we are configured to do so
       case METHOD:
       case CONSTRUCTOR:
       case FIELD:
@@ -267,9 +266,9 @@ public class MissingDoclet extends StandardDoclet {
   }
 
   /**
-   * Return true if the method is synthetic enum method (values/valueOf).
-   * According to the doctree documentation, the "included" set never includes 
synthetic elements.
-   * UweSays: It should not happen but it happens!
+   * Return true if the method is synthetic enum method (values/valueOf). 
According to the doctree
+   * documentation, the "included" set never includes synthetic elements. 
UweSays: It should not
+   * happen but it happens!
    */
   private boolean isSyntheticEnumMethod(Element element) {
     String simpleName = element.getSimpleName().toString();
@@ -280,20 +279,23 @@ public class MissingDoclet extends StandardDoclet {
     }
     return false;
   }
-  
+
   /**
-   * Checks that an element doesn't have missing javadocs.
-   * In addition to truly "missing", check that comments aren't solely 
whitespace (generated by some IDEs),
-   * that they aren't a license header masquerading as a javadoc comment.
+   * Checks that an element doesn't have missing javadocs. In addition to 
truly "missing", check
+   * that comments aren't solely whitespace (generated by some IDEs), that 
they aren't a license
+   * header masquerading as a javadoc comment.
    */
   private void checkComment(Element element) {
     // sanity check that the element is really "included", because we do some 
recursion into types
     if (!docEnv.isIncluded(element)) {
       return;
     }
-    // check that this element isn't on our ignore list. This is only used as 
a workaround for "split packages".
-    // ignoring a package isn't recursive (on purpose), we still check all the 
classes, etc. inside it.
-    // we just need to cope with the fact package-info.java isn't there 
because it is split across multiple jars.
+    // check that this element isn't on our ignore list. This is only used as 
a workaround for
+    // "split packages".
+    // ignoring a package isn't recursive (on purpose), we still check all the 
classes, etc. inside
+    // it.
+    // we just need to cope with the fact package-info.java isn't there 
because it is split across
+    // multiple jars.
     if (ignored.contains(element.toString())) {
       return;
     }
@@ -306,14 +308,17 @@ public class MissingDoclet extends StandardDoclet {
         error(element, "javadocs are missing");
       }
     } else {
-      var normalized = tree.getFirstSentence().get(0).toString()
-                       .replace('\u00A0', ' ')
-                       .trim()
-                       .toLowerCase(Locale.ROOT);
+      var normalized =
+          tree.getFirstSentence()
+              .get(0)
+              .toString()
+              .replace('\u00A0', ' ')
+              .trim()
+              .toLowerCase(Locale.ROOT);
       if (normalized.isEmpty()) {
         error(element, "blank javadoc comment");
-      } else if (normalized.startsWith("licensed to the apache software 
foundation") ||
-                 normalized.startsWith("copyright 2004 the apache software 
foundation")) {
+      } else if (normalized.startsWith("licensed to the apache software 
foundation")
+          || normalized.startsWith("copyright 2004 the apache software 
foundation")) {
         error(element, "comment is really a license");
       }
     }
@@ -323,13 +328,15 @@ public class MissingDoclet extends StandardDoclet {
   }
 
   private boolean hasInheritedJavadocs(Element element) {
-    boolean hasOverrides = element.getAnnotationMirrors().stream()
-        .anyMatch(ann -> 
ann.getAnnotationType().toString().equals(Override.class.getName()));
+    boolean hasOverrides =
+        element.getAnnotationMirrors().stream()
+            .anyMatch(ann -> 
ann.getAnnotationType().toString().equals(Override.class.getName()));
 
     if (hasOverrides) {
       // If an element has explicit @Overrides annotation, assume it does
       // have inherited javadocs somewhere.
-      reporter.print(Diagnostic.Kind.NOTE, element, "javadoc empty but 
@Override declared, skipping.");
+      reporter.print(
+          Diagnostic.Kind.NOTE, element, "javadoc empty but @Override 
declared, skipping.");
       return true;
     }
 
@@ -346,7 +353,10 @@ public class MissingDoclet extends StandardDoclet {
             // We could check supMethod for non-empty javadoc here. Don't know 
if this makes
             // sense though as all methods will be verified in the end so it'd 
fail on the
             // top of the hierarchy (if empty) anyway.
-            reporter.print(Diagnostic.Kind.NOTE, element, "javadoc empty but 
method overrides another, skipping.");
+            reporter.print(
+                Diagnostic.Kind.NOTE,
+                element,
+                "javadoc empty but method overrides another, skipping.");
             return true;
           }
         }
@@ -356,15 +366,14 @@ public class MissingDoclet extends StandardDoclet {
     return false;
   }
 
-
   /* Find types from which methods in type may inherit javadoc, in the proper 
order.*/
   private Stream<Element> superTypeForInheritDoc(Element type) {
     TypeElement clazz = (TypeElement) type;
-    List<Element> interfaces = clazz.getInterfaces()
-        .stream()
-        .filter(tm -> tm.getKind() == TypeKind.DECLARED)
-        .map(tm -> ((DeclaredType) tm).asElement())
-        .collect(Collectors.toList());
+    List<Element> interfaces =
+        clazz.getInterfaces().stream()
+            .filter(tm -> tm.getKind() == TypeKind.DECLARED)
+            .map(tm -> ((DeclaredType) tm).asElement())
+            .collect(Collectors.toList());
 
     Stream<Element> result = interfaces.stream();
     result = Stream.concat(result, 
interfaces.stream().flatMap(this::superTypeForInheritDoc));
@@ -386,13 +395,13 @@ public class MissingDoclet extends StandardDoclet {
       if (tree != null) {
         for (var tag : tree.getBlockTags()) {
           if (tag instanceof ParamTree) {
-            var name = ((ParamTree)tag).getName().getName().toString();
+            var name = ((ParamTree) tag).getName().getName().toString();
             seenParameters.add(name);
           }
         }
       }
       // now compare the method's formal parameter list against it
-      for (var param : ((ExecutableElement)element).getParameters()) {
+      for (var param : ((ExecutableElement) element).getParameters()) {
         var name = param.getSimpleName().toString();
         if (!seenParameters.contains(name)) {
           error(element, "missing javadoc @param for parameter '" + name + 
"'");
@@ -400,7 +409,7 @@ public class MissingDoclet extends StandardDoclet {
       }
     }
   }
-  
+
   /** logs a new error for the particular element */
   private void error(Element element, String message) {
     var fullMessage = new StringBuilder();
diff --git a/buildSrc/scriptDepVersions.gradle 
b/build-tools/scriptDepVersions.gradle
similarity index 97%
rename from buildSrc/scriptDepVersions.gradle
rename to build-tools/scriptDepVersions.gradle
index a1d2bc9467d..2a83896148e 100644
--- a/buildSrc/scriptDepVersions.gradle
+++ b/build-tools/scriptDepVersions.gradle
@@ -21,6 +21,7 @@
 
 ext {
   scriptDepVersions = [
+      "min-java-version": "11",
       "apache-rat": "0.15",
       "commons-codec": "1.16.0",
       "ecj": "3.33.0",
diff --git a/build.gradle b/build.gradle
index 3b3c0e0999e..3cecb1aa0cd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,6 +20,7 @@ import java.time.format.DateTimeFormatter
 
 plugins {
   id 'base'
+  id 'solr.build-infra'
   id 'com.palantir.consistent-versions' version '2.16.0'
   id 'org.owasp.dependencycheck' version '9.0.8'
   id 'ca.cutterslade.analyze' version '1.10.0'
@@ -30,9 +31,11 @@ plugins {
   id 'com.github.node-gradle.node' version '7.0.1' apply false
 }
 
+apply from: file('build-tools/scriptDepVersions.gradle')
+
 // Declare default Java versions for the entire project and for SolrJ 
separately
-rootProject.ext.minJavaVersionDefault = JavaVersion.VERSION_11
-rootProject.ext.minJavaVersionSolrJ = JavaVersion.VERSION_11
+rootProject.ext.minJavaVersionDefault = 
JavaVersion.toVersion(scriptDepVersions['min-java-version'])
+rootProject.ext.minJavaVersionSolrJ = 
JavaVersion.toVersion(scriptDepVersions['min-java-version'])
 
 apply from: file('gradle/globals.gradle')
 
@@ -108,14 +111,13 @@ ext {
   }
 }
 
-apply from: file('buildSrc/scriptDepVersions.gradle')
-
 // Include smaller chunks configuring dedicated build areas.
 // Some of these intersect or add additional functionality.
 // The order of inclusion of these files shouldn't matter (but may
 // if the build file is incorrectly written and evaluates something
 // eagerly).
 
+apply from: file('gradle/conventions.gradle')
 apply from: file('gradle/generation/local-settings.gradle')
 
 // Ant-compatibility layer: apply folder layout early so that
@@ -162,6 +164,14 @@ apply from: 
file('gradle/validation/solr.config-file-sanity.gradle')
 
 apply from: file('gradle/validation/spotless.gradle')
 
+// Wire up included builds to some validation tasks.
+rootProject.tasks.named("tidy").configure {
+  dependsOn gradle.includedBuilds*.task(":tidy")
+}
+rootProject.tasks.named("clean").configure {
+  dependsOn gradle.includedBuilds*.task(":clean")
+}
+
 // Source or data regeneration tasks
 apply from: file('gradle/generation/regenerate.gradle')
 apply from: file('gradle/generation/javacc.gradle')
diff --git 
a/buildSrc/src/main/java/org/apache/lucene/gradle/ErrorReportingTestListener.java
 
b/buildSrc/src/main/java/org/apache/lucene/gradle/ErrorReportingTestListener.java
deleted file mode 100644
index 44cd09b33f4..00000000000
--- 
a/buildSrc/src/main/java/org/apache/lucene/gradle/ErrorReportingTestListener.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.lucene.gradle;
-
-import java.io.*;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.regex.Pattern;
-
-import org.gradle.api.internal.tasks.testing.logging.FullExceptionFormatter;
-import org.gradle.api.internal.tasks.testing.logging.TestExceptionFormatter;
-import org.gradle.api.logging.Logger;
-import org.gradle.api.logging.Logging;
-import org.gradle.api.tasks.testing.TestDescriptor;
-import org.gradle.api.tasks.testing.TestListener;
-import org.gradle.api.tasks.testing.TestOutputEvent;
-import org.gradle.api.tasks.testing.TestOutputListener;
-import org.gradle.api.tasks.testing.TestResult;
-import org.gradle.api.tasks.testing.logging.TestLogging;
-
-/**
- * An error reporting listener that queues test output streams and displays 
them
- * on failure.
- * <p>
- * Heavily inspired by Elasticsearch's ErrorReportingTestListener (ASL 2.0 
licensed).
- */
-public class ErrorReportingTestListener implements TestOutputListener, 
TestListener {
-   private static final Logger LOGGER = 
Logging.getLogger(ErrorReportingTestListener.class);
-
-   private final TestExceptionFormatter formatter;
-   private final Map<TestKey, OutputHandler> outputHandlers = new 
ConcurrentHashMap<>();
-   private final Path spillDir;
-   private final Path outputsDir;
-   private final boolean verboseMode;
-
-   public ErrorReportingTestListener(TestLogging testLogging, Path spillDir, 
Path outputsDir, boolean verboseMode) {
-      this.formatter = new FullExceptionFormatter(testLogging);
-      this.spillDir = spillDir;
-      this.outputsDir = outputsDir;
-      this.verboseMode = verboseMode;
-   }
-
-   @Override
-   public void onOutput(TestDescriptor testDescriptor, TestOutputEvent 
outputEvent) {
-      handlerFor(testDescriptor).write(outputEvent);
-   }
-
-   @Override
-   public void beforeSuite(TestDescriptor suite) {
-      // noop.
-   }
-
-   @Override
-   public void beforeTest(TestDescriptor testDescriptor) {
-      // Noop.
-   }
-
-   @Override
-   public void afterSuite(final TestDescriptor suite, TestResult result) {
-      if (suite.getParent() == null || suite.getName().startsWith("Gradle")) {
-         return;
-      }
-
-      TestKey key = TestKey.of(suite);
-      try {
-         OutputHandler outputHandler = outputHandlers.get(key);
-         if (outputHandler != null) {
-            long length = outputHandler.length();
-            if (length > 1024 * 1024 * 10) {
-               LOGGER.warn(String.format(Locale.ROOT, "WARNING: Test %s wrote 
%,d bytes of output.",
-                   suite.getName(),
-                   length));
-            }
-         }
-
-         boolean echoOutput = Objects.equals(result.getResultType(), 
TestResult.ResultType.FAILURE);
-         boolean dumpOutput = echoOutput;
-
-         // If the test suite failed, report output.
-         if (dumpOutput || echoOutput) {
-            Files.createDirectories(outputsDir);
-            Path outputLog = outputsDir.resolve(getOutputLogName(suite));
-
-            // Save the output of a failing test to disk.
-            try (Writer w = Files.newBufferedWriter(outputLog, 
StandardCharsets.UTF_8)) {
-               if (outputHandler != null) {
-                  outputHandler.copyTo(w);
-               }
-            }
-
-            if (echoOutput && !verboseMode) {
-               synchronized (this) {
-                  System.out.println("");
-                  System.out.println(suite.getClassName() + " > test suite's 
output saved to " + outputLog + ", copied below:");
-                  try (BufferedReader reader = 
Files.newBufferedReader(outputLog, StandardCharsets.UTF_8)) {
-                     char[] buf = new char[1024];
-                     int len;
-                     while ((len = reader.read(buf)) >= 0) {
-                        System.out.print(new String(buf, 0, len));
-                     }
-                     System.out.println();
-                  }
-               }
-            }
-         }
-      } catch (IOException e) {
-         throw new UncheckedIOException(e);
-      } finally {
-         OutputHandler handler = outputHandlers.remove(key);
-         if (handler != null) {
-            try {
-               handler.close();
-            } catch (IOException e) {
-               LOGGER.error("Failed to close output handler for: " + key, e);
-            }
-         }
-      }
-   }
-
-   private static Pattern SANITIZE = Pattern.compile("[^a-zA-Z .\\-_0-9]+");
-
-   public static String getOutputLogName(TestDescriptor suite) {
-      return SANITIZE.matcher("OUTPUT-" + suite.getName() + 
".txt").replaceAll("_");
-   }
-
-   @Override
-   public void afterTest(TestDescriptor testDescriptor, TestResult result) {
-      // Include test failure exception stacktrace(s) in test output log.
-      if (result.getResultType() == TestResult.ResultType.FAILURE) {
-         if (result.getExceptions().size() > 0) {
-            String message = formatter.format(testDescriptor, 
result.getExceptions());
-            handlerFor(testDescriptor).write(message);
-         }
-      }
-   }
-
-   private OutputHandler handlerFor(TestDescriptor descriptor) {
-      // Attach output of leaves (individual tests) to their parent.
-      if (!descriptor.isComposite()) {
-         descriptor = descriptor.getParent();
-      }
-      return outputHandlers.computeIfAbsent(TestKey.of(descriptor), (key) -> 
new OutputHandler());
-   }
-
-   public static class TestKey {
-      private final String key;
-
-      private TestKey(String key) {
-         this.key = key;
-      }
-
-      public static TestKey of(TestDescriptor d) {
-         StringBuilder key = new StringBuilder();
-         key.append(d.getClassName());
-         key.append("::");
-         key.append(d.getName());
-         key.append("::");
-         key.append(d.getParent() == null ? "-" : d.getParent().toString());
-         return new TestKey(key.toString());
-      }
-
-      @Override
-      public boolean equals(Object o) {
-         return o != null &&
-             o.getClass() == this.getClass() &&
-             Objects.equals(((TestKey) o).key, key);
-      }
-
-      @Override
-      public int hashCode() {
-         return key.hashCode();
-      }
-
-       @Override
-       public String toString() {
-           return key;
-       }
-   }
-
-   private class OutputHandler implements Closeable {
-      // Max single-line buffer before automatic wrap occurs.
-      private static final int MAX_LINE_WIDTH = 1024 * 4;
-
-      private final SpillWriter buffer;
-
-      // internal stream.
-      private final PrefixedWriter sint;
-      // stdout
-      private final PrefixedWriter sout;
-      // stderr
-      private final PrefixedWriter serr;
-
-      // last used stream (so that we can flush it properly and prefixes are 
not screwed up).
-      private PrefixedWriter last;
-
-      public OutputHandler() {
-         buffer = new SpillWriter(() -> {
-            try {
-               return Files.createTempFile(spillDir, "spill-", ".tmp");
-            } catch (IOException e) {
-               throw new UncheckedIOException(e);
-            }
-         });
-
-         Writer sink = buffer;
-         if (verboseMode) {
-            sink = new StdOutTeeWriter(buffer);
-         }
-
-         sint = new PrefixedWriter("   > ", sink, MAX_LINE_WIDTH);
-         sout = new PrefixedWriter("  1> ", sink, MAX_LINE_WIDTH);
-         serr = new PrefixedWriter("  2> ", sink, MAX_LINE_WIDTH);
-         last = sint;
-      }
-
-      public void write(TestOutputEvent event) {
-         write((event.getDestination() == TestOutputEvent.Destination.StdOut ? 
sout : serr), event.getMessage());
-      }
-
-      public void write(String message) {
-         write(sint, message);
-      }
-
-      public long length() throws IOException {
-         return buffer.length();
-      }
-
-      private void write(PrefixedWriter out, String message) {
-         try {
-            if (out != last) {
-               last.completeLine();
-               last = out;
-            }
-            out.write(message);
-         } catch (IOException e) {
-            throw new UncheckedIOException("Unable to write to test output.", 
e);
-         }
-      }
-
-      public void copyTo(Writer out) throws IOException {
-         flush();
-         buffer.copyTo(out);
-      }
-
-      public void flush() throws IOException {
-         sout.completeLine();
-         serr.completeLine();
-         buffer.flush();
-      }
-
-      @Override
-      public void close() throws IOException {
-         buffer.close();
-      }
-   }
-}
diff --git 
a/buildSrc/src/main/java/org/apache/lucene/gradle/StdOutTeeWriter.java 
b/buildSrc/src/main/java/org/apache/lucene/gradle/StdOutTeeWriter.java
deleted file mode 100644
index 20a4c8524f6..00000000000
--- a/buildSrc/src/main/java/org/apache/lucene/gradle/StdOutTeeWriter.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.lucene.gradle;
-
-import java.io.IOException;
-import java.io.PrintStream;
-import java.io.Writer;
-
-class StdOutTeeWriter extends Writer {
-   private final Writer delegate;
-   private final PrintStream out = System.out;
-
-   public StdOutTeeWriter(Writer delegate) {
-      this.delegate = delegate;
-   }
-
-   @Override
-   public void write(int c) throws IOException {
-      delegate.write(c);
-      out.write(c);
-   }
-
-   @Override
-   public void write(char[] cbuf) throws IOException {
-      delegate.write(cbuf);
-      out.print(cbuf);
-   }
-
-   @Override
-   public void write(String str) throws IOException {
-      delegate.write(str);
-      out.print(str);
-   }
-
-   @Override
-   public void write(String str, int off, int len) throws IOException {
-      delegate.write(str, off, len);
-      out.append(str, off, len);
-   }
-
-   @Override
-   public Writer append(CharSequence csq) throws IOException {
-      delegate.append(csq);
-      out.append(csq);
-      return this;
-   }
-
-   @Override
-   public Writer append(CharSequence csq, int start, int end) throws 
IOException {
-      delegate.append(csq, start, end);
-      out.append(csq, start, end);
-      return this;
-   }
-
-   @Override
-   public Writer append(char c) throws IOException {
-      delegate.append(c);
-      out.append(c);
-      return this;
-   }
-
-   @Override
-   public void write(char[] cbuf, int off, int len) throws IOException {
-      delegate.write(cbuf, off, len);
-      out.print(new String(cbuf, off, len));
-   }
-
-   @Override
-   public void flush() throws IOException {
-      delegate.flush();
-      out.flush();
-   }
-
-   @Override
-   public void close() throws IOException {
-      delegate.close();
-      // Don't close the actual output.
-   }
-}
diff --git a/dev-tools/README.txt b/dev-tools/README.txt
index db5a8e179b2..76ae0c52f3a 100644
--- a/dev-tools/README.txt
+++ b/dev-tools/README.txt
@@ -5,7 +5,6 @@ as to the usefulness of the tools.
 
 Description of dev-tools/ contents:
 
-./missing-doclet -- JavaDoc validation doclet subproject
 ./doap/       -- Lucene and Solr project descriptors in DOAP RDF format.
 ./scripts/    -- Odds and ends for building releases, etc.
 ./test-patch/ -- Scripts for automatically validating patches
diff --git a/buildSrc/build.gradle b/gradle/conventions.gradle
similarity index 61%
rename from buildSrc/build.gradle
rename to gradle/conventions.gradle
index 91f88741322..fabc9b4cc58 100644
--- a/buildSrc/build.gradle
+++ b/gradle/conventions.gradle
@@ -15,24 +15,19 @@
  * limitations under the License.
  */
 
-repositories {
-  mavenCentral()
+configure(allprojects) {
+    tasks.register("tidy").configure {
+        description "Applies formatters and cleanups to sources."
+        group "verification"
+    }
 }
 
-ext {
-  // Minimum Java version required to compile buildSrc.
-  minJavaVersion = JavaVersion.VERSION_11
-}
-
-// Make sure the build environment is consistent.
-apply from: file('../gradle/validation/check-environment.gradle')
-
-// Load common buildSrc and script deps.
-apply from: file("scriptDepVersions.gradle")
-
-dependencies {
-  implementation gradleApi()
-  implementation localGroovy()
-
-  implementation 
"commons-codec:commons-codec:${scriptDepVersions['commons-codec']}"
+// Locate script-relative resource folder. This is context-sensitive so pass
+// the right buildscript (top-level).
+configure(rootProject) {
+    ext {
+        scriptResources = { buildscript ->
+            return 
file(buildscript.sourceFile.absolutePath.replaceAll('.gradle$', ""))
+        }
+    }
 }
diff --git a/gradle/testing/defaults-tests.gradle 
b/gradle/testing/defaults-tests.gradle
index d291ca85a40..f0af07c81de 100644
--- a/gradle/testing/defaults-tests.gradle
+++ b/gradle/testing/defaults-tests.gradle
@@ -18,7 +18,6 @@
 import org.apache.tools.ant.taskdefs.condition.Os
 import org.apache.tools.ant.types.Commandline
 import org.gradle.api.tasks.testing.logging.*
-import org.apache.lucene.gradle.ErrorReportingTestListener
 
 def resources = scriptResources(buildscript)
 def verboseModeHookInstalled = false
@@ -173,7 +172,7 @@ allprojects {
       }
 
       def spillDir = getTemporaryDir().toPath()
-      def listener = new ErrorReportingTestListener(test.testLogging, 
spillDir, testOutputsDir.toPath(), verboseMode)
+      def listener = 
buildinfra.newErrorReportingTestListener(test.testLogging, spillDir, 
testOutputsDir.toPath(), verboseMode)
       addTestOutputListener(listener)
       addTestListener(listener)
 
diff --git a/gradle/testing/failed-tests-at-end.gradle 
b/gradle/testing/failed-tests-at-end.gradle
index 5bffe9c9926..5b3381751d4 100644
--- a/gradle/testing/failed-tests-at-end.gradle
+++ b/gradle/testing/failed-tests-at-end.gradle
@@ -15,8 +15,6 @@
  * limitations under the License.
  */
 
-import org.apache.lucene.gradle.ErrorReportingTestListener
-
 // Display all failed tests at the end of the build.
 
 def failedTests = new LinkedHashSet() // for dedupe due to weird afterTest 
classMethod issue
@@ -29,7 +27,7 @@ def genFailInfo(def task, TestDescriptor desc) {
     historyUrl += "&tests.test=$desc.name"
     historyUrl += " 
http://fucit.org/solr-jenkins-reports/history-trend-of-recent-failures.html#series/$name";
   }
-  def logName = ErrorReportingTestListener.getOutputLogName(desc.parent ?: 
desc)
+  def logName = buildinfra.getOutputLogName(desc.parent ?: desc)
   def output = file("${task.testOutputsDir}/${logName}")
   def repro = "./gradlew ${task.project.path}:test --tests \"${name}\" 
${task.project.testOptionsForReproduceLine}"
   return ["name": name, "project": "${task.project.path}", "historyUrl": 
historyUrl, "output": output, "reproduce": repro]
diff --git a/gradle/testing/profiling.gradle b/gradle/testing/profiling.gradle
index 34b3efe59fa..ce9e7d43e03 100644
--- a/gradle/testing/profiling.gradle
+++ b/gradle/testing/profiling.gradle
@@ -15,8 +15,6 @@
  * limitations under the License.
  */
 
-import org.apache.lucene.gradle.ProfileResults;
-
 def recordings = files()
 
 allprojects {
@@ -48,7 +46,7 @@ allprojects {
 
 gradle.buildFinished {
   if (!recordings.isEmpty()) {
-    ProfileResults.printReport(recordings.getFiles().collect { it.toString() },
+    buildinfra.profileResultsClass().printReport(recordings.getFiles().collect 
{ it.toString() },
         propertyOrDefault(ProfileResults.MODE_KEY, 
ProfileResults.MODE_DEFAULT) as String,
         Integer.parseInt(propertyOrDefault(ProfileResults.STACKSIZE_KEY, 
ProfileResults.STACKSIZE_DEFAULT)),
         Integer.parseInt(propertyOrDefault(ProfileResults.COUNT_KEY, 
ProfileResults.COUNT_DEFAULT)),
diff --git a/gradle/validation/check-environment.gradle 
b/gradle/validation/check-environment.gradle
index d9ea66b694e..6fcaf761a19 100644
--- a/gradle/validation/check-environment.gradle
+++ b/gradle/validation/check-environment.gradle
@@ -31,6 +31,7 @@ configure(rootProject) {
   }
 
   def currentJavaVersion = JavaVersion.current()
+  def minJavaVersion = rootProject.sourceCompatibility// 
JavaVersion.VERSION_11 // TODO Write out java version
   if (currentJavaVersion < minJavaVersion) {
     throw new GradleException("At least Java ${minJavaVersion} is required, 
you are running Java ${currentJavaVersion} "
       + "[${System.getProperty('java.vm.name')} 
${System.getProperty('java.vm.version')}]")
diff --git a/gradle/validation/jar-checks.gradle 
b/gradle/validation/jar-checks.gradle
index d416a9561fd..6c87c50f023 100644
--- a/gradle/validation/jar-checks.gradle
+++ b/gradle/validation/jar-checks.gradle
@@ -24,8 +24,6 @@
 // Because of this all tasks here must always execute together, so they cannot 
define task outputs.
 // TODO: Rewrite the internal state to use state files containing the 
ext.jarInfos and its referencedFiles
 
-import org.apache.commons.codec.digest.DigestUtils
-
 // This should be false only for debugging.
 def failOnError = true
 
@@ -152,7 +150,7 @@ subprojects {
                 jarName        : file.toPath().getFileName().toString(),
                 path           : file,
                 module         : resolvedArtifact.moduleVersion,
-                checksum       : provider { new 
DigestUtils(DigestUtils.sha1Digest).digestAsHex(file).trim() },
+                checksum       : provider { 
buildinfra.sha1Digest().digestAsHex(file).trim() },
                 // We keep track of the files referenced by this dependency 
(sha, license, notice, etc.)
                 // so that we can determine unused dangling files later on.
                 referencedFiles: []
diff --git a/gradle/validation/owasp-dependency-check.gradle 
b/gradle/validation/owasp-dependency-check.gradle
index eb5961e8269..f6352877dda 100644
--- a/gradle/validation/owasp-dependency-check.gradle
+++ b/gradle/validation/owasp-dependency-check.gradle
@@ -26,7 +26,7 @@ configure(rootProject) {
   dependencyCheck {
     failBuildOnCVSS = propertyOrDefault("validation.owasp.threshold", 7) as 
Integer
     formats = ['ALL']
-    skipProjects = [':solr:solr-ref-guide', ':solr-missing-doclet']
+    skipProjects = [':solr:solr-ref-guide', ':missing-doclet']
     skipConfigurations = ['unifiedClasspath', 'permitUnusedDeclared']
     suppressionFile = file("${resources}/exclusions.xml")
     analyzers {
diff --git a/gradle/validation/rat-sources.gradle 
b/gradle/validation/rat-sources.gradle
index 91f2278e249..577529a9c16 100644
--- a/gradle/validation/rat-sources.gradle
+++ b/gradle/validation/rat-sources.gradle
@@ -96,10 +96,10 @@ allprojects {
                     exclude "dev-tools/scripts/README.md"
                     exclude "dev-tools/scripts/create_line_file_docs.py"
 
-                    // The root project also includes patterns for the 
boostrap (buildSrc) and composite
+                    // The root project also includes patterns for the include 
composite
                     // projects. Include their sources in the scan.
-                    include "buildSrc/src/**"
-                    include "dev-tools/solr-missing-doclet/src/**"
+                    include "build-tools/build-infra/src/**"
+                    include "build-tools/missing-doclet/src/**"
                     break
 
                 case ":solr:modules:clustering":
diff --git a/gradle/validation/spotless.gradle 
b/gradle/validation/spotless.gradle
index 95607c67327..76fecdfad98 100644
--- a/gradle/validation/spotless.gradle
+++ b/gradle/validation/spotless.gradle
@@ -20,9 +20,7 @@
  * spotless and Google Java Format.
  */
 
-def resources = scriptResources(buildscript)
-
-configure(project(":solr").subprojects) { prj ->
+configure(allprojects) { prj ->
   plugins.withType(JavaPlugin) {
     prj.apply plugin: 'com.diffplug.spotless'
 
@@ -95,23 +93,20 @@ configure(project(":solr").subprojects) { prj ->
 
   // Emit a custom message about how to fix formatting errors.
   tasks.matching { task -> task.name == "spotlessJavaCheck" }.configureEach {
-    runToFixMessage.set("\nIMPORTANT: run the top-level './gradlew tidy' to 
format code automatically (see help/formatting.txt for more info).")
+    it.runToFixMessage.set("\nIMPORTANT: run the top-level './gradlew tidy' to 
format code automatically (see help/formatting.txt for more info).")
   }
 
-  // Add an alias to 'spotlessApply' simply called 'tidy' and wire up
-  // spotlessCheck to convention's check.
-  task tidy() {
-    description "Applies formatters and cleanups to sources."
-    group "verification"
+  // Hook up spotless to tidy and check tasks.
+
+  tasks.matching { it.name == "tidy" }.configureEach { v ->
+    v.dependsOn tasks.matching { it.name == "spotlessApply" }
   }
 
-  tasks.matching { task -> task.name == "spotlessApply" }.configureEach { v ->
-    tidy.dependsOn v
-    v.dependsOn ":checkJdkInternalsExportedToGradle"
+  tasks.matching { it.name == "check" }.configureEach { v ->
+    v.dependsOn tasks.matching { it.name == "spotlessCheck" }
   }
 
-  tasks.matching { task -> task.name == "spotlessCheck" }.configureEach { v ->
-    check.dependsOn v
+  tasks.matching { task -> task.name in ["spotlessApply", "spotlessCheck"] 
}.configureEach { v ->
     v.dependsOn ":checkJdkInternalsExportedToGradle"
   }
 }
diff --git a/gradlew b/gradlew
index c0f76e91038..0aa671a76c2 100755
--- a/gradlew
+++ b/gradlew
@@ -158,7 +158,7 @@ fi
 
 GRADLE_WRAPPER_JAR="$APP_HOME/gradle/wrapper/gradle-wrapper.jar"
 if [ ! -e "$GRADLE_WRAPPER_JAR" ]; then
-    "$JAVACMD" $JAVA_OPTS 
"$APP_HOME/buildSrc/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java"
 "$GRADLE_WRAPPER_JAR"
+    "$JAVACMD" $JAVA_OPTS 
"$APP_HOME/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java"
 "$GRADLE_WRAPPER_JAR"
     WRAPPER_STATUS=$?
     if [ "$WRAPPER_STATUS" -eq 1 ]; then
         echo "ERROR: Something went wrong. Make sure you're using Java version 
between 11 and 21."
@@ -173,7 +173,7 @@ CLASSPATH=$GRADLE_WRAPPER_JAR
 # START OF LUCENE CUSTOMIZATION
 # Generate gradle.properties if they don't exist
 if [ ! -e "$APP_HOME/gradle.properties" ]; then
-    "$JAVACMD" $JAVA_OPTS 
"$APP_HOME/buildSrc/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java"
 "$APP_HOME/gradle/template.gradle.properties" "$APP_HOME/gradle.properties"
+    "$JAVACMD" $JAVA_OPTS 
"$APP_HOME/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java"
 "$APP_HOME/gradle/template.gradle.properties" "$APP_HOME/gradle.properties"
     GENERATOR_STATUS=$?
     if [ "$GENERATOR_STATUS" -ne 0 ]; then
         exit $GENERATOR_STATUS
diff --git a/gradlew.bat b/gradlew.bat
index 172618e3ea4..938e3ce94ee 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -76,7 +76,7 @@ goto fail
 @rem LUCENE-9266: verify and download the gradle wrapper jar if we don't have 
one.
 set GRADLE_WRAPPER_JAR=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 IF NOT EXIST "%GRADLE_WRAPPER_JAR%" (
-    "%JAVA_EXE%" %JAVA_OPTS% 
"%APP_HOME%/buildSrc/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java"
 "%GRADLE_WRAPPER_JAR%"
+    "%JAVA_EXE%" %JAVA_OPTS% 
"%APP_HOME%/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/WrapperDownloader.java"
 "%GRADLE_WRAPPER_JAR%"
     IF %ERRORLEVEL% EQU 1 goto failWithJvmMessage
     IF %ERRORLEVEL% NEQ 0 goto fail
 )
@@ -89,7 +89,7 @@ set CLASSPATH=%GRADLE_WRAPPER_JAR%
 IF NOT EXIST "%APP_HOME%\gradle.properties" (
   @rem local expansion is needed to check ERRORLEVEL inside control blocks.
   setlocal enableDelayedExpansion
-  "%JAVA_EXE%" %JAVA_OPTS% 
"%APP_HOME%/buildSrc/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java"
 "%APP_HOME%\gradle\template.gradle.properties" "%APP_HOME%\gradle.properties"
+  "%JAVA_EXE%" %JAVA_OPTS% 
"%APP_HOME%/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/GradlePropertiesGenerator.java"
 "%APP_HOME%\gradle\template.gradle.properties" "%APP_HOME%\gradle.properties"
   IF %ERRORLEVEL% NEQ 0 goto fail
   endlocal
 )
diff --git a/settings.gradle b/settings.gradle
index b39d92b1234..a9dd4a99ba1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -20,6 +20,8 @@ pluginManagement {
         mavenCentral()
         gradlePluginPortal()
     }
+
+    includeBuild("build-tools/build-infra")
 }
 
 plugins {
@@ -31,7 +33,7 @@ apply from: file('gradle/develocity.gradle')
 
 rootProject.name = "solr-root"
 
-includeBuild("dev-tools/solr-missing-doclet")
+includeBuild("build-tools/missing-doclet")
 
 include "solr:api"
 include "solr:solrj"
diff --git a/solr/distribution/build.gradle b/solr/distribution/build.gradle
index 8ebc5872f53..8609ffab173 100644
--- a/solr/distribution/build.gradle
+++ b/solr/distribution/build.gradle
@@ -15,8 +15,6 @@
  * limitations under the License.
  */
 
-import org.apache.lucene.gradle.Checksum
-
 import java.nio.charset.StandardCharsets
 import java.nio.file.Files
 
@@ -68,8 +66,7 @@ def fullDistTarTask = 
rootProject.getTasksByName("fullDistTar", true)[0]
 def slimDistTarTask = rootProject.getTasksByName("slimDistTar", true)[0]
 
 // Compute checksums for release archives.
-task computeChecksums(type: Checksum) {
-  algorithm = Checksum.Algorithm.SHA512
+task computeChecksums(type: buildinfra.checksumClass()) {
 
   files = objects.fileCollection()
   [


Reply via email to