elastic/elasticsearch · error · UncheckedIOException

Failed to extract foreign API stubs

Error message

Failed to extract foreign API stubs

What it means

Thrown as an UncheckedIOException inside ExtractForeignApiTask.ExtractionWorkAction.execute() when an IOException occurs while walking the JDK's jrt:/ filesystem (java.lang.foreign classes) or writing the stub JAR via JarOutputStream. The worker reads class files from jrt:/modules/java.base/java/lang/foreign, strips @PreviewFeature annotations, and writes a patched JAR for --patch-module use. Any I/O failure (disk full, broken jrt fs, output dir locked) is wrapped into this message.

Source

Thrown at build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ExtractForeignApiTask.java:187

                        continue;
                    }
                    byte[] stubBytes;
                    try (InputStream is = Files.newInputStream(file)) {
                        stubBytes = createStub(is);
                    }
                    if (stubBytes == null) {
                        continue;
                    }
                    String entryName = FOREIGN_PACKAGE_PREFIX + file.getFileName().toString();
                    JarEntry entry = new JarEntry(entryName);
                    entry.setTime(0);
                    jar.putNextEntry(entry);
                    jar.write(stubBytes);
                    jar.closeEntry();
                    count++;
                }
            } catch (IOException e) {
                throw new UncheckedIOException("Failed to extract foreign API stubs", e);
            }

            LOGGER.info("Generated {} with {} class(es)", outputPath, count);
        }

        private static void checkRuntimeJava21() {
            int jdkVersion = Runtime.version().feature();
            if (jdkVersion != 21) {
                throw new IllegalStateException(
                    "ExtractForeignApiTask worker must run on JDK 21 (found JDK "
                        + jdkVersion
                        + "). "
                        + "The Foreign Function & Memory API is preview in JDK 21 only; on JDK 22+ it is standard and this stub JAR is unnecessary."
                );
            }
        }

        static byte[] createStub(InputStream classStream) throws IOException {

View on GitHub (pinned to db6a809a66)

Solutions

  1. Verify the output path (getOutputJar()) is writable and its parent directory can be created; check disk space on the build volume.
  2. Confirm the worker JVM is a full OpenJDK 21 (not a JRE/stripped runtime) so jrt:/ is available — check getJdk21Launcher() resolves to a real JDK 21.
  3. Re-run with --info --stacktrace to capture the wrapped IOException's cause, which names the failing path or operation.
  4. If running on Windows with antivirus, exclude the build directory from scanning, or move the build output to a local non-synced path.

Example fix

// before: output jar resolved to a read-only or network path
getOutputJar().set(project.getLayout().getBuildDirectory().file("libs/native/build/jdk21-foreign-api.jar"));

// after: ensure parent is a local writable dir, force-create before submit
File out = getOutputJar().get().getAsFile();
Files.createDirectories(out.getParentFile().toPath());
Defensive patterns

Strategy: try-catch

Validate before calling

Path out = task.getOutputJar().get().getAsFile().toPath();
if (Files.isWritable(out.getParent()) == false) {
    throw new IllegalStateException("Output dir not writable: " + out.getParent());
}
if (FileSystems.getFileSystem(URI.create("jrt:/")) == null) {
    throw new IllegalStateException("jrt:/ filesystem unavailable; not a full JDK");
}

Try / catch

try {
    task.extract();
} catch (UncheckedIOException e) {
    // inspect e.getCause() for the underlying IOException
    throw new GradleException("Foreign API stub extraction failed: " + e.getCause().getMessage(), e.getCause());
}

Prevention

When it happens

Trigger: The try-with-resources block at line 163-188 opens a JarOutputStream on outputPath and walks foreignRoot via Files.walk(). An IOException from Files.walk, Files.newOutputStream, jar.putNextEntry, or jar.write propagates to the catch at line 186 and is rethrown wrapped. Common triggers: output directory not creatable, disk full, jrt:/ filesystem unavailable (non-OpenJDK JVM), concurrent write to the same outputPath.

Common situations: Running the extractForeignApi task under a JRE that lacks the jrt:/ filesystem (some bundled JREs), build output directory on a network filesystem with locking issues, disk-full CI agents, antivirus locking the output JAR on Windows, or a misconfigured getOutputJar() pointing to a read-only location.

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/594147d967a6d080. Report an issue: GitHub.