Skip to content

Commit bfb6f37

Browse files
authored
[jnigen] General cleanup (#80)
1 parent 4af881e commit bfb6f37

File tree

73 files changed

+4601
-3499
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+4601
-3499
lines changed

.github/workflows/test-package.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,18 @@ jobs:
121121
with:
122122
distribution: 'zulu'
123123
java-version: '11'
124+
- name: install clang tools
125+
run: |
126+
sudo apt-get update -y
127+
sudo apt-get install -y clang-format
124128
- run: flutter pub get
125129
- name: Check formatting
126130
run: flutter format --output=none --set-exit-if-changed .
127131
- name: Run lints
128132
run: flutter analyze --fatal-infos
133+
- name: Check C code formatting using clang-format
134+
run: clang-format --dry-run -Werror dartjni.c dartjni.h third_party/*.c third_party/*.h
135+
working-directory: pkg/jni/src
129136

130137
test_jni:
131138
runs-on: ubuntu-latest
@@ -192,7 +199,9 @@ jobs:
192199
- run: Add-Content $env:GITHUB_PATH "$env:JAVA_HOME\bin\server"
193200
- run: dart pub get
194201
- run: dart run jni:setup
195-
- run: dart test
202+
## TODO(#51): -j 1 is required to avoid a rare race condition which is caused by
203+
## all tests which spawn JVM being run in same process.
204+
- run: dart test -j 1
196205

197206
test_jnigen_windows_minimal:
198207
needs: [analyze_jnigen]
@@ -362,7 +371,7 @@ jobs:
362371
diff -qr _c src/
363372
diff -qr _dart lib/third_party
364373
- name: Generate full bindings
365-
run: dart run jnigen --config jnigen_full.yaml
374+
run: dart run jnigen --config jnigen.yaml --override classes="org.apache.pdfbox.pdmodel;org.apache.pdfbox.text"
366375
- name: Analyze generated bindings
367376
run: |
368377
flutter pub get # dart-analyze errors on flutter example

pkgs/jni/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This is intended for one-off / debugging uses of JNI, as well as providing a bas
1717
__To generate type-safe bindings from Java libraries, use `jnigen`.__
1818

1919
## SDK Note
20-
Dart standalone is supported, but due to some current limitations of the `pubspec` format, `dart` command must be from Flutter SDK and not dart SDK. See dart-lang/pub#3563.
20+
Dart standalone is supported, but due to some current limitations of the `pubspec` format, `dart` command must be from Flutter SDK and not dart SDK.
2121

2222
## Version note
2323
This library is at an early stage of development and we do not provide backwards compatibility of the API at this point.

pkgs/jni/android/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This folder contains the [Android plugin class](src/main/java/com/github/dart_lang/jni/JniPlugin.java) which takes care of initializing JNI on Android.

pkgs/jni/bin/setup.dart

Lines changed: 136 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -7,111 +7,141 @@ import 'dart:io';
77
import 'package:args/args.dart';
88
import 'package:package_config/package_config.dart';
99

10-
const _buildDir = "build-dir";
11-
const _srcDir = "source-dir";
10+
const ansiRed = '\x1b[31m';
11+
const ansiDefault = '\x1b[39;49m';
12+
13+
const _defaultRelativeBuildPath = "build/jni_libs";
14+
15+
const _buildPath = "build-path";
16+
const _srcPath = "source-path";
1217
const _packageName = 'package-name';
1318
const _verbose = "verbose";
1419
const _cmakeArgs = "cmake-args";
15-
const _clean = "clean";
16-
17-
const _cmakeTemporaryFiles = [
18-
'CMakeCache.txt',
19-
'CMakeFiles/',
20-
'cmake_install.cmake',
21-
'Makefile'
22-
];
23-
24-
void deleteCMakeTemps(Uri buildDir) async {
25-
for (var filename in _cmakeTemporaryFiles) {
26-
if (options.verbose) {
27-
stderr.writeln('remove $filename');
28-
}
29-
await File(buildDir.resolve(filename).toFilePath()).delete(recursive: true);
20+
21+
Future<void> runCommand(
22+
String exec, List<String> args, String workingDir) async {
23+
// For printing relative path always.
24+
var current = Directory.current.path;
25+
if (!current.endsWith(Platform.pathSeparator)) {
26+
current += Platform.pathSeparator;
27+
}
28+
if (workingDir.startsWith(current)) {
29+
workingDir.replaceFirst(current, "");
3030
}
31-
}
3231

33-
// Sets up input output channels and maintains state.
34-
class CommandRunner {
35-
CommandRunner({this.printCmds = false});
36-
bool printCmds = false;
37-
int? time;
38-
Future<CommandRunner> run(
39-
String exec, List<String> args, String workingDir) async {
40-
if (printCmds) {
41-
final cmd = "$exec ${args.join(" ")}";
42-
stderr.writeln("\n+ [$workingDir] $cmd");
43-
}
44-
final process = await Process.start(exec, args,
45-
workingDirectory: workingDir,
46-
runInShell: true,
47-
mode: ProcessStartMode.inheritStdio);
48-
final exitCode = await process.exitCode;
32+
final cmd = "$exec ${args.join(" ")}";
33+
stderr.writeln("+ [$workingDir] $cmd");
34+
int exitCode;
35+
if (options.verbose) {
36+
final process = await Process.start(
37+
exec, args,
38+
workingDirectory: workingDir,
39+
mode: ProcessStartMode.inheritStdio,
40+
// without `runInShell`, sometimes cmake doesn't run on windows.
41+
runInShell: true,
42+
);
43+
exitCode = await process.exitCode;
44+
} else {
45+
// ProcessStartMode.normal sometimes hangs on windows. No idea why.
46+
final process = await Process.run(exec, args,
47+
runInShell: true, workingDirectory: workingDir);
48+
exitCode = process.exitCode;
4949
if (exitCode != 0) {
50-
stderr.writeln("command exited with $exitCode");
50+
var out = process.stdout;
51+
var err = process.stderr;
52+
if (stdout.supportsAnsiEscapes) {
53+
out = "$ansiRed$out$ansiDefault";
54+
err = "$ansiRed$err$ansiDefault";
55+
}
56+
stdout.writeln(out);
57+
stderr.writeln(err);
5158
}
52-
return this;
59+
}
60+
if (exitCode != 0) {
61+
stderr.writeln("Command exited with $exitCode.");
5362
}
5463
}
5564

5665
class Options {
5766
Options(ArgResults arg)
58-
: buildDir = arg[_buildDir],
59-
srcDir = arg[_srcDir],
67+
: buildPath = arg[_buildPath],
68+
srcPath = arg[_srcPath],
6069
packageName = arg[_packageName] ?? 'jni',
6170
cmakeArgs = arg[_cmakeArgs],
62-
verbose = arg[_verbose] ?? false,
63-
clean = arg[_clean] ?? false;
71+
verbose = arg[_verbose] ?? false;
6472

65-
String? buildDir, srcDir;
73+
String? buildPath, srcPath;
6674
String packageName;
6775
List<String> cmakeArgs;
68-
bool verbose, clean;
76+
bool verbose;
6977
}
7078

7179
late Options options;
72-
void log(String msg) {
80+
81+
void verboseLog(String msg) {
7382
if (options.verbose) {
7483
stderr.writeln(msg);
7584
}
7685
}
7786

78-
/// tries to find package:jni's source folder in pub cache
79-
/// if not possible, returns null.
80-
Future<String?> findSources() async {
87+
/// Find path to C sources in pub cache for package specified by [packageName].
88+
///
89+
/// It's assumed C FFI sources are in "src/" relative to package root.
90+
/// If package cannot be found, null is returned.
91+
Future<String?> findSources(String packageName) async {
8192
final packageConfig = await findPackageConfig(Directory.current);
8293
if (packageConfig == null) {
8394
return null;
8495
}
85-
final packages = packageConfig.packages;
86-
for (var package in packages) {
87-
if (package.name == options.packageName) {
88-
return package.root.resolve("src/").toFilePath();
96+
final package = packageConfig[options.packageName];
97+
if (package == null) {
98+
return null;
99+
}
100+
return package.root.resolve("src").toFilePath();
101+
}
102+
103+
/// Returns true if [artifact] does not exist, or any file in [sourceDir] is
104+
/// newer than [artifact].
105+
bool needsBuild(File artifact, Directory sourceDir) {
106+
if (!artifact.existsSync()) return true;
107+
final fileLastModified = artifact.lastModifiedSync();
108+
for (final entry in sourceDir.listSync(recursive: true)) {
109+
if (entry.statSync().modified.isAfter(fileLastModified)) {
110+
return true;
89111
}
90112
}
91-
return null;
113+
return false;
114+
}
115+
116+
/// Returns the name of file built using sources in [cDir]
117+
String getTargetName(Directory cDir) {
118+
for (final file in cDir.listSync(recursive: true)) {
119+
if (file.path.endsWith(".c")) {
120+
final cFileName = file.uri.pathSegments.last;
121+
final librarySuffix = Platform.isWindows ? "dll" : "so";
122+
return cFileName.substring(0, cFileName.length - 1) + librarySuffix;
123+
}
124+
}
125+
throw Exception("Could not find a C file in ${cDir.path}");
92126
}
93127

94128
void main(List<String> arguments) async {
95129
final parser = ArgParser()
96-
..addOption(_buildDir,
97-
abbr: 'B', help: 'Directory to place built artifacts')
98-
..addOption(_srcDir,
99-
abbr: 'S', help: 'alternative path to package:jni sources')
130+
..addOption(_buildPath,
131+
abbr: 'b', help: 'Directory to place built artifacts')
132+
..addOption(_srcPath,
133+
abbr: 's', help: 'alternative path to package:jni sources')
100134
..addOption(_packageName,
101135
abbr: 'p',
102136
help: 'package for which native'
103137
'library should be built',
104138
defaultsTo: 'jni')
105139
..addFlag(_verbose, abbr: 'v', help: 'Enable verbose output')
106-
..addFlag(_clean,
107-
negatable: false,
108-
abbr: 'C',
109-
help: 'Clear built artifacts instead of running a build')
110140
..addMultiOption(_cmakeArgs,
111-
abbr: 'm', help: 'additional argument to pass to CMake');
112-
final cli = parser.parse(arguments);
113-
options = Options(cli);
114-
final rest = cli.rest;
141+
abbr: 'm', help: 'Pass additional argument to CMake');
142+
final argResults = parser.parse(arguments);
143+
options = Options(argResults);
144+
final rest = argResults.rest;
115145

116146
if (rest.isNotEmpty) {
117147
stderr.writeln("one or more unrecognized arguments: $rest");
@@ -121,79 +151,69 @@ void main(List<String> arguments) async {
121151
return;
122152
}
123153

124-
final srcPath = options.srcDir ?? await findSources();
154+
final srcPath = options.srcPath ?? await findSources(options.packageName);
125155

126156
if (srcPath == null) {
127-
stderr.writeln("No sources specified and current directory is not a "
128-
"package root.");
157+
stderr.writeln("Cannot find sources for package ${options.packageName} "
158+
"and no sources were manually specified.");
129159
exitCode = 1;
130160
return;
131161
}
132162

133163
final srcDir = Directory(srcPath);
134-
if (!await srcDir.exists() && !options.clean) {
135-
throw 'Directory $srcPath does not exist';
164+
if (!srcDir.existsSync()) {
165+
stderr.writeln('Directory $srcPath does not exist');
166+
exitCode = 1;
167+
return;
136168
}
137169

138-
log("srcPath: $srcPath");
170+
verboseLog("srcPath: $srcPath");
139171

140-
final currentDirUri = Uri.file(".");
141-
final buildPath =
142-
options.buildDir ?? currentDirUri.resolve("build/jni_libs").toFilePath();
172+
final currentDirUri = Uri.directory(".");
173+
final buildPath = options.buildPath ??
174+
currentDirUri.resolve(_defaultRelativeBuildPath).toFilePath();
143175
final buildDir = Directory(buildPath);
144176
await buildDir.create(recursive: true);
145-
log("buildPath: $buildPath");
177+
verboseLog("buildPath: $buildPath");
146178

147179
if (buildDir.absolute.uri == srcDir.absolute.uri) {
148180
stderr.writeln("Please build in a directory different than source.");
149-
exit(2);
181+
exitCode = 1;
182+
return;
150183
}
151184

152-
if (options.clean) {
153-
await cleanup(options, srcDir.absolute.path, buildDir.absolute.path);
154-
} else {
155-
// pass srcDir absolute path because it will be passed to CMake as arg
156-
// which will be running in different directory
157-
final jniDirUri = Uri.directory(".dart_tool").resolve("jni");
158-
final jniDir = Directory.fromUri(jniDirUri);
159-
await jniDir.create(recursive: true);
160-
final tempDir = await jniDir.createTemp("jni_native_build_");
161-
await build(options, srcDir.absolute.path, tempDir.path);
162-
final dllDirUri =
163-
Platform.isWindows ? tempDir.uri.resolve("Debug") : tempDir.uri;
164-
final dllDir = Directory.fromUri(dllDirUri);
165-
for (var entry in dllDir.listSync()) {
166-
final dllSuffix = Platform.isWindows ? "dll" : "so";
167-
if (entry.path.endsWith(dllSuffix)) {
168-
final dllName = entry.uri.pathSegments.last;
169-
final target = buildDir.uri.resolve(dllName);
170-
entry.renameSync(target.toFilePath());
171-
}
172-
}
173-
await tempDir.delete(recursive: true);
185+
final targetFileUri = buildDir.uri.resolve(getTargetName(srcDir));
186+
final targetFile = File.fromUri(targetFileUri);
187+
if (!needsBuild(targetFile, srcDir)) {
188+
verboseLog("last modified of ${targetFile.path}: "
189+
"${targetFile.lastModifiedSync()}");
190+
stderr.writeln("target newer than source, skipping build");
191+
return;
174192
}
175-
}
176193

177-
Future<void> build(Options options, String srcPath, String buildPath) async {
178-
final runner = CommandRunner(printCmds: true);
194+
// Note: creating temp dir in .dart_tool/jni instead of SystemTemp
195+
// because latter can fail tests on Windows CI, when system temp is on
196+
// separate drive or something.
197+
final jniDirUri = Uri.directory(".dart_tool").resolve("jni");
198+
final jniDir = Directory.fromUri(jniDirUri);
199+
await jniDir.create(recursive: true);
200+
final tempDir = await jniDir.createTemp("jni_native_build_");
179201
final cmakeArgs = <String>[];
180202
cmakeArgs.addAll(options.cmakeArgs);
181-
cmakeArgs.add(srcPath);
182-
await runner.run("cmake", cmakeArgs, buildPath);
183-
await runner.run("cmake", ["--build", "."], buildPath);
184-
}
185-
186-
Future<void> cleanup(Options options, String srcPath, String buildPath) async {
187-
if (srcPath == buildPath) {
188-
stderr.writeln('Error: build path is same as source path.');
189-
}
190-
191-
stderr.writeln("deleting $buildPath");
192-
193-
try {
194-
await Directory(buildPath).delete(recursive: true);
195-
} catch (e) {
196-
stderr.writeln("Error: cannot be deleted");
197-
stderr.writeln(e);
203+
// Pass absolute path of srcDir because cmake command is run in temp dir
204+
cmakeArgs.add(srcDir.absolute.path);
205+
await runCommand("cmake", cmakeArgs, tempDir.path);
206+
await runCommand("cmake", ["--build", "."], tempDir.path);
207+
final dllDirUri =
208+
Platform.isWindows ? tempDir.uri.resolve("Debug") : tempDir.uri;
209+
final dllDir = Directory.fromUri(dllDirUri);
210+
for (var entry in dllDir.listSync()) {
211+
final dllSuffix = Platform.isWindows ? "dll" : "so";
212+
if (entry.path.endsWith(dllSuffix)) {
213+
final dllName = entry.uri.pathSegments.last;
214+
final target = buildDir.uri.resolve(dllName);
215+
entry.renameSync(target.toFilePath());
216+
}
198217
}
218+
await tempDir.delete(recursive: true);
199219
}

0 commit comments

Comments
 (0)