You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by dw...@apache.org on 2021/12/19 11:05:19 UTC

[lucene] branch branch_9x updated (d2a022f -> 4102b98)

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

dweiss pushed a change to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git.


    from d2a022f  Reverting back to 9d19b49038b.
     new c3d3d07  Revert "Reverting back to 9d19b49038b."
     new 70b4239  LUCENE-10327: workaround for gradle emitting empty sourcepath.
     new 4102b98  Don't log warnings from ant (different class loader, I guess). Makes Alan happier.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 build.gradle                                       |   9 +-
 gradle/documentation/render-javadoc.gradle         |  68 +++--
 gradle/ide/eclipse.gradle                          |   6 +-
 gradle/ide/intellij-idea.gradle                    |   2 +-
 gradle/java/jar-manifest.gradle                    |  32 ---
 gradle/java/javac.gradle                           |   3 +-
 gradle/java/modules-debugging.gradle               |  84 ++++++
 gradle/java/modules.gradle                         | 257 ++++++++++++++++++
 gradle/maven/publications.gradle                   |  10 +-
 gradle/testing/randomization.gradle                |   4 +-
 gradle/validation/ecj-lint.gradle                  |  60 ++++-
 gradle/validation/ecj-lint/ecj.javadocs.prefs      |   8 +-
 gradle/validation/rat-sources.gradle               |   1 +
 lucene/CHANGES.txt                                 |   3 +
 lucene/analysis/common/build.gradle                |   2 +-
 lucene/analysis/common/src/java/module-info.java   | 222 ++++++++++++++++
 lucene/analysis/icu/build.gradle                   |   6 +-
 lucene/analysis/icu/src/java/module-info.java      |  37 +++
 lucene/analysis/kuromoji/build.gradle              |   4 +-
 lucene/analysis/kuromoji/src/java/module-info.java |  40 +++
 .../{kuromoji => morfologik.tests}/build.gradle    |  11 +-
 .../src/test/module-info.java}                     |  18 +-
 .../morfologik/tests/TestMorfologikAnalyzer.java   |  47 ++++
 lucene/analysis/morfologik/build.gradle            |  11 +-
 .../{build.gradle => src/java/module-info.java}    |  24 +-
 .../analysis/uk/UkrainianMorfologikAnalyzer.java   | 104 +++++---
 lucene/analysis/nori/build.gradle                  |   4 +-
 .../src/java/module-info.java}                     |  22 +-
 lucene/analysis/opennlp/build.gradle               |   6 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/analysis/phonetic/build.gradle              |   6 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/analysis/smartcn/build.gradle               |   4 +-
 .../src/java/module-info.java}                     |  17 +-
 lucene/analysis/stempel/build.gradle               |   4 +-
 .../src/java/module-info.java}                     |  18 +-
 lucene/backward-codecs/build.gradle                |   4 +-
 lucene/backward-codecs/src/java/module-info.java   |  47 ++++
 lucene/benchmark/build.gradle                      |  24 +-
 lucene/benchmark/src/java/module-info.java         |  40 +++
 lucene/classification/build.gradle                 |   6 +-
 .../src/java/module-info.java}                     |  18 +-
 lucene/codecs/build.gradle                         |   2 +-
 lucene/codecs/src/java/module-info.java            |  39 +++
 lucene/core/src/java/module-info.java              |  72 +++++
 lucene/demo/build.gradle                           |  12 +-
 .../src/java/module-info.java}                     |  24 +-
 lucene/distribution.tests/build.gradle             |  62 +++++
 .../lucene/distribution/TestModularLayer.java      | 295 +++++++++++++++++++++
 lucene/distribution/binary-release.gradle          |  59 +++--
 .../distribution/src/binary-release/bin/luke.cmd   |   2 +-
 lucene/distribution/src/binary-release/bin/luke.sh |   2 +-
 lucene/expressions/build.gradle                    |  17 +-
 .../src/java/module-info.java}                     |  19 +-
 lucene/facet/build.gradle                          |   6 +-
 .../src/java/module-info.java}                     |  25 +-
 lucene/grouping/build.gradle                       |   4 +-
 .../src/java/module-info.java}                     |  12 +-
 lucene/highlighter/build.gradle                    |   6 +-
 .../src/java/module-info.java}                     |  19 +-
 lucene/join/build.gradle                           |   2 +-
 .../src/java/module-info.java}                     |  11 +-
 lucene/licenses/asm-analysis-7.2.jar.sha1          |   1 +
 lucene/licenses/asm-tree-7.2.jar.sha1              |   1 +
 lucene/licenses/assertj-core-3.21.0.jar.sha1       |   1 +
 ...ICENSE-ASL.txt => assertj-core-LICENSE-ASL.txt} |   0
 ...ary-java-NOTICE.txt => assertj-core-NOTICE.txt} |   0
 lucene/luke/build.gradle                           |  53 ++--
 .../src/java/module-info.java}                     |  20 +-
 .../apache/lucene/luke/app/desktop/LukeMain.java   |  30 ++-
 .../lucene/luke/app/desktop/util/FontUtils.java    |   6 +-
 .../app/desktop/{font => util}/ElegantIcons.ttf    | Bin
 lucene/memory/build.gradle                         |   2 +-
 .../src/java/module-info.java}                     |  11 +-
 lucene/misc/build.gradle                           |   2 +-
 .../src/java/module-info.java}                     |  20 +-
 lucene/monitor/build.gradle                        |   6 +-
 .../src/java/module-info.java}                     |  14 +-
 lucene/queries/build.gradle                        |   2 +-
 .../src/java/module-info.java}                     |  24 +-
 lucene/queryparser/build.gradle                    |   6 +-
 lucene/queryparser/src/java/module-info.java       |  52 ++++
 lucene/replicator/build.gradle                     |   8 +-
 .../src/java/module-info.java}                     |  20 +-
 lucene/sandbox/build.gradle                        |   4 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/spatial-extras/build.gradle                 |  10 +-
 .../src/java/module-info.java}                     |  28 +-
 lucene/spatial3d/build.gradle                      |   2 +-
 .../src/java/module-info.java}                     |  12 +-
 lucene/suggest/build.gradle                        |   4 +-
 lucene/suggest/src/java/module-info.java           |  36 +++
 settings.gradle                                    |   8 +-
 versions.lock                                      |   5 +-
 versions.props                                     |   3 +-
 95 files changed, 1945 insertions(+), 498 deletions(-)
 create mode 100644 gradle/java/modules-debugging.gradle
 create mode 100644 gradle/java/modules.gradle
 create mode 100644 lucene/analysis/common/src/java/module-info.java
 create mode 100644 lucene/analysis/icu/src/java/module-info.java
 create mode 100644 lucene/analysis/kuromoji/src/java/module-info.java
 copy lucene/analysis/{kuromoji => morfologik.tests}/build.gradle (76%)
 copy lucene/analysis/{icu/build.gradle => morfologik.tests/src/test/module-info.java} (68%)
 create mode 100644 lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java
 copy lucene/analysis/morfologik/{build.gradle => src/java/module-info.java} (57%)
 copy lucene/analysis/{icu/build.gradle => nori/src/java/module-info.java} (54%)
 copy lucene/analysis/{icu/build.gradle => opennlp/src/java/module-info.java} (52%)
 copy lucene/analysis/{morfologik/build.gradle => phonetic/src/java/module-info.java} (58%)
 copy lucene/analysis/{icu/build.gradle => smartcn/src/java/module-info.java} (67%)
 copy lucene/analysis/{icu/build.gradle => stempel/src/java/module-info.java} (66%)
 create mode 100644 lucene/backward-codecs/src/java/module-info.java
 create mode 100644 lucene/benchmark/src/java/module-info.java
 copy lucene/{analysis/icu/build.gradle => classification/src/java/module-info.java} (70%)
 create mode 100644 lucene/codecs/src/java/module-info.java
 create mode 100644 lucene/core/src/java/module-info.java
 copy lucene/{analysis/morfologik/build.gradle => demo/src/java/module-info.java} (65%)
 create mode 100644 lucene/distribution.tests/build.gradle
 create mode 100644 lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java
 copy lucene/{analysis/icu/build.gradle => expressions/src/java/module-info.java} (70%)
 copy lucene/{analysis/morfologik/build.gradle => facet/src/java/module-info.java} (62%)
 copy lucene/{backward-codecs/build.gradle => grouping/src/java/module-info.java} (80%)
 copy lucene/{analysis/icu/build.gradle => highlighter/src/java/module-info.java} (67%)
 copy lucene/{backward-codecs/build.gradle => join/src/java/module-info.java} (81%)
 create mode 100644 lucene/licenses/asm-analysis-7.2.jar.sha1
 create mode 100644 lucene/licenses/asm-tree-7.2.jar.sha1
 create mode 100644 lucene/licenses/assertj-core-3.21.0.jar.sha1
 copy lucene/licenses/{commons-compress-LICENSE-ASL.txt => assertj-core-LICENSE-ASL.txt} (100%)
 copy lucene/licenses/{s2-geometry-library-java-NOTICE.txt => assertj-core-NOTICE.txt} (100%)
 copy lucene/{analysis/icu/build.gradle => luke/src/java/module-info.java} (71%)
 rename lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/{font => util}/ElegantIcons.ttf (100%)
 copy lucene/{backward-codecs/build.gradle => memory/src/java/module-info.java} (81%)
 copy lucene/{analysis/icu/build.gradle => misc/src/java/module-info.java} (67%)
 copy lucene/{analysis/stempel/build.gradle => monitor/src/java/module-info.java} (75%)
 copy lucene/{analysis/morfologik/build.gradle => queries/src/java/module-info.java} (61%)
 create mode 100644 lucene/queryparser/src/java/module-info.java
 copy lucene/{analysis/icu/build.gradle => replicator/src/java/module-info.java} (66%)
 copy lucene/{analysis/morfologik/build.gradle => sandbox/src/java/module-info.java} (60%)
 copy lucene/{analysis/icu/build.gradle => spatial-extras/src/java/module-info.java} (53%)
 copy lucene/{backward-codecs/build.gradle => spatial3d/src/java/module-info.java} (80%)
 create mode 100644 lucene/suggest/src/java/module-info.java

[lucene] 01/03: Revert "Reverting back to 9d19b49038b."

Posted by dw...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dweiss pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git

commit c3d3d0703b1b4b027e411a3f474a75825a144dd7
Author: Dawid Weiss <da...@carrotsearch.com>
AuthorDate: Sun Dec 19 11:59:13 2021 +0100

    Revert "Reverting back to 9d19b49038b."
    
    This reverts commit d2a022f49bb84a409c96307febd314da6eb22353.
---
 build.gradle                                       |   9 +-
 gradle/documentation/render-javadoc.gradle         |  68 +++--
 gradle/ide/eclipse.gradle                          |   6 +-
 gradle/ide/intellij-idea.gradle                    |   2 +-
 gradle/java/jar-manifest.gradle                    |  32 ---
 gradle/java/javac.gradle                           |   3 +-
 gradle/java/modules-debugging.gradle               |  84 ++++++
 gradle/java/modules.gradle                         | 254 ++++++++++++++++++
 gradle/maven/publications.gradle                   |  10 +-
 gradle/testing/randomization.gradle                |   4 +-
 gradle/validation/ecj-lint.gradle                  |  60 ++++-
 gradle/validation/ecj-lint/ecj.javadocs.prefs      |   8 +-
 lucene/CHANGES.txt                                 |   3 +
 lucene/analysis/common/build.gradle                |   2 +-
 lucene/analysis/common/src/java/module-info.java   | 222 ++++++++++++++++
 lucene/analysis/icu/build.gradle                   |   6 +-
 lucene/analysis/icu/src/java/module-info.java      |  37 +++
 lucene/analysis/kuromoji/build.gradle              |   4 +-
 lucene/analysis/kuromoji/src/java/module-info.java |  40 +++
 .../{kuromoji => morfologik.tests}/build.gradle    |  11 +-
 .../src/test/module-info.java}                     |  18 +-
 .../morfologik/tests/TestMorfologikAnalyzer.java   |  47 ++++
 lucene/analysis/morfologik/build.gradle            |  11 +-
 .../{build.gradle => src/java/module-info.java}    |  24 +-
 .../analysis/uk/UkrainianMorfologikAnalyzer.java   | 104 +++++---
 lucene/analysis/nori/build.gradle                  |   4 +-
 .../src/java/module-info.java}                     |  22 +-
 lucene/analysis/opennlp/build.gradle               |   6 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/analysis/phonetic/build.gradle              |   6 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/analysis/smartcn/build.gradle               |   4 +-
 .../src/java/module-info.java}                     |  17 +-
 lucene/analysis/stempel/build.gradle               |   4 +-
 .../src/java/module-info.java}                     |  18 +-
 lucene/backward-codecs/build.gradle                |   4 +-
 lucene/backward-codecs/src/java/module-info.java   |  47 ++++
 lucene/benchmark/build.gradle                      |  24 +-
 lucene/benchmark/src/java/module-info.java         |  40 +++
 lucene/classification/build.gradle                 |   6 +-
 .../src/java/module-info.java}                     |  18 +-
 lucene/codecs/build.gradle                         |   2 +-
 lucene/codecs/src/java/module-info.java            |  39 +++
 lucene/core/src/java/module-info.java              |  72 +++++
 lucene/demo/build.gradle                           |  12 +-
 .../src/java/module-info.java}                     |  24 +-
 lucene/distribution.tests/build.gradle             |  62 +++++
 .../lucene/distribution/TestModularLayer.java      | 295 +++++++++++++++++++++
 lucene/distribution/binary-release.gradle          |  59 +++--
 .../distribution/src/binary-release/bin/luke.cmd   |   2 +-
 lucene/distribution/src/binary-release/bin/luke.sh |   2 +-
 lucene/expressions/build.gradle                    |  17 +-
 .../src/java/module-info.java}                     |  19 +-
 lucene/facet/build.gradle                          |   6 +-
 .../src/java/module-info.java}                     |  25 +-
 lucene/grouping/build.gradle                       |   4 +-
 .../src/java/module-info.java}                     |  12 +-
 lucene/highlighter/build.gradle                    |   6 +-
 .../src/java/module-info.java}                     |  19 +-
 lucene/join/build.gradle                           |   2 +-
 .../src/java/module-info.java}                     |  11 +-
 lucene/licenses/asm-analysis-7.2.jar.sha1          |   1 +
 lucene/licenses/asm-tree-7.2.jar.sha1              |   1 +
 lucene/licenses/assertj-core-3.21.0.jar.sha1       |   1 +
 lucene/licenses/assertj-core-LICENSE-ASL.txt       | 201 ++++++++++++++
 lucene/licenses/assertj-core-NOTICE.txt            |   0
 lucene/luke/build.gradle                           |  53 ++--
 .../src/java/module-info.java}                     |  20 +-
 .../apache/lucene/luke/app/desktop/LukeMain.java   |  30 ++-
 .../lucene/luke/app/desktop/util/FontUtils.java    |   6 +-
 .../app/desktop/{font => util}/ElegantIcons.ttf    | Bin
 lucene/memory/build.gradle                         |   2 +-
 .../src/java/module-info.java}                     |  11 +-
 lucene/misc/build.gradle                           |   2 +-
 .../src/java/module-info.java}                     |  20 +-
 lucene/monitor/build.gradle                        |   6 +-
 .../src/java/module-info.java}                     |  14 +-
 lucene/queries/build.gradle                        |   2 +-
 .../src/java/module-info.java}                     |  24 +-
 lucene/queryparser/build.gradle                    |   6 +-
 lucene/queryparser/src/java/module-info.java       |  52 ++++
 lucene/replicator/build.gradle                     |   8 +-
 .../src/java/module-info.java}                     |  20 +-
 lucene/sandbox/build.gradle                        |   4 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/spatial-extras/build.gradle                 |  10 +-
 .../src/java/module-info.java}                     |  28 +-
 lucene/spatial3d/build.gradle                      |   2 +-
 .../src/java/module-info.java}                     |  12 +-
 lucene/suggest/build.gradle                        |   4 +-
 lucene/suggest/src/java/module-info.java           |  36 +++
 settings.gradle                                    |   8 +-
 versions.lock                                      |   5 +-
 versions.props                                     |   3 +-
 94 files changed, 2142 insertions(+), 498 deletions(-)

diff --git a/build.gradle b/build.gradle
index bf2f34d..220ada7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -110,6 +110,10 @@ apply from: file('buildSrc/scriptDepVersions.gradle')
 
 apply from: file('gradle/generation/local-settings.gradle')
 
+// IDE support, settings and specials.
+apply from: file('gradle/ide/intellij-idea.gradle')
+apply from: file('gradle/ide/eclipse.gradle')
+
 // Set up defaults and configure aspects for certain modules or functionality
 // (java, tests)
 apply from: file('gradle/java/folder-layout.gradle')
@@ -119,14 +123,11 @@ apply from: file('gradle/testing/randomization.gradle')
 apply from: file('gradle/testing/fail-on-no-tests.gradle')
 apply from: file('gradle/testing/alternative-jdk-support.gradle')
 apply from: file('gradle/java/jar-manifest.gradle')
+apply from: file('gradle/java/modules.gradle')
 
 // Maven artifact publishing.
 apply from: file('gradle/maven/publications.gradle')
 
-// IDE support, settings and specials.
-apply from: file('gradle/ide/intellij-idea.gradle')
-apply from: file('gradle/ide/eclipse.gradle')
-
 // Validation tasks
 apply from: file('gradle/validation/measure-task-times.gradle')
 apply from: file('gradle/validation/error-prone.gradle')
diff --git a/gradle/documentation/render-javadoc.gradle b/gradle/documentation/render-javadoc.gradle
index 11499be..9579c5d 100644
--- a/gradle/documentation/render-javadoc.gradle
+++ b/gradle/documentation/render-javadoc.gradle
@@ -59,20 +59,28 @@ allprojects {
       outputDir = project.javadoc.destinationDir
     }
 
-    task renderSiteJavadoc(type: RenderJavadocTask) {
-      description "Generates Javadoc API documentation for the site (relative links)."
-      group "documentation"
-
-      taskResources = resources
-      dependsOn sourceSets.main.compileClasspath
-      classpath = sourceSets.main.compileClasspath;
-      srcDirSet = sourceSets.main.java;
-
-      relativeProjectLinks = true
-
-      // Place the documentation under Lucene or Solr's documentation directory.
-      // docroot is defined in 'documentation.gradle'
-      outputDir = project.docroot.toPath().resolve(project.relativeDocPath).toFile()
+    if (project.path == ':lucene:luke' || project.path.endsWith(".tests")) {
+      // These projects are not part of the public API so we don't render their javadocs
+      // as part of the site's creation. A side-effect of this is that javadocs would not
+      // be linted for these projects. To avoid this, we connect the regular javadoc task
+      // to check so that everything is validated.
+      project.tasks.getByName("check").dependsOn renderJavadoc
+    } else {
+      task renderSiteJavadoc(type: RenderJavadocTask) {
+        description "Generates Javadoc API documentation for the site (relative links)."
+        group "documentation"
+
+        taskResources = resources
+        dependsOn sourceSets.main.compileClasspath
+        classpath = sourceSets.main.compileClasspath
+        srcDirSet = sourceSets.main.java
+
+        relativeProjectLinks = true
+
+        // Place the documentation under the documentation directory.
+        // docroot is defined in 'documentation.gradle'
+        outputDir = project.docroot.toPath().resolve(project.relativeDocPath).toFile()
+      }
     }
   }
 }
@@ -257,10 +265,6 @@ configure(subprojects) {
   }
 }
 
-configure(project(':lucene:luke')) {
-  project.tasks.matching { it.name == 'renderSiteJavadoc' }.configureEach { it.enabled = false }
-}
-
 class RenderJavadocTask extends DefaultTask {
   @InputFiles
   @SkipWhenEmpty
@@ -329,16 +333,18 @@ class RenderJavadocTask extends DefaultTask {
 
   @TaskAction
   public void render() {
-    def srcDirs = srcDirSet.srcDirs.findAll { dir -> dir.exists() }
+    def srcDirs = srcDirSet.sourceDirectories.filter { dir -> dir.exists() }
+
     def optionsFile = project.file("${getTemporaryDir()}/javadoc-options.txt")
     
     // create the directory, so relative link calculation knows that it's a directory:
     outputDir.mkdirs();
     
     def opts = []
-    opts << [ '-overview', project.file("${srcDirs[0]}/overview.html") ]
-    opts << [ '-sourcepath', srcDirs.join(File.pathSeparator) ]
-    opts << [ '-subpackages', project.path.startsWith(':lucene') ? 'org.apache.lucene' : 'org.apache.solr' ]
+
+    def overviewSourceSetDir = srcDirs.filter { dir -> project.file("${dir}/overview.html").exists() }.singleFile
+    opts << [ '-overview', project.file("${overviewSourceSetDir}/overview.html") ]
+
     opts << [ '-d', outputDir ]
     opts << '-protected'
     opts << [ '-encoding', 'UTF-8' ]
@@ -430,6 +436,24 @@ class RenderJavadocTask extends DefaultTask {
     def jOpts = opts.findAll { opt -> opt instanceof String && opt.startsWith("-J") }
     opts.removeAll(jOpts)
 
+    // Collect all source files, for now excluding module descriptors.
+    opts.addAll(
+        srcDirs.collectMany { dir ->
+          project.fileTree(dir: dir, include: "**/*.java", exclude: "**/module-info.java").files
+        }.collect { it.toString() }
+    )
+
+    // handle doc-files manually since in explicit source file mode javadoc does not copy them.
+    srcDirs.each { File dir ->
+      project.copy {
+        into outputDir
+
+        from(dir, {
+          include "**/doc-files/**"
+        })
+      }
+    }
+
     // Temporary file that holds all javadoc options for the current task (except jOpts)
     optionsFile.withWriter("UTF-8", { writer ->
       // escapes an option with single quotes or whitespace to be passed in the options.txt file for
diff --git a/gradle/ide/eclipse.gradle b/gradle/ide/eclipse.gradle
index 0a5b608..4db9ae3 100644
--- a/gradle/ide/eclipse.gradle
+++ b/gradle/ide/eclipse.gradle
@@ -58,7 +58,11 @@ configure(rootProject) {
             jars += prj.configurations.testCompileClasspath.resolve()
           }
 
-          classpath.entries += sources.sort().collect {name -> new SourceFolder(name, "build/eclipse/" + name) }
+          classpath.entries += sources.sort().collect { name -> 
+            def sourceFolder = new SourceFolder(name, "build/eclipse/" + name) 
+            sourceFolder.setExcludes(["module-info.java"])
+            return sourceFolder
+          }
           classpath.entries += jars.unique().findAll { location -> location.isFile() }.collect { location ->
             new LibEntry(location.toString())
           }
diff --git a/gradle/ide/intellij-idea.gradle b/gradle/ide/intellij-idea.gradle
index 1d5b8d2..589aaec 100644
--- a/gradle/ide/intellij-idea.gradle
+++ b/gradle/ide/intellij-idea.gradle
@@ -16,7 +16,7 @@
  */
 
 // Try to detect IntelliJ model loader ("reimport") early.
-def isIdea = System.getProperty("idea.active") != null ||
+rootProject.ext.isIdea = System.getProperty("idea.active") != null ||
     gradle.startParameter.taskNames.contains('idea') ||
     gradle.startParameter.taskNames.contains('cleanIdea')
 
diff --git a/gradle/java/jar-manifest.gradle b/gradle/java/jar-manifest.gradle
index d4ab812..359b579 100644
--- a/gradle/java/jar-manifest.gradle
+++ b/gradle/java/jar-manifest.gradle
@@ -69,12 +69,6 @@ subprojects {
               "X-Build-OS"            : "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}"
           ]
 
-          // Only apply automatic module name to jar task.
-          if (task.name in ["jar"]) {
-            manifestAttrs["Automatic-Module-Name"] =
-              "${->  project.path.replaceFirst(/^:lucene/, Matcher.quoteReplacement(project.group)).replace(':', '.').replace('-', '_')}"
-          }
-
           manifest {
             attributes(manifestAttrs)
           }
@@ -88,29 +82,3 @@ subprojects {
           }
       }
 }
-
-configure(rootProject) {
-  tasks.register("showModuleNames", { showModuleTask ->
-    def allJarTasks = []
-
-    rootProject.subprojects.each { subproject ->
-      subproject.tasks.matching { it.name == 'jar' }.all {
-        allJarTasks.add it
-      }
-    }
-
-    dependsOn allJarTasks
-
-    doFirst {
-      allJarTasks.each { jarTask ->
-        File jarFile = jarTask.outputs.files.singleFile
-        try (def jar = new JarFile(jarFile)) {
-          logger.lifecycle(String.format(Locale.ROOT,
-              "%-50s -> %s",
-              jarFile.name,
-              jar.manifest.mainAttributes.getValue("Automatic-Module-Name")))
-        }
-      }
-    }
-  })
-}
diff --git a/gradle/java/javac.gradle b/gradle/java/javac.gradle
index 78c5fb4..25f66c3 100644
--- a/gradle/java/javac.gradle
+++ b/gradle/java/javac.gradle
@@ -39,7 +39,8 @@ allprojects {
         "-Xlint:dep-ann",
         "-Xlint:divzero",
         "-Xlint:empty",
-        "-Xlint:exports",
+        // TODO: uh-oh we have broken APIs.
+        "-Xlint:-exports",
         "-Xlint:fallthrough",
         "-Xlint:finally",
         "-Xlint:opens",
diff --git a/gradle/java/modules-debugging.gradle b/gradle/java/modules-debugging.gradle
new file mode 100644
index 0000000..52bc6fe
--- /dev/null
+++ b/gradle/java/modules-debugging.gradle
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+// Debugging/ validation utilities and helpers to aid transition
+// to java modules.
+
+allprojects {
+  plugins.withType(JavaPlugin) {
+    // Show all non-empty package names
+    tasks.register("showPackageNames", { task ->
+      doFirst {
+        listPackageNames(sourceSets).each { println(it) }
+      }
+    })
+
+    tasks.register("showServiceProviders", { task ->
+      doFirst {
+        def services = listServices(sourceSets)
+        services.each { entry -> {
+          println(entry.key)
+          entry.value.each { println("  ${it}") }
+        }}
+      }
+    })
+  }
+}
+
+/* Utility method to collect all package names in a source sets. */
+static def listPackageNames(SourceSetContainer sourceSets) {
+  var pkgNameSet = [] as Set<String>
+  sourceSets.main.each { sourceSet ->
+    var dirs = sourceSet.allJava.srcDirTrees.collect { it.dir.toPath() }
+    var pattern = new PatternSet()
+        .include('**/*.java')
+        .exclude('module-info.java')
+        .exclude('**/package-info.java')
+    sourceSet.allJava.matching(pattern).each {srcFile ->
+      var srcPath = srcFile.toPath()
+      var dir = dirs.find { srcPath.startsWith(it) }
+      var pkgName = srcPath.subpath(dir.nameCount, srcPath.nameCount).parent.stream().map(Object::toString).collect(Collectors.joining('.'))
+      pkgNameSet.add(pkgName)
+    }
+  }
+  var pkgNames = pkgNameSet as List<String>
+  pkgNames.sort()
+  return pkgNames
+}
+
+/* Utility method to collect all service providers in a source sets. */
+static def listServices(SourceSetContainer sourceSets) {
+  def services = [:] as Map<String, List<String>>
+  sourceSets.main.each {sourceSet ->
+    var pattern = new PatternSet().include('META-INF/services/*')
+    sourceSet.resources.matching(pattern).each {file ->
+      def serviceName = file.name
+      def providers = []
+      file.withReader { reader -> {
+        reader.lines().each { l ->
+          def line = l.trim()
+          if (line != "" && !line.startsWith("#")) {
+            def provider = line.replace('$', '.')
+            providers.add(provider)
+          }
+        }
+      }}
+      services.put(serviceName, providers)
+    }
+  }
+  return services
+}
diff --git a/gradle/java/modules.gradle b/gradle/java/modules.gradle
new file mode 100644
index 0000000..45405a2
--- /dev/null
+++ b/gradle/java/modules.gradle
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+
+// Configure miscellaneous aspects required for supporting the java module system layer.
+
+allprojects {
+  plugins.withType(JavaPlugin) {
+    // We won't be using gradle's built-in automatic module finder.
+    java {
+      modularity.inferModulePath.set(false)
+    }
+
+    // Map convention configuration names to "modular" corresponding configurations.
+    Closure<String> moduleConfigurationNameFor = { String configurationName ->
+      return "module" + configurationName.capitalize().replace("Classpath", "Path")
+    }
+
+    //
+    // For each source set, create explicit configurations for declaring modular dependencies.
+    // These "modular" configurations correspond 1:1 to Gradle's conventions but have a 'module' prefix
+    // and a capitalized remaining part of the conventional name. For example, an 'api' configuration in
+    // the main source set would have a corresponding 'moduleApi' configuration for declaring modular
+    // dependencies.
+    //
+    // Gradle's java plugin "convention" configurations extend from their modular counterparts
+    // so all dependencies end up on classpath by default for backward compatibility with other
+    // tasks and gradle infrastructure.
+    //
+    // At the same time, we also know which dependencies (and their transitive graph of dependencies!)
+    // should be placed on module-path only.
+    //
+    // Note that an explicit configuration of modular dependencies also opens up the possibility of automatically
+    // validating whether the dependency configuration for a gradle project is consistent with the information in
+    // the module-info descriptor because there is a (nearly?) direct correspondence between the two:
+    //
+    // moduleApi            - 'requires transitive'
+    // moduleImplementation - 'requires'
+    // moduleCompileOnly    - 'requires static'
+    //
+    project.sourceSets.all { SourceSet sourceSet ->
+      ConfigurationContainer configurations = project.configurations
+
+      // Create modular configurations for convention configurations.
+      Closure<Configuration> createModuleConfigurationForConvention = { String configurationName ->
+        Configuration conventionConfiguration = configurations.maybeCreate(configurationName)
+        Configuration moduleConfiguration = configurations.maybeCreate(moduleConfigurationNameFor(configurationName))
+        moduleConfiguration.canBeConsumed(false)
+        moduleConfiguration.canBeResolved(false)
+        conventionConfiguration.extendsFrom(moduleConfiguration)
+
+        project.logger.info("Created module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}")
+        return moduleConfiguration
+      }
+
+      Configuration moduleApi = createModuleConfigurationForConvention(sourceSet.apiConfigurationName)
+      Configuration moduleImplementation = createModuleConfigurationForConvention(sourceSet.implementationConfigurationName)
+      moduleImplementation.extendsFrom(moduleApi)
+      Configuration moduleRuntimeOnly = createModuleConfigurationForConvention(sourceSet.runtimeOnlyConfigurationName)
+      Configuration moduleCompileOnly = createModuleConfigurationForConvention(sourceSet.compileOnlyConfigurationName)
+      // sourceSet.compileOnlyApiConfigurationName  // This seems like a very esoteric use case, leave out.
+
+      // Set up compilation module path configuration combining corresponding convention configurations.
+      Closure<Configuration> createResolvableModuleConfiguration = { String configurationName ->
+        Configuration conventionConfiguration = configurations.maybeCreate(configurationName)
+        Configuration moduleConfiguration = configurations.maybeCreate(
+            moduleConfigurationNameFor(conventionConfiguration.name))
+        moduleConfiguration.canBeConsumed(false)
+        moduleConfiguration.canBeResolved(true)
+        moduleConfiguration.attributes {
+          // Prefer class folders over JARs. The exception is made for tests projects which require a composition
+          // of classes and resources, otherwise split into two folders.
+          if (project.name.endsWith(".tests")) {
+            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR))
+          } else {
+            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.CLASSES))
+          }
+        }
+
+        project.logger.info("Created resolvable module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}")
+        return moduleConfiguration
+      }
+
+      Configuration compileModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.compileClasspathConfigurationName)
+      compileModulePathConfiguration.extendsFrom(moduleCompileOnly, moduleImplementation)
+
+      Configuration runtimeModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.runtimeClasspathConfigurationName)
+      runtimeModulePathConfiguration.extendsFrom(moduleRuntimeOnly, moduleImplementation)
+
+      // Create and register a source set extension for manipulating classpath/ module-path
+      ModularPathsExtension modularPaths = new ModularPathsExtension(project, sourceSet,
+          compileModulePathConfiguration,
+          runtimeModulePathConfiguration)
+      sourceSet.extensions.add("modularPaths", modularPaths)
+
+      // Customized the JavaCompile for this source set so that it has proper module path.
+      tasks.named(sourceSet.getCompileJavaTaskName()).configure({ JavaCompile task ->
+        task.dependsOn modularPaths.compileModulePathConfiguration
+
+        // Add modular dependencies and their transitive dependencies to module path.
+        task.options.compilerArgumentProviders.add((CommandLineArgumentProvider) {
+          def modularPathFiles = modularPaths.compileModulePathConfiguration.files
+          def extraArgs = []
+          if (!modularPathFiles.isEmpty()) {
+            if (!modularPaths.hasModuleDescriptor()) {
+              // We're compiling a non-module so we'll bring everything on module path in
+              // otherwise things wouldn't be part of the resolved module graph.
+              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+            }
+
+            extraArgs += ["--module-path", modularPathFiles.join(File.pathSeparator)]
+          }
+
+          task.logger.info("Module path for ${task.path}:\n  " + modularPathFiles.sort().join("\n  "))
+
+          return extraArgs
+        })
+
+        // LUCENE-10304: if we modify the classpath here, IntelliJ no longer sees the dependencies as compile-time
+        // dependencies, don't know why.
+        if (!rootProject.ext.isIdea) {
+          // Modify the default classpath by removing anything already placed on module path.
+          // This could be done in a fancier way but a set difference is just fine for us here. Use a lazy
+          // provider to delay computation of the actual path.
+          task.classpath = files({ ->
+            def trimmedClasspath = sourceSet.compileClasspath - modularPaths.compileModulePathConfiguration
+            task.logger.info("Class path for ${task.path}:\n  " + trimmedClasspath.files.sort().join("\n  "))
+            return trimmedClasspath
+          })
+        }
+      })
+    }
+
+    //
+    // Configure the (default) test task to use module paths.
+    //
+    // There is no explicit connection between source sets and test tasks so there is no way (?)
+    // to do this automatically, convention-style.
+
+    // This closure can be used to configure a different task, with a different source set, should we
+    // have the need for it.
+    Closure<Void> configureTestTaskForSourceSet = { Test task, SourceSet sourceSet ->
+      task.configure {
+        Configuration modulePath = task.project.configurations.maybeCreate(
+            moduleConfigurationNameFor(sourceSet.getRuntimeClasspathConfigurationName()))
+
+        task.dependsOn modulePath
+
+        // Add modular dependencies and their transitive dependencies to module path.
+        task.jvmArgumentProviders.add((CommandLineArgumentProvider) {
+          def extraArgs = []
+
+          // Determine whether the source set classes themselves should be appended
+          // to classpath or module path.
+          boolean sourceSetIsAModule = sourceSet.modularPaths.hasModuleDescriptor()
+
+          if (!modulePath.isEmpty() || sourceSetIsAModule) {
+            if (sourceSetIsAModule) {
+              // Add source set outputs to module path.
+              extraArgs += ["--module-path", (modulePath + sourceSet.output.classesDirs).files.join(File.pathSeparator)]
+              // Ideally, we should only add the sourceset's module here, everything else would be resolved via the
+              // module descriptor. But this would require parsing the module descriptor and may cause JVM version conflicts
+              // so keeping it simple.
+              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+            } else {
+              extraArgs += ["--module-path", modulePath.files.join(File.pathSeparator)]
+              // In this case we're running a non-module against things on the module path so let's bring in
+              // everything on module path into the resolution graph.
+              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+            }
+          }
+
+          task.logger.info("Module path for ${task.path}:\n  " + modulePath.files.sort().join("\n  "))
+
+          return extraArgs
+        })
+
+
+        // Modify the default classpath by removing anything already placed on module path.
+        // This could be done in a fancier way but a set difference is just fine for us here. Use a lazy
+        // provider to delay computation of the actual path.
+        task.classpath = files({ ->
+          def trimmedClasspath = sourceSet.runtimeClasspath - modulePath
+
+          boolean sourceSetIsAModule = sourceSet.modularPaths.hasModuleDescriptor()
+          if (sourceSetIsAModule) {
+            // also subtract the sourceSet's output directories.
+            trimmedClasspath = trimmedClasspath - sourceSet.output.classesDirs
+          }
+
+          task.logger.info("Class path for ${task.path}:\n  " + trimmedClasspath.files.sort().join("\n  "))
+          return trimmedClasspath
+        })
+      }
+    }
+
+    // Configure (tasks.test, sourceSets.test)
+    tasks.matching { it.name == "test" }.all { Test task ->
+      configureTestTaskForSourceSet(task, task.project.sourceSets.test)
+    }
+
+    // Configure module versions.
+    tasks.withType(JavaCompile).configureEach { task ->
+      // TODO: LUCENE-10267: workaround for gradle bug. Remove when the corresponding issue is fixed.
+      task.options.compilerArgumentProviders.add((CommandLineArgumentProvider) { ->
+        if (task.getClasspath().isEmpty()) {
+          return ["--module-version", project.version.toString()]
+        } else {
+          return []
+        }
+      })
+
+      task.options.javaModuleVersion.set(provider {
+        return project.version.toString()
+      })
+    }
+  }
+}
+
+
+class ModularPathsExtension {
+  Project project
+  SourceSet sourceSet
+  Configuration compileModulePathConfiguration
+  Configuration runtimeModulePathConfiguration
+
+  ModularPathsExtension(Project project, SourceSet sourceSet,
+                        Configuration compileModulePathConfiguration,
+                        Configuration runtimeModulePathConfiguration) {
+    this.project =  project
+    this.sourceSet = sourceSet
+    this.compileModulePathConfiguration = compileModulePathConfiguration
+    this.runtimeModulePathConfiguration = runtimeModulePathConfiguration
+  }
+
+  boolean hasModuleDescriptor() {
+    return sourceSet.allJava.srcDirs.stream()
+        .map(dir -> new File(dir, "module-info.java"))
+        .anyMatch(file -> file.exists())
+  }
+}
\ No newline at end of file
diff --git a/gradle/maven/publications.gradle b/gradle/maven/publications.gradle
index 32422cd..cc9bc96 100644
--- a/gradle/maven/publications.gradle
+++ b/gradle/maven/publications.gradle
@@ -31,15 +31,19 @@
 configure(rootProject) {
   ext {
     mavenProjects = project(":lucene").subprojects.findAll {subproject ->
-      return !(subproject.path in [
-          // Exclude distribution assembly & documentation.
+      def excluded = [
+          // Exclude distribution assembly, tests & documentation.
           ":lucene:distribution",
           ":lucene:documentation",
           // Exclude the parent container project for analysis modules (no artifacts).
           ":lucene:analysis",
           // Exclude the native module.
           ":lucene:misc:native"
-      ])
+      ]
+
+      // Exclude all subprojects that are modular test projects and those explicitly
+      // excluded above.
+      return !(subproject.path.endsWith(".tests") || subproject.path in excluded)
     }
   }
 }
diff --git a/gradle/testing/randomization.gradle b/gradle/testing/randomization.gradle
index fc6c991..938ca39 100644
--- a/gradle/testing/randomization.gradle
+++ b/gradle/testing/randomization.gradle
@@ -164,7 +164,9 @@ allprojects {
         // Enable security manager, if requested. We could move the selection of security manager and security policy
         // to each project's build/ configuration but it seems compact enough to keep it here for now.
         if (Boolean.parseBoolean(testOptionsResolved["tests.useSecurityManager"])) {
-          if (project.path == ":lucene:replicator") {
+          if (project.path.endsWith(".tests")) {
+            // LUCENE-10301: for now, do not use the security manager for modular tests (test framework is not available).
+          } else if (project.path == ":lucene:replicator") {
             systemProperty 'java.security.manager', "org.apache.lucene.util.TestSecurityManager"
             systemProperty 'java.security.policy', file("${resources}/policies/replicator-tests.policy")
           } else if (project.path.startsWith(":lucene")) {
diff --git a/gradle/validation/ecj-lint.gradle b/gradle/validation/ecj-lint.gradle
index 398c26e..591eb84 100644
--- a/gradle/validation/ecj-lint.gradle
+++ b/gradle/validation/ecj-lint.gradle
@@ -35,9 +35,10 @@ allprojects {
     // with a non-empty java.srcDirs. These tasks are then
     // attached to project's "ecjLint" task.
     def lintTasks = sourceSets.collect { sourceSet ->
-      def srcDirs = sourceSet.java.srcDirs.findAll { dir -> dir.exists() }
+      def srcDirs = sourceSet.java.sourceDirectories
+          .filter { dir -> dir.exists() }
 
-      tasks.create(sourceSet.getTaskName("ecjLint", null), JavaExec, {task ->
+      tasks.create(sourceSet.getTaskName("ecjLint", null), JavaExec, {JavaExec task ->
         // This dependency is on a configuration; technically it causes
         // all dependencies to be resolved before this task executes
         // (this includes scheduling tasks that compile the
@@ -60,6 +61,24 @@ allprojects {
         def tmpDst = getTemporaryDir()
         workingDir tmpDst
 
+        // Place input files in an external file to dodge command line argument
+        // limits. We could pass a directory but ecj seems to be buggy: when it
+        // encounters a module-info.java file it no longer compiles other source files.
+        def inputsFile = file("${tmpDst}/ecj-inputs.txt")
+        // escape filename accoring to ECJ's rules:
+        // https://github.com/eclipse/aspectj.eclipse.jdt.core/blob/a05312e746b9bc2b48b4b039f6e7b5e061b5b393/org.eclipse.jdt.core/batch/org/eclipse/jdt/internal/compiler/batch/Main.java#L1533-L1537
+        // Basically surround all whitespace by quotes:
+        def escapeFileName = { String s -> s.replaceAll(/ +/, /"$0"/) }
+        inputsFile.setText(
+            srcDirs.collectMany { dir ->
+              project.fileTree(dir: dir, include: "**/*.java" ).files
+            }
+            // Try to sort all input files; a side-effect of this should be that module-info.java
+            // is placed first on the list, which works around ECJ bug:
+            // https://bugs.eclipse.org/bugs/show_bug.cgi?id=569833
+            .sort()
+            .collect {file -> escapeFileName(file.absolutePath.toString())}.join("\n"), "UTF-8")
+
         args += [ "-d", "none" ]
 
         // Compilation environment.
@@ -72,22 +91,41 @@ allprojects {
         args += [ "-enableJavadoc" ]
         args += [ "-properties", file("${resources}/ecj.javadocs.prefs").absolutePath ]
 
-        doFirst {
-          tmpDst.mkdirs()
+        // We depend on modular paths.
+        def modularPaths = sourceSet.modularPaths
+        dependsOn modularPaths.compileModulePathConfiguration
+
+        task.argumentProviders.add((CommandLineArgumentProvider) {
+          // Add modular dependencies and their transitive dependencies to module path.
+          def modularPathFiles = modularPaths.compileModulePathConfiguration.files
+          def extraArgs = []
+          if (!modularPathFiles.isEmpty()) {
+            if (!modularPaths.hasModuleDescriptor()) {
+              // We're compiling a non-module so we'll bring everything on module path in
+              // otherwise things wouldn't be part of the resolved module graph.
+              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+            }
+
+            extraArgs += ["--module-path", modularPathFiles.join(File.pathSeparator)]
+          }
 
-          // Add classpath locations at execution time (can't resolve the
+          // Add classpath locations in a lazy provider (can't resolve the
           // configuration at evaluation time). Filter out non-existing entries
           // (output folders for non-existing input source dirs like resources).
           def cpath = sourceSet.compileClasspath.filter { p -> p.exists() }
+          cpath = cpath - modularPathFiles
           if (!cpath.isEmpty()) {
-            args += ["-classpath", cpath.asPath]
+            extraArgs += ["-classpath", cpath.join(File.pathSeparator)]
           }
 
-          // Add source location(s). Ideally we'd provide a set of files as in:
-          // args += sourceSet.java.files
-          // but this exceeds max allowed command line size. So we pass source
-          // directories instead:
-          args += srcDirs
+          // Add source location(s) in an external file to avoid command line argument limits.
+          extraArgs += ["@" + inputsFile.absolutePath]
+
+          return extraArgs
+        })
+
+        doFirst {
+          tmpDst.mkdirs()
         }
       })
     }
diff --git a/gradle/validation/ecj-lint/ecj.javadocs.prefs b/gradle/validation/ecj-lint/ecj.javadocs.prefs
index a5352aa..25d527a 100644
--- a/gradle/validation/ecj-lint/ecj.javadocs.prefs
+++ b/gradle/validation/ecj-lint/ecj.javadocs.prefs
@@ -17,7 +17,8 @@ org.eclipse.jdt.core.compiler.debug.lineNumber=generate
 org.eclipse.jdt.core.compiler.debug.localVariable=generate
 org.eclipse.jdt.core.compiler.debug.sourceFile=generate
 org.eclipse.jdt.core.compiler.doc.comment.support=enabled
-org.eclipse.jdt.core.compiler.problem.APILeak=error
+# TODO: disabled because we do have api leaks in modules
+org.eclipse.jdt.core.compiler.problem.APILeak=ignore
 org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=error
 org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=error
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
@@ -120,7 +121,8 @@ org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=error
 org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
 org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
 org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
-org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=error
+# TODO: ideally, we shouldn't rely on these... but we do.
+org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=ignore
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
@@ -144,4 +146,4 @@ org.eclipse.jdt.core.compiler.release=disabled
 org.eclipse.jdt.core.compiler.source=11
 org.eclipse.jdt.core.compiler.taskCaseSensitive=enabled
 org.eclipse.jdt.core.compiler.taskPriorities=HIGH
-org.eclipse.jdt.core.compiler.taskTags=nocommit
+org.eclipse.jdt.core.compiler.taskTags=nocommit
\ No newline at end of file
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 6460edd..0584b68 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -14,6 +14,9 @@ API Changes
 New Features
 ---------------------
 
+* LUCENE-10255: Lucene JARs are now proper modules, with module descriptors and dependency information.
+  (Chris Hegarty, Uwe Schindler, Tomoko Uchida, Dawid Weiss)
+
 * LUCENE-10223: Add interval function support to StandardQueryParser. Add min-should-match operator
   support to StandardQueryParser. Update and clean up package documentation in flexible query parser
   module. (Dawid Weiss, Alan Woodward)
diff --git a/lucene/analysis/common/build.gradle b/lucene/analysis/common/build.gradle
index 6fba70a..1815041 100644
--- a/lucene/analysis/common/build.gradle
+++ b/lucene/analysis/common/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 description = 'Analyzers for indexing content in different languages and domains'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
   testImplementation project(':lucene:test-framework')
 }
 
diff --git a/lucene/analysis/common/src/java/module-info.java b/lucene/analysis/common/src/java/module-info.java
new file mode 100644
index 0000000..e736a95
--- /dev/null
+++ b/lucene/analysis/common/src/java/module-info.java
@@ -0,0 +1,222 @@
+/*
+ * 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.
+ */
+
+/** Lucene Analysis Common. */
+module org.apache.lucene.analysis.common {
+  requires java.xml;
+  requires org.apache.lucene.core;
+
+  exports org.apache.lucene.analysis.ar;
+  exports org.apache.lucene.analysis.bg;
+  exports org.apache.lucene.analysis.bn;
+  exports org.apache.lucene.analysis.boost;
+  exports org.apache.lucene.analysis.br;
+  exports org.apache.lucene.analysis.ca;
+  exports org.apache.lucene.analysis.charfilter;
+  exports org.apache.lucene.analysis.cjk;
+  exports org.apache.lucene.analysis.ckb;
+  exports org.apache.lucene.analysis.classic;
+  exports org.apache.lucene.analysis.commongrams;
+  exports org.apache.lucene.analysis.compound.hyphenation;
+  exports org.apache.lucene.analysis.compound;
+  exports org.apache.lucene.analysis.core;
+  exports org.apache.lucene.analysis.custom;
+  exports org.apache.lucene.analysis.cz;
+  exports org.apache.lucene.analysis.da;
+  exports org.apache.lucene.analysis.de;
+  exports org.apache.lucene.analysis.el;
+  exports org.apache.lucene.analysis.email;
+  exports org.apache.lucene.analysis.en;
+  exports org.apache.lucene.analysis.es;
+  exports org.apache.lucene.analysis.et;
+  exports org.apache.lucene.analysis.eu;
+  exports org.apache.lucene.analysis.fa;
+  exports org.apache.lucene.analysis.fi;
+  exports org.apache.lucene.analysis.fr;
+  exports org.apache.lucene.analysis.ga;
+  exports org.apache.lucene.analysis.gl;
+  exports org.apache.lucene.analysis.hi;
+  exports org.apache.lucene.analysis.hu;
+  exports org.apache.lucene.analysis.hunspell;
+  exports org.apache.lucene.analysis.hy;
+  exports org.apache.lucene.analysis.id;
+  exports org.apache.lucene.analysis.in;
+  exports org.apache.lucene.analysis.it;
+  exports org.apache.lucene.analysis.lt;
+  exports org.apache.lucene.analysis.lv;
+  exports org.apache.lucene.analysis.minhash;
+  exports org.apache.lucene.analysis.miscellaneous;
+  exports org.apache.lucene.analysis.ne;
+  exports org.apache.lucene.analysis.ngram;
+  exports org.apache.lucene.analysis.nl;
+  exports org.apache.lucene.analysis.no;
+  exports org.apache.lucene.analysis.path;
+  exports org.apache.lucene.analysis.pattern;
+  exports org.apache.lucene.analysis.payloads;
+  exports org.apache.lucene.analysis.pt;
+  exports org.apache.lucene.analysis.query;
+  exports org.apache.lucene.analysis.reverse;
+  exports org.apache.lucene.analysis.ro;
+  exports org.apache.lucene.analysis.ru;
+  exports org.apache.lucene.analysis.shingle;
+  exports org.apache.lucene.analysis.sinks;
+  exports org.apache.lucene.analysis.snowball;
+  exports org.apache.lucene.analysis.sr;
+  exports org.apache.lucene.analysis.sv;
+  exports org.apache.lucene.analysis.synonym;
+  exports org.apache.lucene.analysis.ta;
+  exports org.apache.lucene.analysis.te;
+  exports org.apache.lucene.analysis.th;
+  exports org.apache.lucene.analysis.tr;
+  exports org.apache.lucene.analysis.util;
+  exports org.apache.lucene.analysis.wikipedia;
+  exports org.apache.lucene.collation.tokenattributes;
+  exports org.apache.lucene.collation;
+  exports org.tartarus.snowball.ext;
+  exports org.tartarus.snowball;
+
+  provides org.apache.lucene.analysis.CharFilterFactory with
+      org.apache.lucene.analysis.charfilter.HTMLStripCharFilterFactory,
+      org.apache.lucene.analysis.charfilter.MappingCharFilterFactory,
+      org.apache.lucene.analysis.cjk.CJKWidthCharFilterFactory,
+      org.apache.lucene.analysis.fa.PersianCharFilterFactory,
+      org.apache.lucene.analysis.pattern.PatternReplaceCharFilterFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.tr.ApostropheFilterFactory,
+      org.apache.lucene.analysis.ar.ArabicNormalizationFilterFactory,
+      org.apache.lucene.analysis.ar.ArabicStemFilterFactory,
+      org.apache.lucene.analysis.bg.BulgarianStemFilterFactory,
+      org.apache.lucene.analysis.boost.DelimitedBoostTokenFilterFactory,
+      org.apache.lucene.analysis.bn.BengaliNormalizationFilterFactory,
+      org.apache.lucene.analysis.bn.BengaliStemFilterFactory,
+      org.apache.lucene.analysis.br.BrazilianStemFilterFactory,
+      org.apache.lucene.analysis.cjk.CJKBigramFilterFactory,
+      org.apache.lucene.analysis.cjk.CJKWidthFilterFactory,
+      org.apache.lucene.analysis.ckb.SoraniNormalizationFilterFactory,
+      org.apache.lucene.analysis.ckb.SoraniStemFilterFactory,
+      org.apache.lucene.analysis.classic.ClassicFilterFactory,
+      org.apache.lucene.analysis.commongrams.CommonGramsFilterFactory,
+      org.apache.lucene.analysis.commongrams.CommonGramsQueryFilterFactory,
+      org.apache.lucene.analysis.compound.DictionaryCompoundWordTokenFilterFactory,
+      org.apache.lucene.analysis.compound.HyphenationCompoundWordTokenFilterFactory,
+      org.apache.lucene.analysis.core.DecimalDigitFilterFactory,
+      org.apache.lucene.analysis.core.LowerCaseFilterFactory,
+      org.apache.lucene.analysis.core.StopFilterFactory,
+      org.apache.lucene.analysis.core.TypeTokenFilterFactory,
+      org.apache.lucene.analysis.core.UpperCaseFilterFactory,
+      org.apache.lucene.analysis.cz.CzechStemFilterFactory,
+      org.apache.lucene.analysis.de.GermanLightStemFilterFactory,
+      org.apache.lucene.analysis.de.GermanMinimalStemFilterFactory,
+      org.apache.lucene.analysis.de.GermanNormalizationFilterFactory,
+      org.apache.lucene.analysis.de.GermanStemFilterFactory,
+      org.apache.lucene.analysis.el.GreekLowerCaseFilterFactory,
+      org.apache.lucene.analysis.el.GreekStemFilterFactory,
+      org.apache.lucene.analysis.en.EnglishMinimalStemFilterFactory,
+      org.apache.lucene.analysis.en.EnglishPossessiveFilterFactory,
+      org.apache.lucene.analysis.en.KStemFilterFactory,
+      org.apache.lucene.analysis.en.PorterStemFilterFactory,
+      org.apache.lucene.analysis.es.SpanishLightStemFilterFactory,
+      org.apache.lucene.analysis.es.SpanishMinimalStemFilterFactory,
+      org.apache.lucene.analysis.es.SpanishPluralStemFilterFactory,
+      org.apache.lucene.analysis.fa.PersianNormalizationFilterFactory,
+      org.apache.lucene.analysis.fi.FinnishLightStemFilterFactory,
+      org.apache.lucene.analysis.fr.FrenchLightStemFilterFactory,
+      org.apache.lucene.analysis.fr.FrenchMinimalStemFilterFactory,
+      org.apache.lucene.analysis.ga.IrishLowerCaseFilterFactory,
+      org.apache.lucene.analysis.gl.GalicianMinimalStemFilterFactory,
+      org.apache.lucene.analysis.gl.GalicianStemFilterFactory,
+      org.apache.lucene.analysis.hi.HindiNormalizationFilterFactory,
+      org.apache.lucene.analysis.hi.HindiStemFilterFactory,
+      org.apache.lucene.analysis.hu.HungarianLightStemFilterFactory,
+      org.apache.lucene.analysis.hunspell.HunspellStemFilterFactory,
+      org.apache.lucene.analysis.id.IndonesianStemFilterFactory,
+      org.apache.lucene.analysis.in.IndicNormalizationFilterFactory,
+      org.apache.lucene.analysis.it.ItalianLightStemFilterFactory,
+      org.apache.lucene.analysis.lv.LatvianStemFilterFactory,
+      org.apache.lucene.analysis.minhash.MinHashFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.CapitalizationFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.CodepointCountFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ConcatenateGraphFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.DateRecognizerFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.DelimitedTermFrequencyTokenFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.DropIfFlaggedFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.FingerprintFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.FixBrokenOffsetsFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.HyphenatedWordsFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.KeepWordFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.KeywordMarkerFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.KeywordRepeatFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.LengthFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.LimitTokenCountFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.LimitTokenOffsetFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.LimitTokenPositionFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.RemoveDuplicatesTokenFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.StemmerOverrideFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ProtectedTermFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.TrimFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.TruncateTokenFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.TypeAsSynonymFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.WordDelimiterFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ScandinavianFoldingFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ScandinavianNormalizationFilterFactory,
+      org.apache.lucene.analysis.ngram.EdgeNGramFilterFactory,
+      org.apache.lucene.analysis.ngram.NGramFilterFactory,
+      org.apache.lucene.analysis.no.NorwegianLightStemFilterFactory,
+      org.apache.lucene.analysis.no.NorwegianMinimalStemFilterFactory,
+      org.apache.lucene.analysis.no.NorwegianNormalizationFilterFactory,
+      org.apache.lucene.analysis.pattern.PatternReplaceFilterFactory,
+      org.apache.lucene.analysis.pattern.PatternCaptureGroupFilterFactory,
+      org.apache.lucene.analysis.pattern.PatternTypingFilterFactory,
+      org.apache.lucene.analysis.payloads.DelimitedPayloadTokenFilterFactory,
+      org.apache.lucene.analysis.payloads.NumericPayloadTokenFilterFactory,
+      org.apache.lucene.analysis.payloads.TokenOffsetPayloadTokenFilterFactory,
+      org.apache.lucene.analysis.payloads.TypeAsPayloadTokenFilterFactory,
+      org.apache.lucene.analysis.pt.PortugueseLightStemFilterFactory,
+      org.apache.lucene.analysis.pt.PortugueseMinimalStemFilterFactory,
+      org.apache.lucene.analysis.pt.PortugueseStemFilterFactory,
+      org.apache.lucene.analysis.reverse.ReverseStringFilterFactory,
+      org.apache.lucene.analysis.ru.RussianLightStemFilterFactory,
+      org.apache.lucene.analysis.shingle.ShingleFilterFactory,
+      org.apache.lucene.analysis.shingle.FixedShingleFilterFactory,
+      org.apache.lucene.analysis.snowball.SnowballPorterFilterFactory,
+      org.apache.lucene.analysis.sr.SerbianNormalizationFilterFactory,
+      org.apache.lucene.analysis.sv.SwedishLightStemFilterFactory,
+      org.apache.lucene.analysis.sv.SwedishMinimalStemFilterFactory,
+      org.apache.lucene.analysis.synonym.SynonymFilterFactory,
+      org.apache.lucene.analysis.synonym.SynonymGraphFilterFactory,
+      org.apache.lucene.analysis.core.FlattenGraphFilterFactory,
+      org.apache.lucene.analysis.te.TeluguNormalizationFilterFactory,
+      org.apache.lucene.analysis.te.TeluguStemFilterFactory,
+      org.apache.lucene.analysis.tr.TurkishLowerCaseFilterFactory,
+      org.apache.lucene.analysis.util.ElisionFilterFactory;
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.classic.ClassicTokenizerFactory,
+      org.apache.lucene.analysis.core.KeywordTokenizerFactory,
+      org.apache.lucene.analysis.core.LetterTokenizerFactory,
+      org.apache.lucene.analysis.core.WhitespaceTokenizerFactory,
+      org.apache.lucene.analysis.email.UAX29URLEmailTokenizerFactory,
+      org.apache.lucene.analysis.ngram.EdgeNGramTokenizerFactory,
+      org.apache.lucene.analysis.ngram.NGramTokenizerFactory,
+      org.apache.lucene.analysis.path.PathHierarchyTokenizerFactory,
+      org.apache.lucene.analysis.pattern.PatternTokenizerFactory,
+      org.apache.lucene.analysis.pattern.SimplePatternSplitTokenizerFactory,
+      org.apache.lucene.analysis.pattern.SimplePatternTokenizerFactory,
+      org.apache.lucene.analysis.th.ThaiTokenizerFactory,
+      org.apache.lucene.analysis.wikipedia.WikipediaTokenizerFactory;
+}
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/icu/build.gradle
index 2eab963..e76b327 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/icu/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 description = 'Analysis integration with ICU (International Components for Unicode)'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
-  api 'com.ibm.icu:icu4j'
+  moduleApi 'com.ibm.icu:icu4j'
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/src/java/module-info.java b/lucene/analysis/icu/src/java/module-info.java
new file mode 100644
index 0000000..c901af4
--- /dev/null
+++ b/lucene/analysis/icu/src/java/module-info.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+/** Analysis integration with ICU */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.icu {
+  requires com.ibm.icu;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+
+  exports org.apache.lucene.analysis.icu;
+  exports org.apache.lucene.analysis.icu.segmentation;
+  exports org.apache.lucene.analysis.icu.tokenattributes;
+
+  provides org.apache.lucene.analysis.CharFilterFactory with
+      org.apache.lucene.analysis.icu.ICUNormalizer2CharFilterFactory;
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.icu.segmentation.ICUTokenizerFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.icu.ICUFoldingFilterFactory,
+      org.apache.lucene.analysis.icu.ICUNormalizer2FilterFactory,
+      org.apache.lucene.analysis.icu.ICUTransformFilterFactory;
+}
diff --git a/lucene/analysis/kuromoji/build.gradle b/lucene/analysis/kuromoji/build.gradle
index af2bfa5..07fc0b0 100644
--- a/lucene/analysis/kuromoji/build.gradle
+++ b/lucene/analysis/kuromoji/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Japanese Morphological Analyzer'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/kuromoji/src/java/module-info.java b/lucene/analysis/kuromoji/src/java/module-info.java
new file mode 100644
index 0000000..7c829fd
--- /dev/null
+++ b/lucene/analysis/kuromoji/src/java/module-info.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+/** Japanese Morphological Analyzer */
+module org.apache.lucene.analysis.kuromoji {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+
+  exports org.apache.lucene.analysis.ja;
+  exports org.apache.lucene.analysis.ja.completion;
+  exports org.apache.lucene.analysis.ja.dict;
+  exports org.apache.lucene.analysis.ja.tokenattributes;
+  exports org.apache.lucene.analysis.ja.util;
+
+  provides org.apache.lucene.analysis.CharFilterFactory with
+      org.apache.lucene.analysis.ja.JapaneseIterationMarkCharFilterFactory;
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.ja.JapaneseTokenizerFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.ja.JapaneseBaseFormFilterFactory,
+      org.apache.lucene.analysis.ja.JapaneseCompletionFilterFactory,
+      org.apache.lucene.analysis.ja.JapaneseKatakanaStemFilterFactory,
+      org.apache.lucene.analysis.ja.JapaneseNumberFilterFactory,
+      org.apache.lucene.analysis.ja.JapanesePartOfSpeechStopFilterFactory,
+      org.apache.lucene.analysis.ja.JapaneseReadingFormFilterFactory;
+}
diff --git a/lucene/analysis/kuromoji/build.gradle b/lucene/analysis/morfologik.tests/build.gradle
similarity index 76%
copy from lucene/analysis/kuromoji/build.gradle
copy to lucene/analysis/morfologik.tests/build.gradle
index af2bfa5..9cd6720 100644
--- a/lucene/analysis/kuromoji/build.gradle
+++ b/lucene/analysis/morfologik.tests/build.gradle
@@ -17,11 +17,12 @@
 
 apply plugin: 'java-library'
 
-description = 'Japanese Morphological Analyzer'
+description = 'Module tests for :lucene:analysis:morfologik'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:analysis:morfologik')
+  moduleTestImplementation("junit:junit", {
+    exclude group: "org.hamcrest"
+  })
+  moduleTestImplementation "org.hamcrest:hamcrest"
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/morfologik.tests/src/test/module-info.java
similarity index 68%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/morfologik.tests/src/test/module-info.java
index 2eab963..15642a9 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/morfologik.tests/src/test/module-info.java
@@ -15,15 +15,13 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Test module for {@code org.apache.lucene.analysis.morfologik}. */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.morfologik.tests {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.analysis.morfologik;
+  requires junit;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.analysis.morfologik.tests;
 }
diff --git a/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java b/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java
new file mode 100644
index 0000000..c2933fc
--- /dev/null
+++ b/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java
@@ -0,0 +1,47 @@
+/*
+ * 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.analysis.morfologik.tests;
+
+import org.apache.lucene.analysis.morfologik.MorfologikAnalyzer;
+import org.apache.lucene.analysis.uk.UkrainianMorfologikAnalyzer;
+import org.apache.lucene.index.IndexWriter;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestMorfologikAnalyzer {
+  @Test
+  public void testMorfologikAnalyzerLoads() {
+    var analyzer = new MorfologikAnalyzer();
+    Assert.assertNotNull(analyzer);
+  }
+
+  @Test
+  public void testUkrainianMorfologikAnalyzerLoads() {
+    var analyzer = new UkrainianMorfologikAnalyzer();
+    Assert.assertNotNull(analyzer);
+  }
+
+  @Test
+  public void testWeAreModule() {
+    Assert.assertTrue(this.getClass().getModule().isNamed());
+  }
+
+  @Test
+  public void testLuceneIsAModule() {
+    Assert.assertTrue(IndexWriter.class.getModule().isNamed());
+  }
+}
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/analysis/morfologik/build.gradle
index 153515f..03d7c09 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/analysis/morfologik/build.gradle
@@ -20,13 +20,12 @@ apply plugin: 'java-library'
 description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
+  moduleApi 'org.carrot2:morfologik-stemming'
 
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
+  moduleImplementation 'org.carrot2:morfologik-polish'
+  moduleImplementation 'ua.net.nlp:morfologik-ukrainian-search'
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/analysis/morfologik/src/java/module-info.java
similarity index 57%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/analysis/morfologik/src/java/module-info.java
index 153515f..3268d4b 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/analysis/morfologik/src/java/module-info.java
@@ -15,18 +15,18 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Analyzer for dictionary stemming, built-in Polish dictionary */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.morfologik {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.carrot2.morfologik.stemming;
+  requires org.carrot2.morfologik.polish;
+  requires morfologik.ukrainian.search;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
+  exports org.apache.lucene.analysis.morfologik;
+  exports org.apache.lucene.analysis.uk;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.morfologik.MorfologikFilterFactory;
 }
diff --git a/lucene/analysis/morfologik/src/java/org/apache/lucene/analysis/uk/UkrainianMorfologikAnalyzer.java b/lucene/analysis/morfologik/src/java/org/apache/lucene/analysis/uk/UkrainianMorfologikAnalyzer.java
index 425a55c6..b80ccb6 100644
--- a/lucene/analysis/morfologik/src/java/org/apache/lucene/analysis/uk/UkrainianMorfologikAnalyzer.java
+++ b/lucene/analysis/morfologik/src/java/org/apache/lucene/analysis/uk/UkrainianMorfologikAnalyzer.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.io.Reader;
 import java.io.UncheckedIOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Objects;
 import morfologik.stemming.Dictionary;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.CharArraySet;
@@ -42,12 +43,9 @@ import org.apache.lucene.util.IOUtils;
  * @since 6.2.0
  */
 public final class UkrainianMorfologikAnalyzer extends StopwordAnalyzerBase {
-
+  private final Dictionary dictionary;
   private final CharArraySet stemExclusionSet;
 
-  /** File containing default Ukrainian stopwords. */
-  public static final String DEFAULT_STOPWORD_FILE = "stopwords.txt";
-
   private static final NormalizeCharMap NORMALIZER_MAP;
 
   static {
@@ -67,47 +65,72 @@ public final class UkrainianMorfologikAnalyzer extends StopwordAnalyzerBase {
     NORMALIZER_MAP = builder.build();
   }
 
-  /**
-   * Returns an unmodifiable instance of the default stop words set.
-   *
-   * @return default stop words set.
-   */
-  public static CharArraySet getDefaultStopSet() {
-    return DefaultSetHolder.DEFAULT_STOP_SET;
+  /** Returns a lazy singleton with the default Ukrainian resources. */
+  private static volatile DefaultResources defaultResources;
+
+  private static DefaultResources getDefaultResources() {
+    if (defaultResources == null) {
+      synchronized (DefaultResources.class) {
+        try {
+          CharArraySet wordList;
+          try (var is = UkrainianMorfologikAnalyzer.class.getResourceAsStream("stopwords.txt")) {
+            if (is == null) {
+              throw new IOException("Could not locate the required stopwords resource.");
+            }
+            wordList =
+                WordlistLoader.getSnowballWordSet(
+                    IOUtils.getDecodingReader(is, StandardCharsets.UTF_8));
+          }
+
+          // First, try to look up the resource module by name.
+          Dictionary dictionary;
+          Module ourModule = DefaultResources.class.getModule();
+          if (ourModule.isNamed() && ourModule.getLayer() != null) {
+            var module =
+                ourModule
+                    .getLayer()
+                    .findModule("morfologik.ukrainian.search")
+                    .orElseThrow(
+                        () ->
+                            new IOException(
+                                "Can't find the resource module: morfologik.ukrainian.search"));
+
+            try (var fsaStream = module.getResourceAsStream("ua/net/nlp/ukrainian.dict");
+                var metaStream = module.getResourceAsStream("ua/net/nlp/ukrainian.info")) {
+              dictionary = Dictionary.read(fsaStream, metaStream);
+            }
+          } else {
+            dictionary =
+                Dictionary.read(
+                    Objects.requireNonNull(
+                        UkrainianMorfologikAnalyzer.class
+                            .getClassLoader()
+                            .getResource("ua/net/nlp/ukrainian.dict"),
+                        "Could not locate the required Ukrainian dictionary resource."));
+          }
+          defaultResources = new DefaultResources(wordList, dictionary);
+        } catch (IOException e) {
+          throw new UncheckedIOException(
+              "Could not load the required resources for the Ukrainian analyzer.", e);
+        }
+      }
+    }
+    return defaultResources;
   }
 
-  /**
-   * Atomically loads the DEFAULT_STOP_SET and DICTIONARY in a lazy fashion once the outer class
-   * accesses the static final set the first time.;
-   */
-  private static class DefaultSetHolder {
-    static final CharArraySet DEFAULT_STOP_SET;
-    static final Dictionary DICTIONARY;
-
-    static {
-      try {
-        DEFAULT_STOP_SET =
-            WordlistLoader.getSnowballWordSet(
-                IOUtils.getDecodingReader(
-                    UkrainianMorfologikAnalyzer.class,
-                    DEFAULT_STOPWORD_FILE,
-                    StandardCharsets.UTF_8));
-        DICTIONARY =
-            Dictionary.read(
-                UkrainianMorfologikAnalyzer.class
-                    .getClassLoader()
-                    .getResource("ua/net/nlp/ukrainian.dict"));
-      } catch (IOException ex) {
-        // default set should always be present as it is part of the
-        // distribution (JAR)
-        throw new UncheckedIOException("Unable to load analyzer resources", ex);
-      }
+  private static class DefaultResources {
+    final CharArraySet stopSet;
+    final Dictionary dictionary;
+
+    private DefaultResources(CharArraySet stopSet, Dictionary dictionary) {
+      this.stopSet = stopSet;
+      this.dictionary = dictionary;
     }
   }
 
-  /** Builds an analyzer with the default stop words: {@link #DEFAULT_STOPWORD_FILE}. */
+  /** Builds an analyzer with the default stop words. */
   public UkrainianMorfologikAnalyzer() {
-    this(DefaultSetHolder.DEFAULT_STOP_SET);
+    this(getDefaultResources().stopSet);
   }
 
   /**
@@ -129,6 +152,7 @@ public final class UkrainianMorfologikAnalyzer extends StopwordAnalyzerBase {
   public UkrainianMorfologikAnalyzer(CharArraySet stopwords, CharArraySet stemExclusionSet) {
     super(stopwords);
     this.stemExclusionSet = CharArraySet.unmodifiableSet(CharArraySet.copy(stemExclusionSet));
+    this.dictionary = getDefaultResources().dictionary;
   }
 
   @Override
@@ -155,7 +179,7 @@ public final class UkrainianMorfologikAnalyzer extends StopwordAnalyzerBase {
       result = new SetKeywordMarkerFilter(result, stemExclusionSet);
     }
 
-    result = new MorfologikFilter(result, DefaultSetHolder.DICTIONARY);
+    result = new MorfologikFilter(result, dictionary);
     return new TokenStreamComponents(source, result);
   }
 }
diff --git a/lucene/analysis/nori/build.gradle b/lucene/analysis/nori/build.gradle
index 430673c..079cf50 100644
--- a/lucene/analysis/nori/build.gradle
+++ b/lucene/analysis/nori/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Korean Morphological Analyzer'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
   
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/nori/src/java/module-info.java
similarity index 54%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/nori/src/java/module-info.java
index 2eab963..9dd085b 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/nori/src/java/module-info.java
@@ -15,15 +15,19 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Korean Morphological Analyzer */
+module org.apache.lucene.analysis.nori {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
+  exports org.apache.lucene.analysis.ko;
+  exports org.apache.lucene.analysis.ko.dict;
+  exports org.apache.lucene.analysis.ko.tokenattributes;
+  exports org.apache.lucene.analysis.ko.util;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.ko.KoreanTokenizerFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.ko.KoreanPartOfSpeechStopFilterFactory,
+      org.apache.lucene.analysis.ko.KoreanReadingFormFilterFactory;
 }
diff --git a/lucene/analysis/opennlp/build.gradle b/lucene/analysis/opennlp/build.gradle
index c4672c0..3fee61a 100644
--- a/lucene/analysis/opennlp/build.gradle
+++ b/lucene/analysis/opennlp/build.gradle
@@ -20,9 +20,9 @@ apply plugin: 'java-library'
 description = 'OpenNLP Library Integration'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-  api 'org.apache.opennlp:opennlp-tools'
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
+  moduleApi 'org.apache.opennlp:opennlp-tools'
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/opennlp/src/java/module-info.java
similarity index 52%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/opennlp/src/java/module-info.java
index 2eab963..c63ecfa 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/opennlp/src/java/module-info.java
@@ -15,15 +15,20 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** OpenNLP Library Integration */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.opennlp {
+  requires org.apache.opennlp.tools;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
+  exports org.apache.lucene.analysis.opennlp;
+  exports org.apache.lucene.analysis.opennlp.tools;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.opennlp.OpenNLPTokenizerFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.opennlp.OpenNLPChunkerFilterFactory,
+      org.apache.lucene.analysis.opennlp.OpenNLPLemmatizerFilterFactory,
+      org.apache.lucene.analysis.opennlp.OpenNLPPOSFilterFactory;
 }
diff --git a/lucene/analysis/phonetic/build.gradle b/lucene/analysis/phonetic/build.gradle
index 0f716e8..e5595cb 100644
--- a/lucene/analysis/phonetic/build.gradle
+++ b/lucene/analysis/phonetic/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 description = 'Analyzer for indexing phonetic signatures (for sounds-alike search)'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
-  implementation 'commons-codec:commons-codec'
+  moduleImplementation 'commons-codec:commons-codec'
 
   testImplementation project(':lucene:test-framework')
 } 
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/analysis/phonetic/src/java/module-info.java
similarity index 58%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/analysis/phonetic/src/java/module-info.java
index 153515f..706251a 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/analysis/phonetic/src/java/module-info.java
@@ -15,18 +15,17 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Analyzer for indexing phonetic signatures */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.phonetic {
+  requires org.apache.commons.codec;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
+  exports org.apache.lucene.analysis.phonetic;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.phonetic.BeiderMorseFilterFactory,
+      org.apache.lucene.analysis.phonetic.DoubleMetaphoneFilterFactory,
+      org.apache.lucene.analysis.phonetic.PhoneticFilterFactory;
 }
diff --git a/lucene/analysis/smartcn/build.gradle b/lucene/analysis/smartcn/build.gradle
index 8d5eeb1..960be70 100644
--- a/lucene/analysis/smartcn/build.gradle
+++ b/lucene/analysis/smartcn/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Analyzer for indexing Chinese'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
   testImplementation project(':lucene:test-framework')
 } 
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/smartcn/src/java/module-info.java
similarity index 67%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/smartcn/src/java/module-info.java
index 2eab963..44f4eb5 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/smartcn/src/java/module-info.java
@@ -15,15 +15,14 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Analyzer for indexing Chinese */
+module org.apache.lucene.analysis.smartcn {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
+  exports org.apache.lucene.analysis.cn.smart;
+  exports org.apache.lucene.analysis.cn.smart.hhmm;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.cn.smart.HMMChineseTokenizerFactory;
 }
diff --git a/lucene/analysis/stempel/build.gradle b/lucene/analysis/stempel/build.gradle
index 3b80f9f..3299159 100644
--- a/lucene/analysis/stempel/build.gradle
+++ b/lucene/analysis/stempel/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Analyzer for indexing Polish'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/stempel/src/java/module-info.java
similarity index 66%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/stempel/src/java/module-info.java
index 2eab963..d559548 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/stempel/src/java/module-info.java
@@ -15,15 +15,15 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Analyzer for indexing Polish */
+module org.apache.lucene.analysis.stempel {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
+  exports org.apache.lucene.analysis.pl;
+  exports org.apache.lucene.analysis.stempel;
+  exports org.egothor.stemmer;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.stempel.StempelPolishStemFilterFactory;
 }
diff --git a/lucene/backward-codecs/build.gradle b/lucene/backward-codecs/build.gradle
index aa47302..d5e25d9 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/backward-codecs/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 
 description = 'Codecs for older versions of Lucene'
 
-dependencies { 
-  api project(':lucene:core')
+dependencies {
+  moduleApi project(':lucene:core')
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/backward-codecs/src/java/module-info.java b/lucene/backward-codecs/src/java/module-info.java
new file mode 100644
index 0000000..8e01546
--- /dev/null
+++ b/lucene/backward-codecs/src/java/module-info.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+/** Codecs for older versions of Lucene */
+module org.apache.lucene.backward_codecs {
+  requires org.apache.lucene.core;
+
+  exports org.apache.lucene.backward_codecs;
+  exports org.apache.lucene.backward_codecs.lucene40.blocktree;
+  exports org.apache.lucene.backward_codecs.lucene50;
+  exports org.apache.lucene.backward_codecs.lucene50.compressing;
+  exports org.apache.lucene.backward_codecs.lucene60;
+  exports org.apache.lucene.backward_codecs.lucene70;
+  exports org.apache.lucene.backward_codecs.lucene80;
+  exports org.apache.lucene.backward_codecs.lucene84;
+  exports org.apache.lucene.backward_codecs.lucene86;
+  exports org.apache.lucene.backward_codecs.lucene87;
+  exports org.apache.lucene.backward_codecs.packed;
+  exports org.apache.lucene.backward_codecs.store;
+
+  provides org.apache.lucene.codecs.DocValuesFormat with
+      org.apache.lucene.backward_codecs.lucene70.Lucene70DocValuesFormat,
+      org.apache.lucene.backward_codecs.lucene80.Lucene80DocValuesFormat;
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.backward_codecs.lucene50.Lucene50PostingsFormat,
+      org.apache.lucene.backward_codecs.lucene84.Lucene84PostingsFormat;
+  provides org.apache.lucene.codecs.Codec with
+      org.apache.lucene.backward_codecs.lucene70.Lucene70Codec,
+      org.apache.lucene.backward_codecs.lucene80.Lucene80Codec,
+      org.apache.lucene.backward_codecs.lucene84.Lucene84Codec,
+      org.apache.lucene.backward_codecs.lucene86.Lucene86Codec,
+      org.apache.lucene.backward_codecs.lucene87.Lucene87Codec;
+}
diff --git a/lucene/benchmark/build.gradle b/lucene/benchmark/build.gradle
index 67b1644..9da51c3 100644
--- a/lucene/benchmark/build.gradle
+++ b/lucene/benchmark/build.gradle
@@ -22,23 +22,23 @@ plugins {
 description = 'Lucene benchmarking module'
 
 dependencies {  
-  implementation project(':lucene:core')
+  moduleImplementation project(':lucene:core')
 
-  implementation project(':lucene:analysis:common')
-  implementation project(':lucene:facet')
-  implementation project(':lucene:highlighter')
-  implementation project(':lucene:queries')
-  implementation project(':lucene:spatial-extras')
-  implementation project(':lucene:queryparser')
+  moduleImplementation project(':lucene:analysis:common')
+  moduleImplementation project(':lucene:facet')
+  moduleImplementation project(':lucene:highlighter')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:spatial-extras')
+  moduleImplementation project(':lucene:queryparser')
 
-  implementation "org.apache.commons:commons-compress"
-  implementation "com.ibm.icu:icu4j"
-  implementation "org.locationtech.spatial4j:spatial4j"
-  implementation("net.sourceforge.nekohtml:nekohtml", {
+  moduleImplementation "org.apache.commons:commons-compress"
+  moduleImplementation "com.ibm.icu:icu4j"
+  moduleImplementation "org.locationtech.spatial4j:spatial4j"
+  moduleImplementation ("net.sourceforge.nekohtml:nekohtml", {
     exclude module: "xml-apis"
   })
 
-  runtimeOnly project(':lucene:analysis:icu')
+  moduleRuntimeOnly project(':lucene:analysis:icu')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/benchmark/src/java/module-info.java b/lucene/benchmark/src/java/module-info.java
new file mode 100644
index 0000000..f3dfac9
--- /dev/null
+++ b/lucene/benchmark/src/java/module-info.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.benchmark {
+  requires java.xml;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.facet;
+  requires org.apache.lucene.highlighter;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.queryparser;
+  requires org.apache.lucene.spatial_extras;
+  requires spatial4j;
+
+  exports org.apache.lucene.benchmark;
+  exports org.apache.lucene.benchmark.byTask;
+  exports org.apache.lucene.benchmark.byTask.feeds;
+  exports org.apache.lucene.benchmark.byTask.programmatic;
+  exports org.apache.lucene.benchmark.byTask.stats;
+  exports org.apache.lucene.benchmark.byTask.tasks;
+  exports org.apache.lucene.benchmark.byTask.utils;
+  exports org.apache.lucene.benchmark.quality;
+  exports org.apache.lucene.benchmark.quality.trec;
+  exports org.apache.lucene.benchmark.quality.utils;
+  exports org.apache.lucene.benchmark.utils;
+}
diff --git a/lucene/classification/build.gradle b/lucene/classification/build.gradle
index 736dfb3..8566cdb 100644
--- a/lucene/classification/build.gradle
+++ b/lucene/classification/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 description = 'Classification module for Lucene'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:queries')
-  implementation project(':lucene:grouping')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:grouping')
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:analysis:common')
diff --git a/lucene/analysis/icu/build.gradle b/lucene/classification/src/java/module-info.java
similarity index 70%
copy from lucene/analysis/icu/build.gradle
copy to lucene/classification/src/java/module-info.java
index 2eab963..4817d8a 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/classification/src/java/module-info.java
@@ -15,15 +15,13 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Classification module for Lucene */
+module org.apache.lucene.classification {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.grouping;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.classification;
+  exports org.apache.lucene.classification.document;
+  exports org.apache.lucene.classification.utils;
 }
diff --git a/lucene/codecs/build.gradle b/lucene/codecs/build.gradle
index ad26aae..92e0782 100644
--- a/lucene/codecs/build.gradle
+++ b/lucene/codecs/build.gradle
@@ -20,6 +20,6 @@ apply plugin: 'java-library'
 description = 'Lucene codecs and postings formats'
 
 dependencies {
-    implementation project(':lucene:core')
+    moduleImplementation project(':lucene:core')
     testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/codecs/src/java/module-info.java b/lucene/codecs/src/java/module-info.java
new file mode 100644
index 0000000..73f53fb
--- /dev/null
+++ b/lucene/codecs/src/java/module-info.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+/** Lucene codecs and postings formats */
+module org.apache.lucene.codecs {
+  requires org.apache.lucene.core;
+
+  exports org.apache.lucene.codecs.blockterms;
+  exports org.apache.lucene.codecs.blocktreeords;
+  exports org.apache.lucene.codecs.bloom;
+  exports org.apache.lucene.codecs.memory;
+  exports org.apache.lucene.codecs.simpletext;
+  exports org.apache.lucene.codecs.uniformsplit;
+  exports org.apache.lucene.codecs.uniformsplit.sharedterms;
+
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.codecs.blocktreeords.BlockTreeOrdsPostingsFormat,
+      org.apache.lucene.codecs.bloom.BloomFilteringPostingsFormat,
+      org.apache.lucene.codecs.memory.DirectPostingsFormat,
+      org.apache.lucene.codecs.memory.FSTPostingsFormat,
+      org.apache.lucene.codecs.uniformsplit.UniformSplitPostingsFormat,
+      org.apache.lucene.codecs.uniformsplit.sharedterms.STUniformSplitPostingsFormat;
+  provides org.apache.lucene.codecs.Codec with
+      org.apache.lucene.codecs.simpletext.SimpleTextCodec;
+}
diff --git a/lucene/core/src/java/module-info.java b/lucene/core/src/java/module-info.java
new file mode 100644
index 0000000..931d4cb
--- /dev/null
+++ b/lucene/core/src/java/module-info.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+/** Lucene Core. */
+module org.apache.lucene.core {
+  requires jdk.unsupported; // this is needed for MMapDirectory to unmap
+
+  exports org.apache.lucene.analysis;
+  exports org.apache.lucene.analysis.standard;
+  exports org.apache.lucene.analysis.tokenattributes;
+  exports org.apache.lucene.codecs;
+  exports org.apache.lucene.codecs.compressing;
+  exports org.apache.lucene.codecs.lucene90;
+  exports org.apache.lucene.codecs.lucene90.blocktree;
+  exports org.apache.lucene.codecs.lucene90.compressing;
+  exports org.apache.lucene.codecs.perfield;
+  exports org.apache.lucene.document;
+  exports org.apache.lucene.geo;
+  exports org.apache.lucene.index;
+  exports org.apache.lucene.search;
+  exports org.apache.lucene.search.comparators;
+  exports org.apache.lucene.search.similarities;
+  exports org.apache.lucene.store;
+  exports org.apache.lucene.util;
+  exports org.apache.lucene.util.automaton;
+  exports org.apache.lucene.util.bkd;
+  exports org.apache.lucene.util.compress;
+  exports org.apache.lucene.util.fst;
+  exports org.apache.lucene.util.graph;
+  exports org.apache.lucene.util.hnsw;
+  exports org.apache.lucene.util.hppc;
+  exports org.apache.lucene.util.mutable;
+  exports org.apache.lucene.util.packed;
+
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.standard.StandardTokenizerFactory;
+  provides org.apache.lucene.codecs.Codec with
+      org.apache.lucene.codecs.lucene90.Lucene90Codec;
+  provides org.apache.lucene.codecs.DocValuesFormat with
+      org.apache.lucene.codecs.lucene90.Lucene90DocValuesFormat;
+  provides org.apache.lucene.codecs.KnnVectorsFormat with
+      org.apache.lucene.codecs.lucene90.Lucene90HnswVectorsFormat;
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.codecs.lucene90.Lucene90PostingsFormat;
+  provides org.apache.lucene.index.SortFieldProvider with
+      org.apache.lucene.search.SortField.Provider,
+      org.apache.lucene.search.SortedNumericSortField.Provider,
+      org.apache.lucene.search.SortedSetSortField.Provider;
+
+  uses org.apache.lucene.analysis.CharFilterFactory;
+  uses org.apache.lucene.analysis.TokenFilterFactory;
+  uses org.apache.lucene.analysis.TokenizerFactory;
+  uses org.apache.lucene.codecs.Codec;
+  uses org.apache.lucene.codecs.DocValuesFormat;
+  uses org.apache.lucene.codecs.KnnVectorsFormat;
+  uses org.apache.lucene.codecs.PostingsFormat;
+  uses org.apache.lucene.index.SortFieldProvider;
+}
diff --git a/lucene/demo/build.gradle b/lucene/demo/build.gradle
index 3624328..70fd1df 100644
--- a/lucene/demo/build.gradle
+++ b/lucene/demo/build.gradle
@@ -20,12 +20,12 @@ apply plugin: 'java-library'
 description = 'Simple example code for Apache Lucene'
 
 dependencies {
-  implementation project(':lucene:core')
-  implementation project(':lucene:facet')
-  implementation project(':lucene:queries')
-  implementation project(':lucene:analysis:common')
-  implementation project(':lucene:queryparser')
-  implementation project(':lucene:expressions')
+  moduleImplementation project(':lucene:core')
+  moduleImplementation project(':lucene:facet')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:analysis:common')
+  moduleImplementation project(':lucene:queryparser')
+  moduleImplementation project(':lucene:expressions')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/demo/src/java/module-info.java
similarity index 65%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/demo/src/java/module-info.java
index 153515f..13549a5 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/demo/src/java/module-info.java
@@ -15,18 +15,16 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Simple example code for Apache Lucene */
+module org.apache.lucene.demo {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.facet;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.queryparser;
+  requires org.apache.lucene.expressions;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.demo;
+  exports org.apache.lucene.demo.facet;
+  exports org.apache.lucene.demo.knn;
 }
diff --git a/lucene/distribution.tests/build.gradle b/lucene/distribution.tests/build.gradle
new file mode 100644
index 0000000..30c4ae1
--- /dev/null
+++ b/lucene/distribution.tests/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+plugins {
+  id 'java-library'
+}
+
+configurations {
+  // This is a configuration that references an 'folder-expanded' binary distribution
+  // tests will run against. The distribution is slightly trimmed (no docs, licenses)
+  // because we don't test these parts of the distribution anyway.
+  binaryDistribution
+}
+
+dependencies {
+  binaryDistribution project(path: ":lucene:distribution", configuration: "binaryDirForTests")
+
+  moduleTestImplementation("com.carrotsearch.randomizedtesting:randomizedtesting-runner", {
+    exclude group: "junit"
+  })
+
+  moduleTestImplementation("junit:junit", {
+    exclude group: "org.hamcrest"
+  })
+  moduleTestImplementation "org.hamcrest:hamcrest"
+  moduleTestImplementation "org.assertj:assertj-core"
+}
+
+test {
+  dependsOn configurations.binaryDistribution
+
+  // We need to pass the system property using a lazy provider, not supported at the moment:
+  // https://github.com/gradle/gradle/issues/12247
+  // so we'll use a workaround and pass command-line arguments directly.
+  jvmArgumentProviders.add(new CommandLineArgumentProvider() {
+    @Override
+    Iterable<String> asArguments() {
+      return [
+          "-Dlucene.distribution.dir=${configurations.binaryDistribution.singleFile.absolutePath }",
+          "-Dlucene.distribution.version=${project.version}"
+      ]
+    }
+  })
+
+  doFirst {
+    logger.lifecycle("Testing binary distribution at: ${configurations.binaryDistribution.singleFile}")
+  }
+}
\ No newline at end of file
diff --git a/lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java b/lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java
new file mode 100644
index 0000000..5627089
--- /dev/null
+++ b/lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java
@@ -0,0 +1,295 @@
+/*
+ * 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.distribution;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReader;
+import java.lang.module.ModuleReference;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.Assumptions;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Sanity checks concerning the distribution's binary artifacts (modules).
+ *
+ * <p>We do <em>not</em> want this module to depend on any Lucene classes (including the test
+ * framework) so that there is no risk of accidental classpath space pollution. This also means the
+ * default {@code LuceneTestCase} configuration setup is not used (you have to annotate test for
+ * JUnit, for example).
+ */
+public class TestModularLayer {
+  /** A path to a directory with an expanded Lucene distribution. */
+  private static final String DISTRIBUTION_PROPERTY = "lucene.distribution.dir";
+
+  /** The expected distribution version of Lucene modules. */
+  private static final String VERSION_PROPERTY = "lucene.distribution.version";
+
+  /** Only core Lucene modules, no third party modules. */
+  private static Set<ModuleReference> allCoreModules;
+
+  /** {@link ModuleFinder} resolving only the Lucene modules. */
+  private static ModuleFinder coreModulesFinder;
+
+  /** Ensure Lucene classes are not directly visible. */
+  @BeforeClass
+  public static void checkLuceneNotInClasspath() {
+    Assertions.assertThatThrownBy(
+            () -> {
+              Class.forName("org.apache.lucene.index.IndexWriter");
+            })
+        .isInstanceOf(ClassNotFoundException.class);
+  }
+
+  /**
+   * We accept external properties that point to the assembled set of distribution modules and to
+   * their expected version. These properties are collected and passed by gradle but can be provided
+   * manually (for IDE launches).
+   */
+  @BeforeClass
+  public static void checkModulePathProvided() {
+    String modulesPropertyValue = System.getProperty(DISTRIBUTION_PROPERTY);
+    if (modulesPropertyValue == null) {
+      throw new AssertionError(DISTRIBUTION_PROPERTY + " property is required for this test.");
+    }
+
+    Path modulesPath = Paths.get(modulesPropertyValue).resolve("modules");
+    if (!Files.isDirectory(modulesPath)) {
+      throw new AssertionError(
+          DISTRIBUTION_PROPERTY
+              + " property does not point to a directory where this path is present: "
+              + modulesPath.toAbsolutePath());
+    }
+
+    Path thirdPartyModulesPath = Paths.get(modulesPropertyValue).resolve("modules-thirdparty");
+    if (!Files.isDirectory(thirdPartyModulesPath)) {
+      throw new AssertionError(
+          DISTRIBUTION_PROPERTY
+              + " property does not point to a directory where this path is present: "
+              + thirdPartyModulesPath.toAbsolutePath());
+    }
+
+    coreModulesFinder = ModuleFinder.of(modulesPath);
+    allCoreModules = coreModulesFinder.findAll();
+  }
+
+  @AfterClass
+  public static void cleanup() {
+    allCoreModules = null;
+    coreModulesFinder = null;
+  }
+
+  /** Make sure all published module names remain constant, even if we reorganize the build. */
+  @Test
+  public void testExpectedDistributionModuleNames() {
+    Assertions.assertThat(
+            allCoreModules.stream().map(module -> module.descriptor().name()).sorted())
+        .containsExactly(
+            "org.apache.lucene.analysis.common",
+            "org.apache.lucene.analysis.icu",
+            "org.apache.lucene.analysis.kuromoji",
+            "org.apache.lucene.analysis.morfologik",
+            "org.apache.lucene.analysis.nori",
+            "org.apache.lucene.analysis.opennlp",
+            "org.apache.lucene.analysis.phonetic",
+            "org.apache.lucene.analysis.smartcn",
+            "org.apache.lucene.analysis.stempel",
+            "org.apache.lucene.backward_codecs",
+            "org.apache.lucene.benchmark",
+            "org.apache.lucene.classification",
+            "org.apache.lucene.codecs",
+            "org.apache.lucene.core",
+            "org.apache.lucene.demo",
+            "org.apache.lucene.expressions",
+            "org.apache.lucene.facet",
+            "org.apache.lucene.grouping",
+            "org.apache.lucene.highlighter",
+            "org.apache.lucene.join",
+            "org.apache.lucene.luke",
+            "org.apache.lucene.memory",
+            "org.apache.lucene.misc",
+            "org.apache.lucene.monitor",
+            "org.apache.lucene.queries",
+            "org.apache.lucene.queryparser",
+            "org.apache.lucene.replicator",
+            "org.apache.lucene.sandbox",
+            "org.apache.lucene.spatial3d",
+            "org.apache.lucene.spatial_extras",
+            "org.apache.lucene.suggest");
+  }
+
+  /** Make sure we don't publish automatic modules. */
+  @Test
+  public void testAllCoreModulesAreNamedModules() {
+    Assertions.assertThat(allCoreModules)
+        .allSatisfy(
+            module -> {
+              Assertions.assertThat(module.descriptor().isAutomatic())
+                  .as(module.descriptor().name())
+                  .isFalse();
+            });
+  }
+
+  /** Ensure all modules have the same (expected) version. */
+  @Test
+  public void testAllModulesHaveExpectedVersion() {
+    String luceneBuildVersion = System.getProperty(VERSION_PROPERTY);
+    Assumptions.assumeThat(luceneBuildVersion).isNotNull();
+    for (var module : allCoreModules) {
+      Assertions.assertThat(module.descriptor().rawVersion().orElse(null))
+          .as("Version of module: " + module.descriptor().name())
+          .isEqualTo(luceneBuildVersion);
+    }
+  }
+
+  /** Ensure SPIs are equal for the module and classpath layer. */
+  @Test
+  public void testModularAndClasspathProvidersAreConsistent() throws IOException {
+    for (var module : allCoreModules) {
+      TreeMap<String, TreeSet<String>> modularProviders = getModularServiceProviders(module);
+      TreeMap<String, TreeSet<String>> classpathProviders = getClasspathServiceProviders(module);
+
+      // Compare services first so that the exception is shorter.
+      Assertions.assertThat(modularProviders.keySet())
+          .as("Modular services in module: " + module.descriptor().name())
+          .containsExactlyInAnyOrderElementsOf(classpathProviders.keySet());
+
+      // We're sure the services correspond to each other. Now, for each service, compare the
+      // providers.
+      for (var service : modularProviders.keySet()) {
+        Assertions.assertThat(modularProviders.get(service))
+            .as(
+                "Modular providers of service "
+                    + service
+                    + " in module: "
+                    + module.descriptor().name())
+            .containsExactlyInAnyOrderElementsOf(classpathProviders.get(service));
+      }
+    }
+  }
+
+  private TreeMap<String, TreeSet<String>> getClasspathServiceProviders(ModuleReference module)
+      throws IOException {
+    TreeMap<String, TreeSet<String>> services = new TreeMap<>();
+    Pattern serviceEntryPattern = Pattern.compile("META-INF/services/(?<serviceName>.+)");
+    try (ModuleReader reader = module.open();
+        Stream<String> entryStream = reader.list()) {
+      List<String> serviceProviderEntryList =
+          entryStream
+              .filter(entry -> serviceEntryPattern.matcher(entry).find())
+              .collect(Collectors.toList());
+
+      for (String entry : serviceProviderEntryList) {
+        List<String> implementations;
+        try (InputStream is = reader.open(entry).get()) {
+          implementations =
+              Arrays.stream(new String(is.readAllBytes(), StandardCharsets.UTF_8).split("\n"))
+                  .map(String::trim)
+                  .filter(line -> !line.isBlank() && !line.startsWith("#"))
+                  .collect(Collectors.toList());
+        }
+
+        Matcher matcher = serviceEntryPattern.matcher(entry);
+        if (!matcher.find()) {
+          throw new AssertionError("Impossible.");
+        }
+        String service = matcher.group("serviceName");
+        services.computeIfAbsent(service, k -> new TreeSet<>()).addAll(implementations);
+      }
+    }
+
+    return services;
+  }
+
+  private static TreeMap<String, TreeSet<String>> getModularServiceProviders(
+      ModuleReference module) {
+    return module.descriptor().provides().stream()
+        .collect(
+            Collectors.toMap(
+                ModuleDescriptor.Provides::service,
+                provides -> new TreeSet<>(provides.providers()),
+                (k, v) -> {
+                  throw new RuntimeException();
+                },
+                TreeMap::new));
+  }
+
+  /**
+   * Ensure all exported packages in the descriptor are in sync with the module's Java classes.
+   *
+   * <p>This test should be progressively tuned so that certain internal packages are hidden in the
+   * module layer.
+   */
+  @Test
+  public void testAllOpenPackagesInSync() throws IOException {
+    for (var module : allCoreModules) {
+      Set<String> jarPackages = getJarPackages(module);
+
+      if (module.descriptor().name().equals("org.apache.lucene.luke")) {
+        jarPackages.removeIf(
+            entry -> {
+              // Luke's packages are not exported.
+              return entry.startsWith("org.apache.lucene.luke");
+            });
+      }
+
+      Set<ModuleDescriptor.Exports> moduleExports = module.descriptor().exports();
+      Assertions.assertThat(moduleExports)
+          .as("Exported packages in module: " + module.descriptor().name())
+          .allSatisfy(
+              export -> {
+                Assertions.assertThat(export.targets())
+                    .as("We only support unqualified exports for now?")
+                    .isEmpty();
+              })
+          .map(ModuleDescriptor.Exports::source)
+          .containsExactlyInAnyOrderElementsOf(jarPackages);
+    }
+  }
+
+  private Set<String> getJarPackages(ModuleReference module) throws IOException {
+    try (ModuleReader reader = module.open()) {
+      return reader
+          .list()
+          .filter(
+              entry ->
+                  !entry.startsWith("META-INF/")
+                      && !entry.equals("module-info.class")
+                      && !entry.endsWith("/"))
+          .map(entry -> entry.replaceAll("/[^/]+$", ""))
+          .map(entry -> entry.replace('/', '.'))
+          .collect(Collectors.toCollection(TreeSet::new));
+    }
+  }
+}
diff --git a/lucene/distribution/binary-release.gradle b/lucene/distribution/binary-release.gradle
index c06bb1a..be2ae60 100644
--- a/lucene/distribution/binary-release.gradle
+++ b/lucene/distribution/binary-release.gradle
@@ -28,6 +28,8 @@ configure(project(":lucene:distribution")) {
     jars
     jarsTestFramework
     jarsThirdParty
+
+    binaryDirForTests
   }
 
   dependencies { DependencyHandler handler ->
@@ -63,7 +65,6 @@ configure(project(":lucene:distribution")) {
     }
   }
 
-
   task assembleBinaryTgz(type: Tar) {
     description "Assemble binary Lucene artifact as a .tgz file."
 
@@ -73,6 +74,26 @@ configure(project(":lucene:distribution")) {
     reproducibleFileOrder = true
     compression = Compression.GZIP
 
+    // Internal archive folder for all files.
+    into "lucene-${rootProject.version}/"
+  }
+
+  task assembleBinaryDirForTests(type: Sync) {
+    description "Assemble a subset of the binary Lucene distribution as an expanded directory for tests."
+
+    destinationDir file("${packageBaseName}-itests")
+  }
+
+  artifacts {
+    binaryDirForTests tasks.assembleBinaryDirForTests.destinationDir, {
+      builtBy tasks.assembleBinaryDirForTests
+    }
+  }
+
+  // Configure distribution content for archives and stand-alone directories.
+  // This is split into binaries and other artifacts to speed up distribution
+  // tests.
+  Closure<Void> distributionBinaryContent = { AbstractCopyTask task ->
     // Manually correct posix permissions (matters when assembling archives on Windows).
     filesMatching(["**/*.sh", "**/*.bat"]) { copy ->
       copy.setMode(0755)
@@ -83,6 +104,21 @@ configure(project(":lucene:distribution")) {
       filteringCharset = 'UTF-8'
     })
 
+    // Binary modules (Lucene).
+    from(configurations.jars, {
+      into 'modules'
+    })
+    from(configurations.jarsTestFramework, {
+      into 'modules-test-framework'
+    })
+
+    // Binary modules (with dependencies). Don't duplicate project artifacts.
+    from((configurations.jarsThirdParty - configurations.jars), {
+      into 'modules-thirdparty'
+    })
+  }
+
+  Closure<Void> distributionOtherContent = { AbstractCopyTask task ->
     // Cherry-pick certain files from the root.
     from(project(':').projectDir, {
       include "LICENSE.txt"
@@ -102,21 +138,14 @@ configure(project(":lucene:distribution")) {
     from(configurations.docs, {
       into 'docs'
     })
+  }
 
-    // Binary modules (Lucene).
-    from(configurations.jars, {
-      into 'modules'
-    })
-    from(configurations.jarsTestFramework, {
-      into 'modules-test-framework'
-    })
-
-    // Binary modules (with dependencies). Don't duplicate project artifacts.
-    from((configurations.jarsThirdParty - configurations.jars), {
-      into 'modules-thirdparty'
-    })
+  [tasks.assembleBinaryTgz].each { Task task ->
+    task.configure distributionBinaryContent
+    task.configure distributionOtherContent
+  }
 
-    // Internal archive folder for all files.
-    into "lucene-${rootProject.version}/"
+  [tasks.assembleBinaryDirForTests].each { Task task ->
+    task.configure distributionBinaryContent
   }
 }
diff --git a/lucene/distribution/src/binary-release/bin/luke.cmd b/lucene/distribution/src/binary-release/bin/luke.cmd
index b4591ae..ee5d06c 100644
--- a/lucene/distribution/src/binary-release/bin/luke.cmd
+++ b/lucene/distribution/src/binary-release/bin/luke.cmd
@@ -17,5 +17,5 @@
 
 SETLOCAL
 SET MODULES=%~dp0..
-start javaw --module-path "%MODULES%\modules;%MODULES%\modules-thirdparty" --add-modules jdk.unsupported --module org.apache.lucene.luke
+start javaw --module-path "%MODULES%\modules;%MODULES%\modules-thirdparty" --module org.apache.lucene.luke
 ENDLOCAL
diff --git a/lucene/distribution/src/binary-release/bin/luke.sh b/lucene/distribution/src/binary-release/bin/luke.sh
index 053edda..b934f4e 100644
--- a/lucene/distribution/src/binary-release/bin/luke.sh
+++ b/lucene/distribution/src/binary-release/bin/luke.sh
@@ -17,4 +17,4 @@
 
 MODULES=`dirname "$0"`/..
 MODULES=`cd "$MODULES" && pwd`
-java --module-path "$MODULES/modules:$MODULES/modules-thirdparty" --add-modules jdk.unsupported --module org.apache.lucene.luke
+java --module-path "$MODULES/modules:$MODULES/modules-thirdparty" --module org.apache.lucene.luke
diff --git a/lucene/expressions/build.gradle b/lucene/expressions/build.gradle
index 7ba76a7..6823ae6 100644
--- a/lucene/expressions/build.gradle
+++ b/lucene/expressions/build.gradle
@@ -20,21 +20,14 @@ apply plugin: 'java-library'
 description = 'Dynamically computed values to sort/facet/search on based on a pluggable grammar'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:codecs')
+  moduleImplementation project(':lucene:codecs')
 
-  implementation 'org.antlr:antlr4-runtime'
+  moduleImplementation 'org.antlr:antlr4-runtime'
 
-  // It is awkward that we force-omit the intermediate dependency here...
-  // The dependency chain is:
-  //   asm-commons -> asm-tree -> asm
-  // Should we really go through these hoops?
-  implementation 'org.ow2.asm:asm'
-  implementation('org.ow2.asm:asm-commons', {
-    exclude group: "org.ow2.asm", module: "asm-tree"
-    exclude group: "org.ow2.asm", module: "asm-analysis"
-  })
+  moduleImplementation 'org.ow2.asm:asm'
+  moduleImplementation 'org.ow2.asm:asm-commons'
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/expressions/src/java/module-info.java
similarity index 70%
copy from lucene/analysis/icu/build.gradle
copy to lucene/expressions/src/java/module-info.java
index 2eab963..108f453 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/expressions/src/java/module-info.java
@@ -15,15 +15,14 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.expressions {
+  requires org.objectweb.asm;
+  requires org.objectweb.asm.commons;
+  requires antlr4.runtime;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.codecs;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.expressions;
+  exports org.apache.lucene.expressions.js;
 }
diff --git a/lucene/facet/build.gradle b/lucene/facet/build.gradle
index b4f3ac5..1b16f94 100644
--- a/lucene/facet/build.gradle
+++ b/lucene/facet/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 
 description = 'Faceted indexing and search capabilities'
 
-dependencies { 
-  api project(':lucene:core')
+dependencies {
+  moduleApi project(':lucene:core')
 
-  implementation 'com.carrotsearch:hppc'
+  moduleImplementation 'com.carrotsearch:hppc'
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:queries')
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/facet/src/java/module-info.java
similarity index 62%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/facet/src/java/module-info.java
index 153515f..26859cc 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/facet/src/java/module-info.java
@@ -15,18 +15,17 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Faceted indexing and search capabilities */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.facet {
+  requires java.logging;
+  requires com.carrotsearch.hppc;
+  requires org.apache.lucene.core;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.facet;
+  exports org.apache.lucene.facet.range;
+  exports org.apache.lucene.facet.sortedset;
+  exports org.apache.lucene.facet.taxonomy;
+  exports org.apache.lucene.facet.taxonomy.directory;
+  exports org.apache.lucene.facet.taxonomy.writercache;
 }
diff --git a/lucene/grouping/build.gradle b/lucene/grouping/build.gradle
index 6e71964..7035a11 100644
--- a/lucene/grouping/build.gradle
+++ b/lucene/grouping/build.gradle
@@ -21,9 +21,9 @@ apply plugin: 'java-library'
 description = 'Collectors for grouping search results'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:queries')
+  moduleImplementation project(':lucene:queries')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/backward-codecs/build.gradle b/lucene/grouping/src/java/module-info.java
similarity index 80%
copy from lucene/backward-codecs/build.gradle
copy to lucene/grouping/src/java/module-info.java
index aa47302..0f9665e 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/grouping/src/java/module-info.java
@@ -15,12 +15,10 @@
  * limitations under the License.
  */
 
+/** Collectors for grouping search results */
+module org.apache.lucene.grouping {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
 
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies { 
-  api project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.search.grouping;
 }
diff --git a/lucene/highlighter/build.gradle b/lucene/highlighter/build.gradle
index 6bd8426..105f1de 100644
--- a/lucene/highlighter/build.gradle
+++ b/lucene/highlighter/build.gradle
@@ -21,10 +21,10 @@ apply plugin: 'java-library'
 description = 'Highlights search keywords in results'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:queries')
-  implementation project(':lucene:memory')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:memory')
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:analysis:common')
diff --git a/lucene/analysis/icu/build.gradle b/lucene/highlighter/src/java/module-info.java
similarity index 67%
copy from lucene/analysis/icu/build.gradle
copy to lucene/highlighter/src/java/module-info.java
index 2eab963..64da8b5 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/highlighter/src/java/module-info.java
@@ -15,15 +15,14 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Highlights search keywords in results */
+module org.apache.lucene.highlighter {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.memory;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.search.highlight;
+  exports org.apache.lucene.search.matchhighlight;
+  exports org.apache.lucene.search.uhighlight;
+  exports org.apache.lucene.search.vectorhighlight;
 }
diff --git a/lucene/join/build.gradle b/lucene/join/build.gradle
index 143daa1..d8c2eab 100644
--- a/lucene/join/build.gradle
+++ b/lucene/join/build.gradle
@@ -20,6 +20,6 @@ apply plugin: 'java-library'
 description = 'Index-time and Query-time joins for normalized content'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
   testImplementation project(':lucene:test-framework')
 }
\ No newline at end of file
diff --git a/lucene/backward-codecs/build.gradle b/lucene/join/src/java/module-info.java
similarity index 81%
copy from lucene/backward-codecs/build.gradle
copy to lucene/join/src/java/module-info.java
index aa47302..80d2261 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/join/src/java/module-info.java
@@ -15,12 +15,9 @@
  * limitations under the License.
  */
 
+/** Index-time and Query-time joins for normalized content */
+module org.apache.lucene.join {
+  requires org.apache.lucene.core;
 
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies { 
-  api project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.search.join;
 }
diff --git a/lucene/licenses/asm-analysis-7.2.jar.sha1 b/lucene/licenses/asm-analysis-7.2.jar.sha1
new file mode 100644
index 0000000..b0a8b7b
--- /dev/null
+++ b/lucene/licenses/asm-analysis-7.2.jar.sha1
@@ -0,0 +1 @@
+b6e6abe057f23630113f4167c34bda7086691258
diff --git a/lucene/licenses/asm-tree-7.2.jar.sha1 b/lucene/licenses/asm-tree-7.2.jar.sha1
new file mode 100644
index 0000000..077d6f9
--- /dev/null
+++ b/lucene/licenses/asm-tree-7.2.jar.sha1
@@ -0,0 +1 @@
+3a23cc36edaf8fc5a89cb100182758ccb5991487
diff --git a/lucene/licenses/assertj-core-3.21.0.jar.sha1 b/lucene/licenses/assertj-core-3.21.0.jar.sha1
new file mode 100644
index 0000000..9108259
--- /dev/null
+++ b/lucene/licenses/assertj-core-3.21.0.jar.sha1
@@ -0,0 +1 @@
+27a14d6d22c4e3d58f799fb2a5ca8eaf53e6942a
diff --git a/lucene/licenses/assertj-core-LICENSE-ASL.txt b/lucene/licenses/assertj-core-LICENSE-ASL.txt
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/lucene/licenses/assertj-core-LICENSE-ASL.txt
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
diff --git a/lucene/licenses/assertj-core-NOTICE.txt b/lucene/licenses/assertj-core-NOTICE.txt
new file mode 100644
index 0000000..e69de29
diff --git a/lucene/luke/build.gradle b/lucene/luke/build.gradle
index fe556ca..6def3e6 100644
--- a/lucene/luke/build.gradle
+++ b/lucene/luke/build.gradle
@@ -25,25 +25,25 @@ ext {
 }
 
 dependencies {
-  api project(':lucene:core')
-
-  implementation project(':lucene:codecs')
-  implementation project(':lucene:backward-codecs')
-  implementation project(':lucene:analysis:common')
-  implementation project(':lucene:queries')
-  implementation project(':lucene:queryparser')
-  implementation project(':lucene:misc')
-
-  implementation project(":lucene:highlighter")
-  implementation project(':lucene:analysis:icu')
-  implementation project(':lucene:analysis:kuromoji')
-  implementation project(':lucene:analysis:morfologik')
-  implementation project(':lucene:analysis:nori')
-  implementation project(':lucene:analysis:opennlp')
-  implementation project(':lucene:analysis:phonetic')
-  implementation project(':lucene:analysis:smartcn')
-  implementation project(':lucene:analysis:stempel')
-  implementation project(':lucene:suggest')
+  moduleApi project(':lucene:core')
+
+  moduleImplementation project(':lucene:codecs')
+  moduleImplementation project(':lucene:backward-codecs')
+  moduleImplementation project(':lucene:analysis:common')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:queryparser')
+  moduleImplementation project(':lucene:misc')
+
+  moduleImplementation project(":lucene:highlighter")
+  moduleImplementation project(':lucene:analysis:icu')
+  moduleImplementation project(':lucene:analysis:kuromoji')
+  moduleImplementation project(':lucene:analysis:morfologik')
+  moduleImplementation project(':lucene:analysis:nori')
+  moduleImplementation project(':lucene:analysis:opennlp')
+  moduleImplementation project(':lucene:analysis:phonetic')
+  moduleImplementation project(':lucene:analysis:smartcn')
+  moduleImplementation project(':lucene:analysis:stempel')
+  moduleImplementation project(':lucene:suggest')
 
   testImplementation project(':lucene:test-framework')
 }
@@ -55,6 +55,11 @@ tasks.withType(Jar) {
   }
 }
 
+// Configure the main class and version attribute for the module system.
+tasks.compileJava.configure {
+  options.javaModuleMainClass.set("org.apache.lucene.luke.app.desktop.LukeMain")
+}
+
 // Process UTF8 property files to unicode escapes.
 tasks.withType(ProcessResources).configureEach { task ->
   task.filesMatching("**/messages*.properties", {
@@ -105,8 +110,13 @@ task standaloneAssemble(type: Sync) {
   into standaloneDistDir
 
   doLast {
-    logger.lifecycle("Standalone Luke distribution assembled. You can run it with:\n"
-        + "java -jar " + file("${standaloneDistDir}/${standaloneJar.archiveFileName.get()}"))
+    logger.lifecycle(
+        """
+Standalone Luke distribution assembled. You can run it with:
+java -jar "${standaloneDistDir}/${standaloneJar.archiveFileName.get()}"
+java --module-path "${standaloneDistDir}" -m org.apache.lucene.luke
+        """
+    )
   }
 }
 
@@ -141,3 +151,4 @@ task run() {
     }
   }
 }
+
diff --git a/lucene/analysis/icu/build.gradle b/lucene/luke/src/java/module-info.java
similarity index 71%
copy from lucene/analysis/icu/build.gradle
copy to lucene/luke/src/java/module-info.java
index 2eab963..101d4ae 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/luke/src/java/module-info.java
@@ -15,15 +15,13 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
-
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+/** Luke : Lucene toolbox project. */
+module org.apache.lucene.luke {
+  requires java.desktop;
+  requires java.logging;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.queryparser;
+  requires org.apache.lucene.misc;
 }
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java
index f64ec18..cd78468 100644
--- a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java
@@ -20,8 +20,8 @@ package org.apache.lucene.luke.app.desktop;
 import static org.apache.lucene.luke.app.desktop.util.ExceptionHandler.handle;
 
 import java.awt.GraphicsEnvironment;
-import java.io.IOException;
 import java.lang.invoke.MethodHandles;
+import java.util.concurrent.SynchronousQueue;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.swing.JFrame;
@@ -48,19 +48,19 @@ public class LukeMain {
     return frame;
   }
 
-  private static void createAndShowGUI() {
+  /** @return Returns {@code true} if GUI startup and initialization was successful. */
+  private static boolean createAndShowGUI() {
     // uncaught error handler
     MessageBroker messageBroker = MessageBroker.getInstance();
-    Thread.setDefaultUncaughtExceptionHandler((thread, cause) -> handle(cause, messageBroker));
-
     try {
+      Thread.setDefaultUncaughtExceptionHandler((thread, cause) -> handle(cause, messageBroker));
+
       frame = new LukeWindowProvider().get();
       frame.setLocation(200, 100);
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       frame.pack();
       frame.setVisible(true);
 
-      // show open index dialog
       OpenIndexDialogFactory openIndexDialogFactory = OpenIndexDialogFactory.getInstance();
       new DialogOpener<>(openIndexDialogFactory)
           .open(
@@ -68,9 +68,12 @@ public class LukeMain {
               600,
               420,
               (factory) -> {});
-    } catch (IOException e) {
+
+      return true;
+    } catch (Throwable e) {
       messageBroker.showUnknownErrorMessage();
       log.log(Level.SEVERE, "Cannot initialize components.", e);
+      return false;
     }
   }
 
@@ -86,6 +89,19 @@ public class LukeMain {
     GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
     genv.registerFont(FontUtils.createElegantIconFont());
 
-    javax.swing.SwingUtilities.invokeLater(LukeMain::createAndShowGUI);
+    var guiThreadResult = new SynchronousQueue<Boolean>();
+    javax.swing.SwingUtilities.invokeLater(
+        () -> {
+          try {
+            guiThreadResult.put(createAndShowGUI());
+          } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+          }
+        });
+
+    if (Boolean.FALSE.equals(guiThreadResult.take())) {
+      Logger.getGlobal().log(Level.SEVERE, "Luke could not start.");
+      Runtime.getRuntime().exit(1);
+    }
   }
 }
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/util/FontUtils.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/util/FontUtils.java
index 02c201e..90f5c2c 100644
--- a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/util/FontUtils.java
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/util/FontUtils.java
@@ -27,9 +27,7 @@ import javax.swing.JLabel;
 
 /** Font utilities */
 public class FontUtils {
-
-  public static final String TTF_RESOURCE_NAME =
-      "org/apache/lucene/luke/app/desktop/font/ElegantIcons.ttf";
+  private static final String TTF_RESOURCE_NAME = "ElegantIcons.ttf";
 
   @SuppressWarnings("unchecked")
   public static JLabel toLinkText(JLabel label) {
@@ -42,7 +40,7 @@ public class FontUtils {
   }
 
   public static Font createElegantIconFont() throws IOException, FontFormatException {
-    InputStream is = FontUtils.class.getClassLoader().getResourceAsStream(TTF_RESOURCE_NAME);
+    InputStream is = FontUtils.class.getResourceAsStream(TTF_RESOURCE_NAME);
     return Font.createFont(Font.TRUETYPE_FONT, is);
   }
 
diff --git a/lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/font/ElegantIcons.ttf b/lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/util/ElegantIcons.ttf
similarity index 100%
rename from lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/font/ElegantIcons.ttf
rename to lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/util/ElegantIcons.ttf
diff --git a/lucene/memory/build.gradle b/lucene/memory/build.gradle
index 0d3bc1f..eb6626e 100644
--- a/lucene/memory/build.gradle
+++ b/lucene/memory/build.gradle
@@ -21,7 +21,7 @@ apply plugin: 'java-library'
 description = 'Single-document in-memory index implementation'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:queryparser')
diff --git a/lucene/backward-codecs/build.gradle b/lucene/memory/src/java/module-info.java
similarity index 81%
copy from lucene/backward-codecs/build.gradle
copy to lucene/memory/src/java/module-info.java
index aa47302..1d4f84a 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/memory/src/java/module-info.java
@@ -15,12 +15,9 @@
  * limitations under the License.
  */
 
+/** Single-document in-memory index implementation */
+module org.apache.lucene.memory {
+  requires org.apache.lucene.core;
 
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies { 
-  api project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.index.memory;
 }
diff --git a/lucene/misc/build.gradle b/lucene/misc/build.gradle
index efed4f2..b9d7626 100644
--- a/lucene/misc/build.gradle
+++ b/lucene/misc/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 description = 'Index tools and other miscellaneous code'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
   testImplementation project(':lucene:test-framework')
 
   nativeDeps project(":lucene:misc:native")
diff --git a/lucene/analysis/icu/build.gradle b/lucene/misc/src/java/module-info.java
similarity index 67%
copy from lucene/analysis/icu/build.gradle
copy to lucene/misc/src/java/module-info.java
index 2eab963..269e6b0 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/misc/src/java/module-info.java
@@ -15,15 +15,15 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Index tools and other miscellaneous code */
+module org.apache.lucene.misc {
+  requires org.apache.lucene.core;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.misc;
+  exports org.apache.lucene.misc.document;
+  exports org.apache.lucene.misc.index;
+  exports org.apache.lucene.misc.search;
+  exports org.apache.lucene.misc.store;
+  exports org.apache.lucene.misc.util;
+  exports org.apache.lucene.misc.util.fst;
 }
diff --git a/lucene/monitor/build.gradle b/lucene/monitor/build.gradle
index 3dd65a4..ff6677f 100644
--- a/lucene/monitor/build.gradle
+++ b/lucene/monitor/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 description = 'Reverse-search implementation for monitoring and classification'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:memory')
-  implementation project(':lucene:analysis:common')
+  moduleImplementation project(':lucene:memory')
+  moduleImplementation project(':lucene:analysis:common')
 
   testImplementation project(':lucene:queryparser')
   testImplementation project(':lucene:test-framework')
diff --git a/lucene/analysis/stempel/build.gradle b/lucene/monitor/src/java/module-info.java
similarity index 75%
copy from lucene/analysis/stempel/build.gradle
copy to lucene/monitor/src/java/module-info.java
index 3b80f9f..71b70f0 100644
--- a/lucene/analysis/stempel/build.gradle
+++ b/lucene/monitor/src/java/module-info.java
@@ -15,13 +15,11 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Reverse-search implementation for monitoring and classification */
+module org.apache.lucene.monitor {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.memory;
 
-description = 'Analyzer for indexing Polish'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.monitor;
 }
diff --git a/lucene/queries/build.gradle b/lucene/queries/build.gradle
index bd56a4d..d0cc010 100644
--- a/lucene/queries/build.gradle
+++ b/lucene/queries/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 description = 'Filters and Queries that add to core Lucene'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:expressions')
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/queries/src/java/module-info.java
similarity index 61%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/queries/src/java/module-info.java
index 153515f..ae3ff4d 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/queries/src/java/module-info.java
@@ -15,18 +15,16 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Filters and Queries that add to core Lucene */
+module org.apache.lucene.queries {
+  requires org.apache.lucene.core;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.queries;
+  exports org.apache.lucene.queries.function;
+  exports org.apache.lucene.queries.function.docvalues;
+  exports org.apache.lucene.queries.function.valuesource;
+  exports org.apache.lucene.queries.intervals;
+  exports org.apache.lucene.queries.mlt;
+  exports org.apache.lucene.queries.payloads;
+  exports org.apache.lucene.queries.spans;
 }
diff --git a/lucene/queryparser/build.gradle b/lucene/queryparser/build.gradle
index 077eb24..b50b565 100644
--- a/lucene/queryparser/build.gradle
+++ b/lucene/queryparser/build.gradle
@@ -20,9 +20,9 @@ apply plugin: 'java-library'
 description = 'Query parsers and parsing framework'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:queries')
-  api project(':lucene:sandbox')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:queries')
+  moduleApi project(':lucene:sandbox')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/queryparser/src/java/module-info.java b/lucene/queryparser/src/java/module-info.java
new file mode 100644
index 0000000..3dab16d
--- /dev/null
+++ b/lucene/queryparser/src/java/module-info.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+/** Query parsers and parsing framework */
+module org.apache.lucene.queryparser {
+  requires java.xml;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.sandbox;
+
+  exports org.apache.lucene.queryparser.charstream;
+  exports org.apache.lucene.queryparser.classic;
+  exports org.apache.lucene.queryparser.complexPhrase;
+  exports org.apache.lucene.queryparser.ext;
+  exports org.apache.lucene.queryparser.flexible.core;
+  exports org.apache.lucene.queryparser.flexible.core.builders;
+  exports org.apache.lucene.queryparser.flexible.core.config;
+  exports org.apache.lucene.queryparser.flexible.core.messages;
+  exports org.apache.lucene.queryparser.flexible.core.nodes;
+  exports org.apache.lucene.queryparser.flexible.core.parser;
+  exports org.apache.lucene.queryparser.flexible.core.processors;
+  exports org.apache.lucene.queryparser.flexible.core.util;
+  exports org.apache.lucene.queryparser.flexible.messages;
+  exports org.apache.lucene.queryparser.flexible.precedence;
+  exports org.apache.lucene.queryparser.flexible.precedence.processors;
+  exports org.apache.lucene.queryparser.flexible.standard;
+  exports org.apache.lucene.queryparser.flexible.standard.builders;
+  exports org.apache.lucene.queryparser.flexible.standard.config;
+  exports org.apache.lucene.queryparser.flexible.standard.nodes;
+  exports org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn;
+  exports org.apache.lucene.queryparser.flexible.standard.parser;
+  exports org.apache.lucene.queryparser.flexible.standard.processors;
+  exports org.apache.lucene.queryparser.simple;
+  exports org.apache.lucene.queryparser.surround.parser;
+  exports org.apache.lucene.queryparser.surround.query;
+  exports org.apache.lucene.queryparser.xml;
+  exports org.apache.lucene.queryparser.xml.builders;
+}
diff --git a/lucene/replicator/build.gradle b/lucene/replicator/build.gradle
index c51feb0..d3dfc07 100644
--- a/lucene/replicator/build.gradle
+++ b/lucene/replicator/build.gradle
@@ -20,15 +20,15 @@ apply plugin: 'java-library'
 description = 'Lucene index files replication utility'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:facet')
+  moduleImplementation project(':lucene:facet')
 
-  implementation('org.apache.httpcomponents:httpclient', {
+  moduleImplementation('org.apache.httpcomponents:httpclient', {
     exclude group: "commons-codec", module: "commons-codec"
   })
 
-  implementation 'javax.servlet:javax.servlet-api'
+  moduleImplementation 'javax.servlet:javax.servlet-api'
 
   testImplementation 'org.eclipse.jetty:jetty-server'
   testImplementation('org.eclipse.jetty:jetty-servlet', {
diff --git a/lucene/analysis/icu/build.gradle b/lucene/replicator/src/java/module-info.java
similarity index 66%
copy from lucene/analysis/icu/build.gradle
copy to lucene/replicator/src/java/module-info.java
index 2eab963..d44b9ad 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/replicator/src/java/module-info.java
@@ -15,15 +15,15 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Lucene index files replication utility */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.replicator {
+  requires javax.servlet.api;
+  requires org.apache.httpcomponents.httpclient;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.facet;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.replicator;
+  exports org.apache.lucene.replicator.http;
+  exports org.apache.lucene.replicator.nrt;
 }
diff --git a/lucene/sandbox/build.gradle b/lucene/sandbox/build.gradle
index a1a64d1..b7e8018 100644
--- a/lucene/sandbox/build.gradle
+++ b/lucene/sandbox/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 description = 'Various third party contributions and new ideas'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:queries')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:queries')
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/sandbox/src/java/module-info.java
similarity index 60%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/sandbox/src/java/module-info.java
index 153515f..6d19544 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/sandbox/src/java/module-info.java
@@ -15,18 +15,17 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Various third party contributions and new ideas */
+module org.apache.lucene.sandbox {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
+  exports org.apache.lucene.payloads;
+  exports org.apache.lucene.sandbox.codecs.idversion;
+  exports org.apache.lucene.sandbox.document;
+  exports org.apache.lucene.sandbox.queries;
+  exports org.apache.lucene.sandbox.search;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.sandbox.codecs.idversion.IDVersionPostingsFormat;
 }
diff --git a/lucene/spatial-extras/build.gradle b/lucene/spatial-extras/build.gradle
index 252056b..34fa10e 100644
--- a/lucene/spatial-extras/build.gradle
+++ b/lucene/spatial-extras/build.gradle
@@ -20,16 +20,16 @@ apply plugin: 'java-library'
 description = 'Geospatial search'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:spatial3d')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:spatial3d')
 
-  api 'org.locationtech.spatial4j:spatial4j'
-  api 'io.sgr:s2-geometry-library-java'
+  moduleApi 'org.locationtech.spatial4j:spatial4j'
+  moduleApi 'io.sgr:s2-geometry-library-java'
 
   testImplementation project(':lucene:test-framework')
   testImplementation testFixtures(project(':lucene:spatial3d'))
 
-  testImplementation 'org.locationtech.jts:jts-core'
+  moduleTestImplementation 'org.locationtech.jts:jts-core'
   testImplementation 'org.locationtech.spatial4j:spatial4j::tests'
 }
 
diff --git a/lucene/analysis/icu/build.gradle b/lucene/spatial-extras/src/java/module-info.java
similarity index 53%
copy from lucene/analysis/icu/build.gradle
copy to lucene/spatial-extras/src/java/module-info.java
index 2eab963..2464761 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/spatial-extras/src/java/module-info.java
@@ -15,15 +15,23 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Geospatial search */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.spatial_extras {
+  requires java.logging;
+  requires spatial4j;
+  requires s2.geometry.library.java;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.spatial3d;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.spatial;
+  exports org.apache.lucene.spatial.bbox;
+  exports org.apache.lucene.spatial.composite;
+  exports org.apache.lucene.spatial.prefix;
+  exports org.apache.lucene.spatial.prefix.tree;
+  exports org.apache.lucene.spatial.query;
+  exports org.apache.lucene.spatial.serialized;
+  exports org.apache.lucene.spatial.spatial4j;
+  exports org.apache.lucene.spatial.util;
+  exports org.apache.lucene.spatial.vector;
 }
diff --git a/lucene/spatial3d/build.gradle b/lucene/spatial3d/build.gradle
index db2064c..219d029 100644
--- a/lucene/spatial3d/build.gradle
+++ b/lucene/spatial3d/build.gradle
@@ -21,7 +21,7 @@ apply plugin: 'java-test-fixtures'
 description = '3D spatial planar geometry APIs'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
   testFixturesApi project(':lucene:test-framework')
   testImplementation project(':lucene:test-framework')
diff --git a/lucene/backward-codecs/build.gradle b/lucene/spatial3d/src/java/module-info.java
similarity index 80%
copy from lucene/backward-codecs/build.gradle
copy to lucene/spatial3d/src/java/module-info.java
index aa47302..9bffb23 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/spatial3d/src/java/module-info.java
@@ -15,12 +15,10 @@
  * limitations under the License.
  */
 
+/** 3D spatial planar geometry APIs */
+module org.apache.lucene.spatial3d {
+  requires org.apache.lucene.core;
 
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies { 
-  api project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.spatial3d;
+  exports org.apache.lucene.spatial3d.geom;
 }
diff --git a/lucene/suggest/build.gradle b/lucene/suggest/build.gradle
index 54a5596..3afccd8 100644
--- a/lucene/suggest/build.gradle
+++ b/lucene/suggest/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Auto-suggest and Spellchecking support'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
   
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/suggest/src/java/module-info.java b/lucene/suggest/src/java/module-info.java
new file mode 100644
index 0000000..4444a9dd
--- /dev/null
+++ b/lucene/suggest/src/java/module-info.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+/** Auto-suggest and Spellchecking support */
+module org.apache.lucene.suggest {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+
+  exports org.apache.lucene.search.spell;
+  exports org.apache.lucene.search.suggest;
+  exports org.apache.lucene.search.suggest.analyzing;
+  exports org.apache.lucene.search.suggest.document;
+  exports org.apache.lucene.search.suggest.fst;
+  exports org.apache.lucene.search.suggest.tst;
+
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.search.suggest.document.Completion50PostingsFormat,
+      org.apache.lucene.search.suggest.document.Completion84PostingsFormat,
+      org.apache.lucene.search.suggest.document.Completion90PostingsFormat;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.search.suggest.analyzing.SuggestStopFilterFactory;
+}
diff --git a/settings.gradle b/settings.gradle
index e965095..2d48e41 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -30,6 +30,7 @@ include "lucene:analysis:common"
 include "lucene:analysis:icu"
 include "lucene:analysis:kuromoji"
 include "lucene:analysis:morfologik"
+include "lucene:analysis:morfologik.tests"
 include "lucene:analysis:nori"
 include "lucene:analysis:opennlp"
 include "lucene:analysis:phonetic"
@@ -41,6 +42,9 @@ include "lucene:classification"
 include "lucene:codecs"
 include "lucene:core"
 include "lucene:demo"
+include "lucene:distribution"
+include "lucene:distribution.tests"
+include "lucene:documentation"
 include "lucene:expressions"
 include "lucene:facet"
 include "lucene:grouping"
@@ -55,9 +59,7 @@ include "lucene:queries"
 include "lucene:queryparser"
 include "lucene:replicator"
 include "lucene:sandbox"
-include "lucene:spatial-extras"
 include "lucene:spatial3d"
+include "lucene:spatial-extras"
 include "lucene:suggest"
 include "lucene:test-framework"
-include "lucene:documentation"
-include "lucene:distribution"
diff --git a/versions.lock b/versions.lock
index 87c5b3f..c596e6e 100644
--- a/versions.lock
+++ b/versions.lock
@@ -18,12 +18,15 @@ org.carrot2:morfologik-polish:2.1.8 (1 constraints: 0d050036)
 org.carrot2:morfologik-stemming:2.1.8 (2 constraints: 1112dc0c)
 org.hamcrest:hamcrest:2.2 (1 constraints: a8041f2c)
 org.locationtech.spatial4j:spatial4j:0.8 (1 constraints: ac041f2c)
-org.ow2.asm:asm:7.2 (2 constraints: 900e3e5e)
+org.ow2.asm:asm:7.2 (3 constraints: 2717d96b)
+org.ow2.asm:asm-analysis:7.2 (1 constraints: e409d9a5)
 org.ow2.asm:asm-commons:7.2 (1 constraints: ad042e2c)
+org.ow2.asm:asm-tree:7.2 (2 constraints: 2f14468c)
 ua.net.nlp:morfologik-ukrainian-search:4.9.1 (1 constraints: 10051b36)
 xerces:xercesImpl:2.12.0 (2 constraints: 1f14b675)
 
 [Test dependencies]
+org.assertj:assertj-core:3.21.0 (1 constraints: 38053c3b)
 org.eclipse.jetty:jetty-continuation:9.4.41.v20210516 (1 constraints: 7907fe7c)
 org.eclipse.jetty:jetty-http:9.4.41.v20210516 (1 constraints: f60f2ccd)
 org.eclipse.jetty:jetty-io:9.4.41.v20210516 (2 constraints: 141f4566)
diff --git a/versions.props b/versions.props
index c010392..995a7dc 100644
--- a/versions.props
+++ b/versions.props
@@ -11,6 +11,7 @@ org.antlr:antlr4*=4.5.1-1
 org.apache.commons:commons-compress=1.19
 org.apache.httpcomponents:httpclient=4.5.13
 org.apache.opennlp:opennlp-tools=1.9.1
+org.assertj:*=3.21.0
 org.carrot2:morfologik-*=2.1.8
 org.eclipse.jetty:*=9.4.41.v20210516
 org.hamcrest:*=2.2
@@ -18,4 +19,4 @@ org.locationtech.jts:jts-core=1.17.0
 org.locationtech.spatial4j:*=0.8
 org.ow2.asm:*=7.2
 ua.net.nlp:morfologik-ukrainian-search=4.9.1
-xerces:xercesImpl=2.12.0
+xerces:xercesImpl=2.12.0
\ No newline at end of file

[lucene] 02/03: LUCENE-10327: workaround for gradle emitting empty sourcepath.

Posted by dw...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dweiss pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git

commit 70b4239fbd99db6c42c269100610f3b9a5acde63
Author: Dawid Weiss <da...@carrotsearch.com>
AuthorDate: Sun Dec 19 08:52:01 2021 +0100

    LUCENE-10327: workaround for gradle emitting empty sourcepath.
---
 gradle/java/modules.gradle | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/gradle/java/modules.gradle b/gradle/java/modules.gradle
index 45405a2..94604b9 100644
--- a/gradle/java/modules.gradle
+++ b/gradle/java/modules.gradle
@@ -110,6 +110,9 @@ allprojects {
       tasks.named(sourceSet.getCompileJavaTaskName()).configure({ JavaCompile task ->
         task.dependsOn modularPaths.compileModulePathConfiguration
 
+        // LUCENE-10327: don't allow gradle to emit an empty sourcepath as it would break compilation.
+        task.options.setSourcepath(sourceSet.java.sourceDirectories)
+
         // Add modular dependencies and their transitive dependencies to module path.
         task.options.compilerArgumentProviders.add((CommandLineArgumentProvider) {
           def modularPathFiles = modularPaths.compileModulePathConfiguration.files

[lucene] 03/03: Don't log warnings from ant (different class loader, I guess). Makes Alan happier.

Posted by dw...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dweiss pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git

commit 4102b98647891e89eb78daeef9a49b1242e02697
Author: Dawid Weiss <da...@carrotsearch.com>
AuthorDate: Fri Nov 26 11:39:55 2021 +0100

    Don't log warnings from ant (different class loader, I guess). Makes Alan happier.
---
 gradle/validation/rat-sources.gradle | 1 +
 1 file changed, 1 insertion(+)

diff --git a/gradle/validation/rat-sources.gradle b/gradle/validation/rat-sources.gradle
index 0f9dff1..2acc34e 100644
--- a/gradle/validation/rat-sources.gradle
+++ b/gradle/validation/rat-sources.gradle
@@ -135,6 +135,7 @@ class RatTask extends DefaultTask {
     def generateReport(File reportFile) {
         // Set up ant rat task.
         def ratClasspath = project.rootProject.configurations.ratDeps.asPath
+        ant.setLifecycleLogLevel(AntBuilder.AntMessagePriority.ERROR)
         ant.taskdef(resource: 'org/apache/rat/anttasks/antlib.xml', classpath: ratClasspath)
 
         // Collect all output files for debugging.