[jnigen] JAR handling improvements (https://github.com/dart-lang/jnigen/issues/220)
* Move summarizer invocation into summary.dart for better testability.
* Support reading source JARs (https://github.com/dart-lang/jnigen/issues/208)
* Fix an issue where the summarizer failed when providing only classes and no source (https://github.com/dart-lang/jnigen/issues/147)
diff --git a/pkgs/jnigen/bin/jnigen.dart b/pkgs/jnigen/bin/jnigen.dart
index 518ba11..be04b58 100644
--- a/pkgs/jnigen/bin/jnigen.dart
+++ b/pkgs/jnigen/bin/jnigen.dart
@@ -11,10 +11,8 @@
config = Config.parseArgs(args);
} on ConfigException catch (e) {
log.fatal(e);
- return;
} on FormatException catch (e) {
log.fatal(e);
- return;
}
await generateJniBindings(config);
}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java
index d636f65..07f4430 100644
--- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java
@@ -4,134 +4,44 @@
package com.github.dart_lang.jnigen.apisummarizer;
-import static com.github.dart_lang.jnigen.apisummarizer.util.ExceptionUtil.wrapCheckedException;
-
import com.github.dart_lang.jnigen.apisummarizer.disasm.AsmSummarizer;
import com.github.dart_lang.jnigen.apisummarizer.doclet.SummarizerDoclet;
import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
+import com.github.dart_lang.jnigen.apisummarizer.util.InputStreamProvider;
import com.github.dart_lang.jnigen.apisummarizer.util.JsonUtil;
import com.github.dart_lang.jnigen.apisummarizer.util.Log;
-import com.github.dart_lang.jnigen.apisummarizer.util.StreamUtil;
-import java.io.*;
-import java.util.*;
-import java.util.jar.JarFile;
-import java.util.stream.Collectors;
-import java.util.zip.ZipEntry;
+import com.github.dart_lang.jnigen.apisummarizer.util.SearchUtil;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import javax.tools.DocumentationTool;
+import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;
import jdk.javadoc.doclet.Doclet;
-import org.apache.commons.cli.*;
public class Main {
public enum Backend {
- // Produce API descriptions from source files using Doclet API.
+ /** Produce API descriptions from source files using Doclet API. */
DOCLET,
- // Produce API descriptions from class files under classpath.
+ /** Produce API descriptions from class files under classpath. */
ASM,
- // Prefer source but fall back to JARs in classpath if sources not found.
+ /** Prefer source but fall back to JARs in classpath if sources not found. */
AUTO,
}
- public static class SummarizerOptions {
- String sourcePath;
- String classPath;
- boolean useModules;
- Backend backend;
- String modulesList;
- boolean addDependencies;
- String toolArgs;
- boolean verbose;
- String outputFile;
- String[] args;
-
- SummarizerOptions() {}
-
- public static SummarizerOptions fromCommandLine(CommandLine cmd) {
- var opts = new SummarizerOptions();
- opts.sourcePath = cmd.getOptionValue("sources", ".");
- var backendString = cmd.getOptionValue("backend", "auto");
- opts.backend = Backend.valueOf(backendString.toUpperCase());
- opts.classPath = cmd.getOptionValue("classes", null);
- opts.useModules = cmd.hasOption("use-modules");
- opts.modulesList = cmd.getOptionValue("module-names", null);
- opts.addDependencies = cmd.hasOption("recursive");
- opts.toolArgs = cmd.getOptionValue("doctool-args", null);
- opts.verbose = cmd.hasOption("verbose");
- opts.outputFile = cmd.getOptionValue("output-file", null);
- opts.args = cmd.getArgs();
- return opts;
- }
- }
-
- private static final CommandLineParser parser = new DefaultParser();
static SummarizerOptions options;
- public static SummarizerOptions parseArgs(String[] args) {
- var options = new Options();
- Option sources = new Option("s", "sources", true, "paths to search for source files");
- Option classes = new Option("c", "classes", true, "paths to search for compiled classes");
- Option backend =
- new Option(
- "b",
- "backend",
- true,
- "backend to use for summary generation ('doclet', 'asm' or 'auto' (default)).");
- Option useModules = new Option("M", "use-modules", false, "use Java modules");
- Option recursive = new Option("r", "recursive", false, "Include dependencies of classes");
- Option moduleNames =
- new Option("m", "module-names", true, "comma separated list of module names");
- Option doctoolArgs =
- new Option("D", "doctool-args", true, "Arguments to pass to the documentation tool");
- Option verbose = new Option("v", "verbose", false, "Enable verbose output");
- Option outputFile =
- new Option("o", "output-file", true, "Write JSON to file instead of stdout");
- for (Option opt :
- new Option[] {
- sources,
- classes,
- backend,
- useModules,
- recursive,
- moduleNames,
- doctoolArgs,
- verbose,
- outputFile,
- }) {
- options.addOption(opt);
- }
-
- HelpFormatter help = new HelpFormatter();
-
- CommandLine cmd;
-
- try {
- cmd = parser.parse(options, args);
- if (cmd.getArgs().length < 1) {
- throw new ParseException("Need to specify paths to source files");
- }
- } catch (ParseException e) {
- System.out.println(e.getMessage());
- help.printHelp(
- "java -jar <JAR> [-s <SOURCE_DIR=.>] "
- + "[-c <CLASSES_JAR>] <CLASS_OR_PACKAGE_NAMES>\n"
- + "Class or package names should be fully qualified.\n\n",
- options);
- System.exit(1);
- throw new RuntimeException("Unreachable code");
- }
- return SummarizerOptions.fromCommandLine(cmd);
- }
-
public static List<ClassDecl> runDocletWithClass(
- Class<? extends Doclet> docletClass, List<File> javaFilePaths, SummarizerOptions options) {
+ DocumentationTool javaDoc,
+ Class<? extends Doclet> docletClass,
+ List<JavaFileObject> fileObjects,
+ SummarizerOptions options) {
Log.setVerbose(options.verbose);
-
- var files = javaFilePaths.stream().map(File::getPath).toArray(String[]::new);
-
- DocumentationTool javadoc = ToolProvider.getSystemDocumentationTool();
- var fileManager = javadoc.getStandardFileManager(null, null, null);
- var fileObjects = fileManager.getJavaFileObjects(files);
-
+ var fileManager = javaDoc.getStandardFileManager(null, null, null);
var cli = new ArrayList<String>();
cli.add((options.useModules ? "--module-" : "--") + "source-path=" + options.sourcePath);
if (options.classPath != null) {
@@ -146,161 +56,56 @@
cli.addAll(List.of(options.toolArgs.split(" ")));
}
- javadoc.getTask(null, fileManager, System.err::println, docletClass, cli, fileObjects).call();
+ javaDoc.getTask(null, fileManager, System.err::println, docletClass, cli, fileObjects).call();
return SummarizerDoclet.getClasses();
}
- public static List<ClassDecl> runDoclet(List<File> javaFilePaths, SummarizerOptions options) {
- return runDocletWithClass(SummarizerDoclet.class, javaFilePaths, options);
- }
-
- /**
- * Lists all files under given directory, which satisfy the condition of filter. The order of
- * listing shall be deterministic.
- */
- public static List<File> recursiveListFiles(File file, FileFilter filter) {
- if (!file.exists()) {
- throw new RuntimeException("File not found: " + file.getPath());
- }
-
- if (!file.isDirectory()) {
- return List.of(file);
- }
-
- // List files using a breadth-first traversal.
- var files = new ArrayList<File>();
- var queue = new ArrayDeque<File>();
- queue.add(file);
- while (!queue.isEmpty()) {
- var dir = queue.poll();
- var list = dir.listFiles(entry -> entry.isDirectory() || filter.accept(entry));
- if (list == null) throw new IllegalArgumentException("File.listFiles returned null!");
- Arrays.sort(list);
- for (var path : list) {
- if (path.isDirectory()) {
- queue.add(path);
- } else {
- files.add(path);
- }
- }
- }
- return files;
- }
-
- /**
- * Finds and returns source file (s) corresponding to qualified name. It's assumed that package
- * hierarchy in Java is same as the filesystem hierarchy, i.e. each package corresponds to a
- * directory and each class corresponds to a file in its respective package. If the respective
- * file or directory does not exist, the returned optional is empty.
- */
- public static Optional<List<File>> findSourceFiles(String qualifiedName, String[] sourcePaths) {
- return findFiles(qualifiedName, sourcePaths, ".java");
- }
-
- public static Optional<List<File>> findFiles(
- String qualifiedName, String[] searchPaths, String suffix) {
- var s = qualifiedName.replace(".", "/");
- for (var folder : searchPaths) {
- var f = new File(folder, s + suffix);
- if (f.exists() && f.isFile()) {
- return Optional.of(List.of(f));
- }
- var d = new File(folder, s);
- if (d.exists() && d.isDirectory()) {
- return Optional.of(recursiveListFiles(d, file -> file.getName().endsWith(".java")));
- }
- }
- return Optional.empty();
- }
-
- public static Optional<List<InputStream>> findClassInputStreamsInJar(
- JarFile jar, String relativePath) {
- var suffix = ".class";
- var classEntry = jar.getEntry(relativePath + suffix);
- if (classEntry != null) {
- return Optional.of(List.of(wrapCheckedException(jar::getInputStream, classEntry)));
- }
- var dirPath = relativePath.endsWith("/") ? relativePath : relativePath + "/";
- var dirEntry = jar.getEntry(dirPath);
- if (dirEntry != null && dirEntry.isDirectory()) {
- var result =
- jar.stream()
- .map(je -> (ZipEntry) je)
- .filter(
- entry -> {
- var name = entry.getName();
- return name.endsWith(suffix) && name.startsWith(dirPath);
- })
- .map(entry -> wrapCheckedException(jar::getInputStream, entry))
- .collect(Collectors.toList());
- return Optional.of(result);
- }
- return Optional.empty();
- }
-
- /**
- * Finds and returns class file(s) corresponding to qualified name from JAR files in classpath.
- */
- public static Optional<List<InputStream>> findClassInputStreams(
- String binaryName, String[] classPaths) {
- String relativePath = binaryName.replace(".", "/");
- for (var path : classPaths) {
- var file = new File(path);
- // A path in classpath can be a directory or JAR. These cases require different logic.
- if (file.isDirectory()) {
- var directorySearchResult = findFiles(binaryName, classPaths, ".class");
- if (directorySearchResult.isPresent()) {
- var list = directorySearchResult.get();
- return Optional.of(
- StreamUtil.map(
- list, fileElement -> wrapCheckedException(FileInputStream::new, fileElement)));
- }
- continue;
- }
- try {
- JarFile jar = new JarFile(file);
- var inJar = findClassInputStreamsInJar(jar, relativePath);
- if (inJar.isPresent()) {
- return inJar;
- } else {
- jar.close();
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- return Optional.empty();
+ public static List<ClassDecl> runDoclet(
+ DocumentationTool javaDoc, List<JavaFileObject> javaFileObjects, SummarizerOptions options) {
+ return runDocletWithClass(javaDoc, SummarizerDoclet.class, javaFileObjects, options);
}
public static void main(String[] args) throws FileNotFoundException {
- options = parseArgs(args);
+ options = SummarizerOptions.parseArgs(args);
OutputStream output;
+
if (options.outputFile == null || options.outputFile.equals("-")) {
output = System.out;
} else {
output = new FileOutputStream(options.outputFile);
}
- var sourcePaths =
- options.sourcePath != null ? options.sourcePath.split(File.pathSeparator) : new String[] {};
- var classPaths =
- options.classPath != null ? options.classPath.split(File.pathSeparator) : new String[] {};
- var classStreams = new ArrayList<InputStream>();
- var sourceFiles = new ArrayList<File>();
+
+ List<String> sourcePaths =
+ options.sourcePath != null
+ ? Arrays.asList(options.sourcePath.split(File.pathSeparator))
+ : List.of();
+ List<String> classPaths =
+ options.classPath != null
+ ? Arrays.asList(options.classPath.split(File.pathSeparator))
+ : List.of();
+
+ var classStreamProviders = new ArrayList<InputStreamProvider>();
+ var sourceFiles = new ArrayList<JavaFileObject>();
var notFound = new ArrayList<String>();
+
+ var javaDoc = ToolProvider.getSystemDocumentationTool();
+
for (var qualifiedName : options.args) {
var found = false;
if (options.backend != Backend.ASM) {
- var sources = findSourceFiles(qualifiedName, sourcePaths);
+ var sources =
+ SearchUtil.findJavaSources(
+ qualifiedName, sourcePaths, javaDoc.getStandardFileManager(null, null, null));
if (sources.isPresent()) {
sourceFiles.addAll(sources.get());
found = true;
}
}
if (options.backend != Backend.DOCLET && !found) {
- var classes = findClassInputStreams(qualifiedName, classPaths);
+ var classes = SearchUtil.findJavaClasses(qualifiedName, classPaths);
if (classes.isPresent()) {
- classStreams.addAll(classes.get());
+ classStreamProviders.addAll(classes.get());
found = true;
}
}
@@ -316,14 +121,19 @@
switch (options.backend) {
case DOCLET:
- JsonUtil.writeJSON(runDoclet(sourceFiles, options), output);
+ JsonUtil.writeJSON(runDoclet(javaDoc, sourceFiles, options), output);
break;
case ASM:
- JsonUtil.writeJSON(AsmSummarizer.run(classStreams), output);
+ JsonUtil.writeJSON(AsmSummarizer.run(classStreamProviders), output);
break;
case AUTO:
- var decls = runDoclet(sourceFiles, options);
- decls.addAll(AsmSummarizer.run(classStreams));
+ List<ClassDecl> decls = new ArrayList<>();
+ if (!sourceFiles.isEmpty()) {
+ decls.addAll(runDoclet(javaDoc, sourceFiles, options));
+ }
+ if (!classStreamProviders.isEmpty()) {
+ decls.addAll(AsmSummarizer.run(classStreamProviders));
+ }
JsonUtil.writeJSON(decls, output);
break;
}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/SummarizerOptions.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/SummarizerOptions.java
new file mode 100644
index 0000000..ca63a45
--- /dev/null
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/SummarizerOptions.java
@@ -0,0 +1,94 @@
+package com.github.dart_lang.jnigen.apisummarizer;
+
+import org.apache.commons.cli.*;
+
+public class SummarizerOptions {
+ private static final CommandLineParser parser = new DefaultParser();
+ String sourcePath;
+ String classPath;
+ boolean useModules;
+ Main.Backend backend;
+ String modulesList;
+ boolean addDependencies;
+ String toolArgs;
+ boolean verbose;
+ String outputFile;
+ String[] args;
+
+ SummarizerOptions() {}
+
+ public static SummarizerOptions fromCommandLine(CommandLine cmd) {
+ var opts = new SummarizerOptions();
+ opts.sourcePath = cmd.getOptionValue("sources", ".");
+ var backendString = cmd.getOptionValue("backend", "auto");
+ opts.backend = Main.Backend.valueOf(backendString.toUpperCase());
+ opts.classPath = cmd.getOptionValue("classes", null);
+ opts.useModules = cmd.hasOption("use-modules");
+ opts.modulesList = cmd.getOptionValue("module-names", null);
+ opts.addDependencies = cmd.hasOption("recursive");
+ opts.toolArgs = cmd.getOptionValue("doctool-args", null);
+ opts.verbose = cmd.hasOption("verbose");
+ opts.outputFile = cmd.getOptionValue("output-file", null);
+ opts.args = cmd.getArgs();
+ if (opts.args.length == 0) {
+ throw new IllegalArgumentException("Need one or more class or package names as arguments");
+ }
+ return opts;
+ }
+
+ public static SummarizerOptions parseArgs(String[] args) {
+ var options = new Options();
+ Option sources = new Option("s", "sources", true, "paths to search for source files");
+ Option classes = new Option("c", "classes", true, "paths to search for compiled classes");
+ Option backend =
+ new Option(
+ "b",
+ "backend",
+ true,
+ "backend to use for summary generation ('doclet', 'asm' or 'auto' (default)).");
+ Option useModules = new Option("M", "use-modules", false, "use Java modules");
+ Option recursive = new Option("r", "recursive", false, "include dependencies of classes");
+ Option moduleNames =
+ new Option("m", "module-names", true, "comma separated list of module names");
+ Option doctoolArgs =
+ new Option("D", "doctool-args", true, "arguments to pass to the documentation tool");
+ Option verbose = new Option("v", "verbose", false, "enable verbose output");
+ Option outputFile =
+ new Option("o", "output-file", true, "write JSON to file instead of stdout");
+ for (Option opt :
+ new Option[] {
+ sources,
+ classes,
+ backend,
+ useModules,
+ recursive,
+ moduleNames,
+ doctoolArgs,
+ verbose,
+ outputFile,
+ }) {
+ options.addOption(opt);
+ }
+
+ HelpFormatter help = new HelpFormatter();
+
+ CommandLine cmd;
+
+ try {
+ cmd = parser.parse(options, args);
+ if (cmd.getArgs().length < 1) {
+ throw new ParseException("Need to specify paths to source files");
+ }
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ help.printHelp(
+ "java -jar <JAR> [-s <SOURCE_DIR=.>] "
+ + "[-c <CLASSES_JAR>] <CLASS_OR_PACKAGE_NAMES>\n"
+ + "Class or package names should be fully qualified.\n\n",
+ options);
+ System.exit(1);
+ throw new RuntimeException("Unreachable code");
+ }
+ return fromCommandLine(cmd);
+ }
+}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmSummarizer.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmSummarizer.java
index 9cf03d9..391b831 100644
--- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmSummarizer.java
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmSummarizer.java
@@ -7,23 +7,23 @@
import static com.github.dart_lang.jnigen.apisummarizer.util.ExceptionUtil.wrapCheckedException;
import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
-import java.io.InputStream;
+import com.github.dart_lang.jnigen.apisummarizer.util.InputStreamProvider;
import java.util.ArrayList;
import java.util.List;
-import java.util.stream.Collectors;
import org.objectweb.asm.ClassReader;
public class AsmSummarizer {
- public static List<ClassDecl> run(ArrayList<InputStream> inputs) {
- return inputs.stream()
- .map(input -> wrapCheckedException(ClassReader::new, input))
- .flatMap(
- reader -> {
- var visitor = new AsmClassVisitor();
- reader.accept(visitor, 0);
- return visitor.getVisited().stream();
- })
- .collect(Collectors.toList());
+ public static List<ClassDecl> run(List<InputStreamProvider> inputProviders) {
+ List<ClassDecl> parsed = new ArrayList<>();
+ for (var provider : inputProviders) {
+ var inputStream = provider.getInputStream();
+ var classReader = wrapCheckedException(ClassReader::new, inputStream);
+ var visitor = new AsmClassVisitor();
+ classReader.accept(visitor, 0);
+ parsed.addAll(visitor.getVisited());
+ provider.close();
+ }
+ return parsed;
}
}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/FileInputStreamProvider.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/FileInputStreamProvider.java
new file mode 100644
index 0000000..da5cadf
--- /dev/null
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/FileInputStreamProvider.java
@@ -0,0 +1,38 @@
+package com.github.dart_lang.jnigen.apisummarizer.util;
+
+import java.io.*;
+
+/** Implementation of InputStreamProvider backed by a File. */
+public class FileInputStreamProvider implements InputStreamProvider {
+ File file;
+ InputStream stream;
+
+ public FileInputStreamProvider(File file) {
+ this.file = file;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ if (stream == null) {
+ try {
+ stream = new FileInputStream(file);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return stream;
+ }
+
+ @Override
+ public void close() {
+ if (stream == null) {
+ return;
+ }
+ try {
+ stream.close();
+ stream = null;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/InputStreamProvider.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/InputStreamProvider.java
new file mode 100644
index 0000000..85ba15b
--- /dev/null
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/InputStreamProvider.java
@@ -0,0 +1,17 @@
+package com.github.dart_lang.jnigen.apisummarizer.util;
+
+import java.io.InputStream;
+
+/**
+ * Implementers of this interface provide an InputStream on-demand for writing, and provide a way to
+ * close the same. <br>
+ * The implementation doesn't need to be thread-safe, since this is only used in AsmSummarizer,
+ * which reads the classes serially.
+ */
+public interface InputStreamProvider {
+ /** Return the input stream, initializing it if needed. */
+ InputStream getInputStream();
+
+ /** close the underlying InputStream. */
+ void close();
+}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JarEntryFileObject.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JarEntryFileObject.java
new file mode 100644
index 0000000..49f187f
--- /dev/null
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JarEntryFileObject.java
@@ -0,0 +1,41 @@
+package com.github.dart_lang.jnigen.apisummarizer.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import javax.tools.SimpleJavaFileObject;
+
+/** Implements JavaFileObject for use by Doclet summarizer. */
+class JarEntryFileObject extends SimpleJavaFileObject {
+ JarFile jarFile;
+ String relativePath;
+
+ protected JarEntryFileObject(JarFile jarFile, ZipEntry entry) {
+ super(URI.create(new File(jarFile.getName()).toURI() + "/" + entry.getName()), Kind.SOURCE);
+ this.jarFile = jarFile;
+ this.relativePath = entry.getName();
+ }
+
+ private int getEntrySize(ZipEntry entry) {
+ long limit = 1024L * 1024L * 16L; // Arbitrary limit, how long can be a source file?
+ long size = entry.getSize();
+ return (int) Long.min(size, limit);
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+ var entry = jarFile.getEntry(relativePath);
+ var out = new ByteArrayOutputStream(getEntrySize(entry));
+
+ try (var stream = jarFile.getInputStream(entry)) {
+ stream.transferTo(out);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return out.toString(StandardCharsets.UTF_8);
+ }
+}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JarEntryInputStreamProvider.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JarEntryInputStreamProvider.java
new file mode 100644
index 0000000..cba3f76
--- /dev/null
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JarEntryInputStreamProvider.java
@@ -0,0 +1,29 @@
+package com.github.dart_lang.jnigen.apisummarizer.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+
+public class JarEntryInputStreamProvider implements InputStreamProvider {
+
+ private final JarFile jarFile;
+ private final ZipEntry zipEntry;
+
+ public JarEntryInputStreamProvider(JarFile jarFile, ZipEntry zipEntry) {
+ this.jarFile = jarFile;
+ this.zipEntry = zipEntry;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ try {
+ return jarFile.getInputStream(zipEntry);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close() {}
+}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/SearchUtil.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/SearchUtil.java
new file mode 100644
index 0000000..9f1611d
--- /dev/null
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/SearchUtil.java
@@ -0,0 +1,155 @@
+package com.github.dart_lang.jnigen.apisummarizer.util;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.jar.JarFile;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+
+public class SearchUtil {
+ public static Optional<List<File>> findFilesInPath(
+ String qualifiedName, String searchPath, String suffix) {
+ var s = qualifiedName.replace(".", File.separator);
+ var f = new File(searchPath, s + suffix);
+ if (f.exists() && f.isFile()) {
+ return Optional.of(List.of(f));
+ }
+
+ var d = new File(searchPath, s);
+ if (d.exists() && d.isDirectory()) {
+ return Optional.of(recursiveListFiles(d, file -> file.getName().endsWith(".java")));
+ }
+
+ return Optional.empty();
+ }
+
+ public static Optional<List<ZipEntry>> findFilesInJar(
+ String qualifiedName, JarFile jar, String suffix) {
+ String relativePath = qualifiedName.replace(".", "/");
+ var classEntry = jar.getEntry(relativePath + suffix);
+ if (classEntry != null) {
+ return Optional.of(List.of(classEntry));
+ }
+ var dirPath = relativePath.endsWith("/") ? relativePath : relativePath + "/";
+ var dirEntry = jar.getEntry(dirPath);
+ if (dirEntry != null && dirEntry.isDirectory()) {
+ var result =
+ jar.stream()
+ .map(je -> (ZipEntry) je)
+ .filter(
+ entry -> {
+ var name = entry.getName();
+ return name.endsWith(suffix) && name.startsWith(dirPath);
+ })
+ .collect(Collectors.toList());
+ return Optional.of(result);
+ }
+ return Optional.empty();
+ }
+
+ public static <T> Optional<List<T>> find(
+ String qualifiedName,
+ List<String> searchPaths,
+ String suffix,
+ Function<List<File>, List<T>> fileMapper,
+ BiFunction<JarFile, List<ZipEntry>, List<T>> entryMapper) {
+ for (var searchPath : searchPaths) {
+ File searchFile = new File(searchPath);
+ if (searchFile.isDirectory()) {
+ var result = findFilesInPath(qualifiedName, searchPath, suffix);
+ if (result.isPresent()) {
+ var mappedResult = fileMapper.apply(result.get());
+ return Optional.of(mappedResult);
+ }
+ }
+ if (searchFile.isFile() && searchPath.endsWith(".jar")) {
+ var jarFile = ExceptionUtil.wrapCheckedException(JarFile::new, searchPath);
+ var result = findFilesInJar(qualifiedName, jarFile, suffix);
+ if (result.isPresent()) {
+ var mappedResult = entryMapper.apply(jarFile, result.get());
+ return Optional.of(mappedResult);
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ private static List<JavaFileObject> getJavaFileObjectsFromFiles(
+ List<File> files, StandardJavaFileManager fm) {
+ var result = new ArrayList<JavaFileObject>();
+ fm.getJavaFileObjectsFromFiles(files).forEach(result::add);
+ return result;
+ }
+
+ private static List<JavaFileObject> getJavaFileObjectsFromJar(
+ JarFile jarFile, List<ZipEntry> entries) {
+ return StreamUtil.map(entries, (entry) -> new JarEntryFileObject(jarFile, entry));
+ }
+
+ private static List<InputStreamProvider> getInputStreamProvidersFromFiles(List<File> files) {
+ return StreamUtil.map(files, FileInputStreamProvider::new);
+ }
+
+ private static List<InputStreamProvider> getInputStreamProvidersFromJar(
+ JarFile jarFile, List<ZipEntry> entries) {
+ return StreamUtil.map(entries, entry -> new JarEntryInputStreamProvider(jarFile, entry));
+ }
+
+ public static Optional<List<JavaFileObject>> findJavaSources(
+ String qualifiedName, List<String> searchPaths, StandardJavaFileManager fm) {
+ return find(
+ qualifiedName,
+ searchPaths,
+ ".java",
+ files -> getJavaFileObjectsFromFiles(files, fm),
+ SearchUtil::getJavaFileObjectsFromJar);
+ }
+
+ public static Optional<List<InputStreamProvider>> findJavaClasses(
+ String qualifiedName, List<String> searchPaths) {
+ return find(
+ qualifiedName,
+ searchPaths,
+ ".class",
+ SearchUtil::getInputStreamProvidersFromFiles,
+ SearchUtil::getInputStreamProvidersFromJar);
+ }
+
+ /**
+ * Lists all files under given directory, which satisfy the condition of filter. <br>
+ * The order of listing will be deterministic.
+ */
+ public static List<File> recursiveListFiles(File file, FileFilter filter) {
+ if (!file.exists()) {
+ throw new RuntimeException("File not found: " + file.getPath());
+ }
+
+ if (!file.isDirectory()) {
+ return List.of(file);
+ }
+
+ // List files using a breadth-first traversal.
+ var files = new ArrayList<File>();
+ var queue = new ArrayDeque<File>();
+ queue.add(file);
+ while (!queue.isEmpty()) {
+ var dir = queue.poll();
+ var list = dir.listFiles(entry -> entry.isDirectory() || filter.accept(entry));
+ if (list == null) throw new IllegalArgumentException("File.listFiles returned null!");
+ Arrays.sort(list);
+ for (var path : list) {
+ if (path.isDirectory()) {
+ queue.add(path);
+ } else {
+ files.add(path);
+ }
+ }
+ }
+ return files;
+ }
+}
diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart
index 9c1202d..bc4deb0 100644
--- a/pkgs/jnigen/lib/src/generate_bindings.dart
+++ b/pkgs/jnigen/lib/src/generate_bindings.dart
@@ -9,7 +9,6 @@
import 'bindings/excluder.dart';
import 'bindings/linker.dart';
import 'bindings/renamer.dart';
-import 'elements/elements.dart';
import 'summary/summary.dart';
import 'config/config.dart';
import 'tools/tools.dart';
@@ -23,83 +22,8 @@
await buildSummarizerIfNotExists();
- final summarizer = SummarizerCommand(
- sourcePath: config.sourcePath,
- classPath: config.classPath,
- classes: config.classes,
- workingDirectory: config.summarizerOptions?.workingDirectory,
- extraArgs: config.summarizerOptions?.extraArgs ?? const [],
- backend: config.summarizerOptions?.backend,
- );
+ final classes = await getSummary(config);
- // Additional sources added using maven downloads and gradle trickery.
- final extraSources = <Uri>[];
- final extraJars = <Uri>[];
- final mavenDl = config.mavenDownloads;
- if (mavenDl != null) {
- final sourcePath = mavenDl.sourceDir;
- await Directory(sourcePath).create(recursive: true);
- await MavenTools.downloadMavenSources(
- MavenTools.deps(mavenDl.sourceDeps), sourcePath);
- extraSources.add(Uri.directory(sourcePath));
- final jarPath = mavenDl.jarDir;
- await Directory(jarPath).create(recursive: true);
- await MavenTools.downloadMavenJars(
- MavenTools.deps(mavenDl.sourceDeps + mavenDl.jarOnlyDeps), jarPath);
- extraJars.addAll(await Directory(jarPath)
- .list()
- .where((entry) => entry.path.endsWith('.jar'))
- .map((entry) => entry.uri)
- .toList());
- }
- final androidConfig = config.androidSdkConfig;
- if (androidConfig != null && androidConfig.addGradleDeps) {
- final deps = AndroidSdkTools.getGradleClasspaths(
- configRoot: config.configRoot,
- androidProject: androidConfig.androidExample ?? '.',
- );
- extraJars.addAll(deps.map(Uri.file));
- }
- if (androidConfig != null && androidConfig.versions != null) {
- final versions = androidConfig.versions!;
- final androidSdkRoot =
- androidConfig.sdkRoot ?? AndroidSdkTools.getAndroidSdkRoot();
- final androidJar = await AndroidSdkTools.getAndroidJarPath(
- sdkRoot: androidSdkRoot, versionOrder: versions);
- if (androidJar != null) {
- extraJars.add(Uri.directory(androidJar));
- }
- }
-
- summarizer.addSourcePaths(extraSources);
- summarizer.addClassPaths(extraJars);
-
- Process process;
- Stream<List<int>> input;
- try {
- process = await summarizer.runProcess();
- input = process.stdout;
- } on Exception catch (e) {
- log.fatal('Cannot obtain API summary: $e');
- return;
- }
- final errorLog = StringBuffer();
- collectOutputStream(process.stderr, errorLog);
- final stream = const JsonDecoder().bind(const Utf8Decoder().bind(input));
- dynamic json;
- try {
- json = await stream.single;
- } on Exception catch (e) {
- printError(errorLog);
- log.fatal('Cannot parse summary: $e');
- return;
- }
- if (json == null) {
- log.fatal('Expected JSON element from summarizer.');
- return;
- }
- final list = json as List;
- final classes = Classes.fromJson(list);
final cBased = config.outputConfig.bindingsType == BindingsType.cBased;
classes
..accept(Excluder(config))
diff --git a/pkgs/jnigen/lib/src/logging/logging.dart b/pkgs/jnigen/lib/src/logging/logging.dart
index 561b06b..67c605e 100644
--- a/pkgs/jnigen/lib/src/logging/logging.dart
+++ b/pkgs/jnigen/lib/src/logging/logging.dart
@@ -49,9 +49,9 @@
}
extension FatalErrors on Logger {
- void fatal(Object? message, {int exitCode = 1}) {
+ Never fatal(Object? message, {int exitCode = 1}) {
message = _colorize('Fatal: $message', _ansiRed);
stderr.writeln(message);
- exit(exitCode);
+ return exit(exitCode);
}
}
diff --git a/pkgs/jnigen/lib/src/summary/summary.dart b/pkgs/jnigen/lib/src/summary/summary.dart
index 4580dbe..29e50b7 100644
--- a/pkgs/jnigen/lib/src/summary/summary.dart
+++ b/pkgs/jnigen/lib/src/summary/summary.dart
@@ -3,8 +3,13 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
+import '../../tools.dart';
+import '../config/config.dart';
+import '../elements/elements.dart';
+import '../generate_bindings.dart';
import '../logging/logging.dart';
/// A command based summary source which calls the ApiSummarizer command.
@@ -86,3 +91,82 @@
return proc;
}
}
+
+Future<Classes> getSummary(Config config) async {
+ setLoggingLevel(config.logLevel);
+ final summarizer = SummarizerCommand(
+ sourcePath: config.sourcePath,
+ classPath: config.classPath,
+ classes: config.classes,
+ workingDirectory: config.summarizerOptions?.workingDirectory,
+ extraArgs: config.summarizerOptions?.extraArgs ?? const [],
+ backend: config.summarizerOptions?.backend,
+ );
+
+ // Additional sources added using maven downloads and gradle trickery.
+ final extraSources = <Uri>[];
+ final extraJars = <Uri>[];
+ final mavenDl = config.mavenDownloads;
+ if (mavenDl != null) {
+ final sourcePath = mavenDl.sourceDir;
+ await Directory(sourcePath).create(recursive: true);
+ await MavenTools.downloadMavenSources(
+ MavenTools.deps(mavenDl.sourceDeps), sourcePath);
+ extraSources.add(Uri.directory(sourcePath));
+ final jarPath = mavenDl.jarDir;
+ await Directory(jarPath).create(recursive: true);
+ await MavenTools.downloadMavenJars(
+ MavenTools.deps(mavenDl.sourceDeps + mavenDl.jarOnlyDeps), jarPath);
+ extraJars.addAll(await Directory(jarPath)
+ .list()
+ .where((entry) => entry.path.endsWith('.jar'))
+ .map((entry) => entry.uri)
+ .toList());
+ }
+ final androidConfig = config.androidSdkConfig;
+ if (androidConfig != null && androidConfig.addGradleDeps) {
+ final deps = AndroidSdkTools.getGradleClasspaths(
+ configRoot: config.configRoot,
+ androidProject: androidConfig.androidExample ?? '.',
+ );
+ extraJars.addAll(deps.map(Uri.file));
+ }
+ if (androidConfig != null && androidConfig.versions != null) {
+ final versions = androidConfig.versions!;
+ final androidSdkRoot =
+ androidConfig.sdkRoot ?? AndroidSdkTools.getAndroidSdkRoot();
+ final androidJar = await AndroidSdkTools.getAndroidJarPath(
+ sdkRoot: androidSdkRoot, versionOrder: versions);
+ if (androidJar != null) {
+ extraJars.add(Uri.directory(androidJar));
+ }
+ }
+
+ summarizer.addSourcePaths(extraSources);
+ summarizer.addClassPaths(extraJars);
+
+ Process process;
+ Stream<List<int>> input;
+ try {
+ process = await summarizer.runProcess();
+ input = process.stdout;
+ } on Exception catch (e) {
+ log.fatal('Cannot obtain API summary: $e');
+ }
+ final errorLog = StringBuffer();
+ collectOutputStream(process.stderr, errorLog);
+ final stream = const JsonDecoder().bind(const Utf8Decoder().bind(input));
+ dynamic json;
+ try {
+ json = await stream.single;
+ } on Exception catch (e) {
+ printError(errorLog);
+ log.fatal('Cannot parse summary: $e');
+ }
+ if (json == null) {
+ log.fatal('Expected JSON element from summarizer.');
+ }
+ final list = json as List;
+ final classes = Classes.fromJson(list);
+ return classes;
+}
diff --git a/pkgs/jnigen/lib/src/tools/build_summarizer.dart b/pkgs/jnigen/lib/src/tools/build_summarizer.dart
index 2647bc8..30ce426 100644
--- a/pkgs/jnigen/lib/src/tools/build_summarizer.dart
+++ b/pkgs/jnigen/lib/src/tools/build_summarizer.dart
@@ -25,7 +25,6 @@
final pkg = await findPackageRoot('jnigen');
if (pkg == null) {
log.fatal('package jnigen not found!');
- return;
}
final pom = pkg.resolve('java/pom.xml');
await Directory(toolPath).create(recursive: true);
diff --git a/pkgs/jnigen/test/summary_generation_test.dart b/pkgs/jnigen/test/summary_generation_test.dart
new file mode 100644
index 0000000..6bf7457
--- /dev/null
+++ b/pkgs/jnigen/test/summary_generation_test.dart
@@ -0,0 +1,168 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// These tests validate summary generation in various scenarios.
+// Currently, no validation of the summary content itself is done.
+
+import 'dart:io';
+
+import 'package:jnigen/src/config/config.dart';
+import 'package:jnigen/src/elements/elements.dart';
+import 'package:jnigen/src/logging/logging.dart';
+import 'package:jnigen/src/summary/summary.dart';
+import 'package:logging/logging.dart';
+
+import 'package:path/path.dart' hide equals;
+import 'package:test/test.dart';
+
+import 'test_util/test_util.dart';
+
+void expectNonEmptySummary(Classes? classes) {
+ expect(classes, isNotNull);
+ final decls = classes!.decls;
+ expect(decls.entries.length, greaterThanOrEqualTo(javaFiles.length));
+ final declNames = decls.keys.toSet();
+ final expectedClasses =
+ javaClasses.where((name) => !name.contains("annotations.")).toList();
+ expect(declNames, containsAll(expectedClasses));
+}
+
+void deleteTempDir(Directory directory) {
+ try {
+ if (Platform.isWindows) {
+ // This appears to avoid "file used by another process" errors.
+ sleep(const Duration(seconds: 1));
+ }
+ directory.deleteSync(recursive: true);
+ } on FileSystemException catch (e) {
+ log.warning("Cannot delete directory: $e");
+ }
+}
+
+List<String> findFiles(Directory dir, String suffix) {
+ return dir
+ .listSync(recursive: true)
+ .map((entry) => relative(entry.path, from: dir.path))
+ .where((path) => path.endsWith(suffix))
+ .toList();
+}
+
+Future<void> createJar(List<String> paths, String target) async {
+ final relativeTarget = relative(target, from: simplePackagePath);
+ final status = await runCommand(
+ 'jar',
+ ['cf', relativeTarget, ...paths],
+ workingDirectory: simplePackagePath,
+ );
+ if (status != 0) {
+ throw ArgumentError('Cannot create JAR from provided arguments');
+ }
+}
+
+Future<void> compileJavaFiles(List<String> paths, Directory target) async {
+ final status = await runCommand(
+ 'javac',
+ ['-d', target.absolute.path, ...paths],
+ workingDirectory: simplePackagePath,
+ );
+ if (status != 0) {
+ throw ArgumentError('Cannot compile Java sources');
+ }
+}
+
+String getClassNameFromPath(String path) {
+ if (!path.endsWith('.java')) {
+ throw ArgumentError('Filename must end with java');
+ }
+ return path
+ .replaceAll('/', '.')
+ .replaceAll('\\', '.')
+ .substring(0, path.length - 5);
+}
+
+final simplePackagePath = join('test', 'simple_package_test', 'java');
+final simplePackageDir = Directory(simplePackagePath);
+final javaFiles = findFiles(simplePackageDir, '.java');
+final javaClasses = javaFiles.map(getClassNameFromPath).toList();
+
+Config getConfig({List<String>? sourcePath, List<String>? classPath}) {
+ return Config(
+ outputConfig: OutputConfig(
+ bindingsType: BindingsType.dartOnly,
+ dartConfig: DartCodeOutputConfig(
+ path: Uri.file('unused.dart'),
+ structure: OutputStructure.singleFile,
+ ),
+ ),
+ classes: javaClasses,
+ sourcePath: sourcePath?.map((e) => Uri.file(e)).toList(),
+ classPath: classPath?.map((e) => Uri.file(e)).toList(),
+ logLevel: Level.WARNING,
+ );
+}
+
+void main() {
+ late Directory tempDir;
+ setUpAll(() {
+ tempDir = getTempDir("jnigen_summary_tests_");
+ });
+
+ test('Test summary generation from compiled JAR', () async {
+ final targetDir = tempDir.createTempSync("compiled_jar_test_");
+ await compileJavaFiles(javaFiles, targetDir);
+ final classFiles = findFiles(targetDir, '.class');
+ final jarFilePath = join(targetDir.absolute.path, 'classes.jar');
+ await createJar(classFiles, jarFilePath);
+ final config = getConfig(classPath: [jarFilePath]);
+ final summaryClasses = await getSummary(config);
+ expectNonEmptySummary(summaryClasses);
+ });
+
+ test('Test summary generation from source JAR', () async {
+ final targetDir = tempDir.createTempSync("source_jar_test_");
+ final jarFilePath = join(targetDir.path, 'sources.jar');
+ await createJar(javaFiles, jarFilePath);
+ final config = getConfig(sourcePath: [jarFilePath]);
+ final summaryClasses = await getSummary(config);
+ expectNonEmptySummary(summaryClasses);
+ });
+
+ test('Test summary generation from source folder', () async {
+ final config = getConfig(sourcePath: [simplePackagePath]);
+ final summaryClasses = await getSummary(config);
+ expectNonEmptySummary(summaryClasses);
+ });
+
+ test('Test summary generation from compiled classes in directory', () async {
+ final targetDir = tempDir.createTempSync("compiled_classes_test_");
+ await compileJavaFiles(javaFiles, targetDir);
+ final config = getConfig(classPath: [targetDir.path]);
+ final summaryClasses = await getSummary(config);
+ expectNonEmptySummary(summaryClasses);
+ });
+
+ // Test summary generation from combination of a source and class path
+ test('Test summary generation from combination', () async {
+ final targetDir = tempDir.createTempSync("combination_test_");
+
+ // remove a class from source files and create a source JAR
+ final sourceFiles = javaFiles.toList();
+ sourceFiles.removeLast();
+ final sourceJarPath = join(targetDir.path, 'sources.jar');
+ await createJar(sourceFiles, sourceJarPath);
+
+ await compileJavaFiles(javaFiles, targetDir);
+ final classFiles = findFiles(targetDir, '.class');
+ final classesJarPath = join(targetDir.path, 'classes.jar');
+ await createJar(classFiles, classesJarPath);
+ final config = getConfig(
+ classPath: [classesJarPath],
+ sourcePath: [sourceJarPath],
+ );
+ final summaryClasses = await getSummary(config);
+ expectNonEmptySummary(summaryClasses);
+ });
+
+ tearDownAll(() => deleteTempDir(tempDir));
+}
diff --git a/pkgs/jnigen/test/test_util/test_util.dart b/pkgs/jnigen/test/test_util/test_util.dart
index 5169684..e48fd0a 100644
--- a/pkgs/jnigen/test/test_util/test_util.dart
+++ b/pkgs/jnigen/test/test_util/test_util.dart
@@ -11,6 +11,12 @@
import 'package:jnigen/src/logging/logging.dart' show printError;
+final _currentDirectory = Directory(".");
+
+Directory getTempDir(String prefix) {
+ return _currentDirectory.createTempSync(prefix);
+}
+
Future<bool> isEmptyOrNotExistDir(String path) async {
final dir = Directory(path);
return (!await dir.exists()) || (await dir.list().length == 0);