| // 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. | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:jni_util/jni_util.dart'; | |
| import '../../tools.dart'; | |
| import '../config/config.dart'; | |
| import '../elements/elements.dart'; | |
| import '../generate_bindings.dart'; | |
| import '../logging/logging.dart'; | |
| class SummaryParseException implements Exception { | |
| final String? stderr; | |
| final String message; | |
| SummaryParseException(this.message) : stderr = null; | |
| SummaryParseException.withStderr(this.stderr, this.message); | |
| @override | |
| String toString() => message; | |
| } | |
| final _unsupportedClassFileVersionRegex = RegExp( | |
| r'Unsupported class file major version\s+(\d+)', | |
| ); | |
| String? getActionableSummaryParseMessage(String stderr) { | |
| final match = _unsupportedClassFileVersionRegex.firstMatch(stderr); | |
| if (match == null) { | |
| return null; | |
| } | |
| final majorVersion = match.group(1); | |
| return 'Cannot generate summary: Java class file version $majorVersion is ' | |
| 'not supported by the summarizer. This usually means your input classes ' | |
| 'were compiled with a newer JDK target than JNIgen supports. Use a ' | |
| 'supported JDK version ($minJavaVersion to $maxJavaVersion), or ' | |
| 'recompile your Java inputs with a lower target (for example: javac ' | |
| '--release $minJavaVersion <your-java-files>).'; | |
| } | |
| /// A command based summary source which calls the ApiSummarizer command. | |
| /// [sourcePaths] and [classPaths] can be provided for the summarizer to find | |
| /// required dependencies. The [classes] argument specifies the fully qualified | |
| /// names of classes or packages included in the generated summary. when a | |
| /// package is specified, its contents are included recursively. | |
| /// | |
| /// When the default summarizer scans the [sourcePaths], it assumes that | |
| /// the directory names reflect actual package paths. For example, a class name | |
| /// com.example.pkg.Cls will be mapped to com/example/pkg/Cls.java. | |
| /// | |
| /// The default summarizer needs to be built with `jnigen:setup` | |
| /// script before this API is used. | |
| class SummarizerCommand { | |
| SummarizerCommand({ | |
| required this.command, | |
| List<Uri>? sourcePath, | |
| List<Uri>? classPath, | |
| this.extraArgs = const [], | |
| required this.classes, | |
| Uri? workingDirectory, | |
| this.backend, | |
| }) : sourcePaths = [...?sourcePath], | |
| classPaths = [...?classPath], | |
| workingDirectory = workingDirectory ?? Uri.directory('.'); | |
| static const sourcePathsOption = '-s'; | |
| static const classPathsOption = '-c'; | |
| String command; | |
| List<Uri> sourcePaths, classPaths; | |
| List<String> extraArgs; | |
| List<String> classes; | |
| Uri workingDirectory; | |
| SummarizerBackend? backend; | |
| void addSourcePaths(List<Uri> paths) { | |
| sourcePaths.addAll(paths); | |
| } | |
| void addClassPaths(List<Uri> paths) { | |
| classPaths.addAll(paths); | |
| } | |
| void _addPathParam(List<String> args, String option, List<Uri> paths) { | |
| if (paths.isNotEmpty) { | |
| final joined = paths | |
| .map((uri) => uri.toFilePath()) | |
| .join(Platform.isWindows ? ';' : ':'); | |
| args.addAll([option, '"$joined"']); | |
| } | |
| } | |
| Future<Process> runProcess() async { | |
| final commandSplit = command.split(' '); | |
| final exec = commandSplit[0]; | |
| final args = commandSplit.sublist(1); | |
| _addPathParam(args, sourcePathsOption, sourcePaths); | |
| _addPathParam(args, classPathsOption, classPaths); | |
| if (backend != null) { | |
| args.addAll(['--backend', backend!.name]); | |
| } | |
| args.addAll(extraArgs); | |
| args.addAll(classes); | |
| final resolvedExec = resolveJavaExecutable(exec); | |
| log.info('execute $resolvedExec ${args.join(' ')}'); | |
| final proc = await Process.start( | |
| resolvedExec, | |
| args, | |
| workingDirectory: workingDirectory.toFilePath(), | |
| environment: { | |
| ...javaEnvironment, | |
| 'JAVA_TOOL_OPTIONS': '-Dfile.encoding=UTF8', | |
| }, | |
| ); | |
| return proc; | |
| } | |
| } | |
| Future<Classes> getSummary(JniGenerator config) async { | |
| final summarizer = SummarizerCommand( | |
| command: config.input.summarizerCommand ?? | |
| 'java -jar .dart_tool/jnigen/ApiSummarizer.jar', | |
| sourcePath: config.input.sourcePath, | |
| classPath: config.input.classPath, | |
| classes: config.input.classes, | |
| workingDirectory: config.input.workingDirectory, | |
| extraArgs: config.input.extraArgs, | |
| backend: config.input.backend, | |
| ); | |
| // Additional sources added using maven downloads and gradle trickery. | |
| final extraSources = <Uri>[]; | |
| final extraJars = <Uri>[]; | |
| final mavenDl = config.input.mavenDownloads; | |
| if (mavenDl != null) { | |
| final sourcePath = mavenDl.sourceDir; | |
| await Directory.fromUri(sourcePath).create(recursive: true); | |
| await GradleTools.downloadMavenSources( | |
| GradleTools.deps(mavenDl.sourceDeps), sourcePath); | |
| extraSources.add(sourcePath); | |
| final jarPath = mavenDl.jarDir; | |
| await Directory.fromUri(jarPath).create(recursive: true); | |
| await GradleTools.downloadMavenJars( | |
| GradleTools.deps(mavenDl.sourceDeps + mavenDl.jarOnlyDeps), jarPath); | |
| extraJars.addAll(await Directory.fromUri(jarPath) | |
| .list() | |
| .where((entry) => entry.path.endsWith('.jar')) | |
| .map((entry) => entry.uri) | |
| .toList()); | |
| } | |
| final androidConfig = config.input.androidSdk; | |
| if (androidConfig != null && androidConfig.addGradleDeps) { | |
| final deps = AndroidSdkTools.getGradleClasspaths( | |
| androidProject: androidConfig.androidExample, | |
| ); | |
| extraJars.addAll(deps.map(Uri.file)); | |
| } | |
| if (androidConfig != null && androidConfig.addGradleSources) { | |
| final deps = AndroidSdkTools.getGradleSources( | |
| androidProject: androidConfig.androidExample, | |
| ); | |
| extraSources.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(androidJar); | |
| } | |
| } | |
| summarizer.addSourcePaths(extraSources); | |
| summarizer.addClassPaths(extraJars); | |
| Process process; | |
| Stream<List<int>> input; | |
| final stopwatch = Stopwatch()..start(); | |
| try { | |
| process = await summarizer.runProcess(); | |
| input = process.stdout; | |
| } on Exception catch (e) { | |
| throw SummaryParseException('Cannot generate API summary: $e'); | |
| } | |
| final stderrBuffer = StringBuffer(); | |
| collectOutputStream(process.stderr, stderrBuffer); | |
| final stream = const JsonDecoder().bind(const Utf8Decoder().bind(input)); | |
| dynamic json; | |
| try { | |
| json = await stream.single; | |
| stopwatch.stop(); | |
| log.info('Parsing inputs took ${stopwatch.elapsedMilliseconds} ms'); | |
| } on Exception catch (e) { | |
| await process.exitCode; | |
| final stderr = stderrBuffer.toString(); | |
| final message = getActionableSummaryParseMessage(stderr) ?? | |
| 'Cannot generate summary: $e'; | |
| throw SummaryParseException.withStderr( | |
| stderr, | |
| message, | |
| ); | |
| } finally { | |
| log.writeSectionToFile('summarizer logs', stderrBuffer.toString()); | |
| } | |
| if (json == null) { | |
| throw SummaryParseException('Expected JSON element from summarizer.'); | |
| } | |
| final classes = Classes.fromJson(json as List<dynamic>); | |
| return classes; | |
| } |