diff --git a/picocli-codegen-tests-java9plus/src/test/java/picocli/annotation/processing/tests/Issue2464Test.java b/picocli-codegen-tests-java9plus/src/test/java/picocli/annotation/processing/tests/Issue2464Test.java
new file mode 100644
index 000000000..00eb8b44a
--- /dev/null
+++ b/picocli-codegen-tests-java9plus/src/test/java/picocli/annotation/processing/tests/Issue2464Test.java
@@ -0,0 +1,83 @@
+package picocli.annotation.processing.tests;
+
+import org.junit.Test;
+import picocli.codegen.aot.graalvm.processor.NativeImageConfigGeneratorProcessor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Ensures {@code info.picocli.codegen} declares a {@code provides ... with}
+ * for {@link javax.annotation.processing.Processor} so the annotation processor
+ * is discovered when placed on {@code --processor-module-path}.
+ *
+ * @see #2464
+ */
+public class Issue2464Test {
+
+ private static final String MODULE_NAME = "info.picocli.codegen";
+ private static final String PROCESSOR_SERVICE = "javax.annotation.processing.Processor";
+ private static final String PROCESSOR_IMPL =
+ "picocli.codegen.aot.graalvm.processor.NativeImageConfigGeneratorProcessor";
+
+ @Test
+ public void moduleInfoProvidesAnnotationProcessor() throws Exception {
+ ModuleDescriptor descriptor = readCodegenModuleDescriptor();
+ assertEquals(MODULE_NAME, descriptor.name());
+
+ Set providers = descriptor.provides().stream()
+ .filter(p -> PROCESSOR_SERVICE.equals(p.service()))
+ .flatMap(p -> p.providers().stream())
+ .collect(Collectors.toSet());
+
+ assertTrue(
+ "info.picocli.codegen must provide " + PROCESSOR_SERVICE
+ + " with " + PROCESSOR_IMPL
+ + " (needed for --processor-module-path discovery). Actual provides: "
+ + descriptor.provides(),
+ providers.contains(PROCESSOR_IMPL));
+ }
+
+ private static ModuleDescriptor readCodegenModuleDescriptor()
+ throws IOException, URISyntaxException {
+ URL location = NativeImageConfigGeneratorProcessor.class
+ .getProtectionDomain()
+ .getCodeSource()
+ .getLocation();
+ Path path = Paths.get(location.toURI());
+
+ if (Files.isRegularFile(path)) {
+ Optional ref = ModuleFinder.of(path).find(MODULE_NAME);
+ if (ref.isPresent()) {
+ return ref.get().descriptor();
+ }
+ fail("Could not find module " + MODULE_NAME + " in jar " + path);
+ }
+
+ // Exploded multi-release layout used by the Gradle build
+ Path moduleInfo = path.resolve("META-INF/versions/9/module-info.class");
+ if (Files.exists(moduleInfo)) {
+ try (InputStream in = Files.newInputStream(moduleInfo)) {
+ return ModuleDescriptor.read(in);
+ }
+ }
+
+ fail("Could not locate module-info for " + MODULE_NAME + " at " + path);
+ return null; // unreachable
+ }
+}
diff --git a/picocli-codegen/src/main/java9/module-info.java b/picocli-codegen/src/main/java9/module-info.java
index 0df99d034..58bb8972d 100644
--- a/picocli-codegen/src/main/java9/module-info.java
+++ b/picocli-codegen/src/main/java9/module-info.java
@@ -39,4 +39,10 @@
exports picocli.codegen.aot.graalvm;
exports picocli.codegen.aot.graalvm.processor;
exports picocli.codegen.docgen.manpage;
+
+ // Required for discovery when picocli-codegen is on the processor module path
+ // (--processor-module-path). META-INF/services covers the classpath case.
+ // See https://github.com/remkop/picocli/issues/2464
+ provides javax.annotation.processing.Processor
+ with picocli.codegen.aot.graalvm.processor.NativeImageConfigGeneratorProcessor;
}