| // 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:io'; | |
| import 'package:args/args.dart'; | |
| import 'package:jni/src/build_util/build_util.dart'; | |
| import 'package:jni_util/jni_util.dart' as jni_util; | |
| import 'package:package_config/package_config.dart'; | |
| const jniNativeBuildDirective = | |
| '# jni_native_build (Build with jni:setup. Do not delete this line.)'; | |
| // When changing this constant here, also change corresponding path in | |
| // test/test_util, and the default value for `Jni._dylibDir`. | |
| const _defaultRelativeBuildPath = 'build/jni_libs'; | |
| const _buildPath = 'build-path'; | |
| const _srcPath = 'add-source'; | |
| const _packageName = 'add-package'; | |
| const _verbose = 'verbose'; | |
| const _cmakeArgs = 'cmake-args'; | |
| Future<void> runCommand( | |
| String exec, | |
| List<String> args, | |
| String workingDir, | |
| ) async { | |
| // For printing relative path always. | |
| var current = Directory.current.path; | |
| if (!current.endsWith(Platform.pathSeparator)) { | |
| current += Platform.pathSeparator; | |
| } | |
| if (workingDir.startsWith(current)) { | |
| workingDir.replaceFirst(current, ''); | |
| } | |
| final resolvedExec = jni_util.resolveJavaExecutable(exec); | |
| final cmd = "$resolvedExec ${args.join(" ")}"; | |
| stderr.writeln('+ [$workingDir] $cmd'); | |
| int status; | |
| if (options.verbose) { | |
| final process = await Process.start( | |
| resolvedExec, | |
| args, | |
| workingDirectory: workingDir, | |
| environment: jni_util.javaEnvironment, | |
| mode: ProcessStartMode.inheritStdio, | |
| // without `runInShell`, sometimes cmake doesn't run on windows. | |
| runInShell: true, | |
| ); | |
| status = await process.exitCode; | |
| if (status != 0) { | |
| exitCode = status; | |
| } | |
| } else { | |
| // ProcessStartMode.normal sometimes hangs on windows. No idea why. | |
| final process = await Process.run( | |
| resolvedExec, | |
| args, | |
| runInShell: true, | |
| environment: jni_util.javaEnvironment, | |
| workingDirectory: workingDir, | |
| ); | |
| status = process.exitCode; | |
| if (status != 0) { | |
| exitCode = status; | |
| var out = process.stdout; | |
| var err = process.stderr; | |
| if (stdout.supportsAnsiEscapes) { | |
| out = '$ansiRed$out$ansiDefault'; | |
| err = '$ansiRed$err$ansiDefault'; | |
| } | |
| stdout.writeln(out); | |
| stderr.writeln(err); | |
| } | |
| } | |
| if (status != 0) { | |
| stderr.writeln('Command exited with status code $status'); | |
| } | |
| } | |
| class Options { | |
| Options(ArgResults arg) | |
| : buildPath = arg[_buildPath] as String?, | |
| sources = arg[_srcPath] as List<String>, | |
| packages = arg[_packageName] as List<String>, | |
| cmakeArgs = arg[_cmakeArgs] as List<String>, | |
| verbose = (arg[_verbose] as bool?) ?? false; | |
| String? buildPath; | |
| List<String> sources; | |
| List<String> packages; | |
| List<String> cmakeArgs; | |
| bool verbose; | |
| } | |
| late Options options; | |
| void verboseLog(String msg) { | |
| if (options.verbose) { | |
| stderr.writeln(msg); | |
| } | |
| } | |
| /// Find path to C or Java sources in pub cache for package specified by | |
| /// [packageName]. | |
| /// | |
| /// If package cannot be found, null is returned. | |
| Future<String> findSources(String packageName, String subDirectory) async { | |
| final packageConfig = await findPackageConfig(Directory.current); | |
| if (packageConfig == null) { | |
| throw UnsupportedError('Please run from project root.'); | |
| } | |
| final package = packageConfig[packageName]; | |
| if (package == null) { | |
| throw UnsupportedError('Cannot find package: $packageName'); | |
| } | |
| return package.root.resolve(subDirectory).toFilePath(); | |
| } | |
| /// Return '/src' directories of all dependencies which has a CMakeLists.txt | |
| /// file. | |
| Future<Map<String, String>> findDependencySources() async { | |
| final packageConfig = await findPackageConfig(Directory.current); | |
| if (packageConfig == null) { | |
| throw UnsupportedError('Please run the command from project root.'); | |
| } | |
| final sources = <String, String>{}; | |
| for (var package in packageConfig.packages) { | |
| final src = package.root.resolve('src/'); | |
| final cmakeLists = src.resolve('CMakeLists.txt'); | |
| final cmakeListsFile = File.fromUri(cmakeLists); | |
| if (cmakeListsFile.existsSync()) { | |
| final firstLine = cmakeListsFile.readAsLinesSync().first; | |
| if (firstLine == jniNativeBuildDirective) { | |
| sources[package.name] = src.toFilePath(); | |
| } | |
| } | |
| } | |
| return sources; | |
| } | |
| /// Returns the name of file built using sources in [cDir] | |
| String getTargetName(Directory cDir) { | |
| for (final file in cDir.listSync(recursive: true)) { | |
| if (file.path.endsWith('.c')) { | |
| final cFileName = file.uri.pathSegments.last; | |
| final librarySuffix = Platform.isWindows ? 'dll' : 'so'; | |
| return cFileName.substring(0, cFileName.length - 1) + librarySuffix; | |
| } | |
| } | |
| throw Exception('Could not find a C file in ${cDir.path}'); | |
| } | |
| void main(List<String> arguments) async { | |
| final parser = ArgParser() | |
| ..addOption( | |
| _buildPath, | |
| abbr: 'b', | |
| help: 'Directory to place built artifacts', | |
| ) | |
| ..addMultiOption( | |
| _srcPath, | |
| abbr: 's', | |
| help: 'alternative path to package:jni sources', | |
| ) | |
| ..addMultiOption( | |
| _packageName, | |
| abbr: 'p', | |
| help: 'package for which native' | |
| 'library should be built', | |
| ) | |
| ..addFlag(_verbose, abbr: 'v', help: 'Enable verbose output') | |
| ..addMultiOption( | |
| _cmakeArgs, | |
| abbr: 'm', | |
| help: 'Pass additional argument to CMake', | |
| ); | |
| final argResults = parser.parse(arguments); | |
| options = Options(argResults); | |
| final rest = argResults.rest; | |
| if (rest.isNotEmpty) { | |
| stderr.writeln('one or more unrecognized arguments: $rest'); | |
| stderr.writeln('usage: dart run jni:setup <options>'); | |
| stderr.writeln(parser.usage); | |
| exitCode = 1; | |
| return; | |
| } | |
| final sources = options.sources; | |
| for (var packageName in options.packages) { | |
| // It's assumed C FFI sources are in "src/" relative to package root. | |
| sources.add(await findSources(packageName, 'src')); | |
| } | |
| if (sources.isEmpty) { | |
| final dependencySources = await findDependencySources(); | |
| stderr.writeln( | |
| 'selecting source directories for dependencies: ' | |
| '${dependencySources.keys}', | |
| ); | |
| sources.addAll(dependencySources.values); | |
| } else { | |
| stderr.writeln('selecting source directories: $sources'); | |
| } | |
| if (sources.isEmpty) { | |
| stderr.writeln('No source paths to build!'); | |
| exitCode = 1; | |
| return; | |
| } | |
| final currentDirUri = Uri.directory('.'); | |
| final buildPath = options.buildPath ?? | |
| currentDirUri.resolve(_defaultRelativeBuildPath).toFilePath(); | |
| final buildDir = Directory(buildPath); | |
| await buildDir.create(recursive: true); | |
| final javaSrc = await findSources('jni', 'java'); | |
| final targetJar = File.fromUri(buildDir.uri.resolve('jni.jar')); | |
| final isWin = Platform.isWindows; | |
| verboseLog('File exists:${targetJar.existsSync()}'); | |
| final gradleWExecutable = (Platform.isLinux || Platform.isMacOS) | |
| ? Uri.directory(javaSrc).resolve('gradlew') | |
| : Uri.directory(javaSrc).resolve('gradlew.bat'); | |
| if (!needsBuild(targetJar, Directory.fromUri(Uri.directory(javaSrc)))) { | |
| verboseLog( | |
| 'Last modified of ${targetJar.path}: ' | |
| '${targetJar.lastModifiedSync()}.', | |
| ); | |
| stderr.writeln('Target newer than source, skipping build.'); | |
| } else { | |
| verboseLog('Running gradle task for building jni sources to $buildPath.'); | |
| await runCommand( | |
| gradleWExecutable.toFilePath(windows: isWin), | |
| [ | |
| 'jar', | |
| '-Pjni.targetDir=${buildDir.absolute.uri.toFilePath(windows: isWin)}', | |
| ], | |
| await findSources('jni', 'java')); | |
| } | |
| for (var srcPath in sources) { | |
| final srcDir = Directory(srcPath); | |
| if (!srcDir.existsSync()) { | |
| stderr.writeln('Directory $srcPath does not exist'); | |
| exitCode = 1; | |
| return; | |
| } | |
| verboseLog('srcPath: $srcPath'); | |
| verboseLog('buildPath: $buildPath'); | |
| final targetFileUri = buildDir.uri.resolve(getTargetName(srcDir)); | |
| final targetFile = File.fromUri(targetFileUri); | |
| if (!needsBuild(targetFile, srcDir)) { | |
| verboseLog( | |
| 'Last modified of ${targetFile.path}: ' | |
| '${targetFile.lastModifiedSync()}.', | |
| ); | |
| stderr.writeln('Target newer than source, skipping build.'); | |
| continue; | |
| } | |
| // Note: creating temp dir in .dart_tool/jni instead of SystemTemp | |
| // because latter can fail tests on Windows CI, when system temp is on | |
| // separate drive or something. | |
| final jniDirUri = Uri.directory('.dart_tool').resolve('jni'); | |
| final jniDir = Directory.fromUri(jniDirUri); | |
| await jniDir.create(recursive: true); | |
| final tempDir = await jniDir.createTemp('jni_native_build_'); | |
| final cmakeArgs = <String>[]; | |
| cmakeArgs.addAll(options.cmakeArgs); | |
| // Force JNI to be required for standalone builds | |
| cmakeArgs.add('-DREQUIRE_JNI=ON'); | |
| // Pass absolute path of srcDir because cmake command is run in temp dir | |
| cmakeArgs.add(srcDir.absolute.path); | |
| await runCommand('cmake', cmakeArgs, tempDir.path); | |
| await runCommand('cmake', ['--build', '.'], tempDir.path); | |
| final dllDirUri = | |
| Platform.isWindows ? tempDir.uri.resolve('Debug') : tempDir.uri; | |
| final dllDir = Directory.fromUri(dllDirUri); | |
| for (var entry in dllDir.listSync()) { | |
| verboseLog(entry.toString()); | |
| final dllSuffix = Platform.isWindows | |
| ? 'dll' | |
| : Platform.isMacOS | |
| ? 'dylib' | |
| : 'so'; | |
| if (entry.path.endsWith(dllSuffix)) { | |
| final dllName = entry.uri.pathSegments.last; | |
| final target = buildDir.uri.resolve(dllName); | |
| entry.renameSync(target.toFilePath()); | |
| } | |
| } | |
| await tempDir.delete(recursive: true); | |
| } | |
| } |