From e2505d1d9e2e49554057a8cd5fb71b0ac0e3ba63 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Sun, 6 Dec 2015 17:03:08 -0800 Subject: Use separate project for native libraries --- .../main/scala/ch/jodersky/sbt/jni/Defaults.scala | 192 --------------------- .../main/scala/ch/jodersky/sbt/jni/JniJvm.scala | 71 ++++++++ .../main/scala/ch/jodersky/sbt/jni/JniNative.scala | 114 ++++++++++++ .../main/scala/ch/jodersky/sbt/jni/JniPlugin.scala | 19 -- .../src/main/scala/ch/jodersky/sbt/jni/Keys.scala | 53 ------ .../ch/jodersky/sbt/jni/build/BuildToolApi.scala | 14 +- .../scala/ch/jodersky/sbt/jni/build/CMake.scala | 53 ++---- .../sbt/jni/build/ConfigureMakeInstall.scala | 49 ++++++ .../scala/ch/jodersky/sbt/jni/build/NoTool.scala | 9 - .../scala/ch/jodersky/sbt/jni/util/ByteCode.scala | 57 ++++++ 10 files changed, 312 insertions(+), 319 deletions(-) delete mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/Defaults.scala create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniJvm.scala create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniNative.scala delete mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPlugin.scala delete mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/Keys.scala create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/ConfigureMakeInstall.scala delete mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/NoTool.scala create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/util/ByteCode.scala (limited to 'jni-plugin/src/main/scala/ch') diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/Defaults.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/Defaults.scala deleted file mode 100644 index 49f4a49..0000000 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/Defaults.scala +++ /dev/null @@ -1,192 +0,0 @@ -package ch.jodersky.sbt.jni - -import sbt._ -import sbt.Keys._ -import Keys._ - -import build.CMake -import build.BuildTool - -import ch.jodersky.jni.Platform -import ch.jodersky.jni.NativeLoader - -object Defaults { - - lazy val buildSettings: Seq[Setting[_]] = Seq( - - baseDirectory in jni := (baseDirectory in ThisBuild).value / "native", - - target in jni := target.value / "native" / (jniPlatform in jni).value.id, - - jniPlatform in jni := Platform.current.getOrElse{ - sLog.value.warn("Warning: cannot determine platform! It will be set to 'unknown'.") - Platform.Unknown - }, - - jniBuildTool in jni := { - val tools = Seq(CMake) - - val base = (baseDirectory in jni).value - - val tool = if (base.exists && base.isDirectory) { - tools.find(t => t detect base) - } else { - None - } - tool.getOrElse( - sys.error("No supported native build tool detected. " + - s"Check that the setting 'baseDirectory in jni' (currently $base) " + - "points to a valid directory. Supported build tools are: " + - tools.map(_.name).mkString(",") - ) - ) - }, - - clean := { - val log = streams.value.log - val tool = try { - Some((jniBuildTool in jni).value) - } catch { - case _: Exception => None - } - tool foreach { t => - log.debug("Cleaning native build") - t.api.clean( - (baseDirectory in jni).value, - log - ) - } - clean.value - }, - - jni := { - val tool = (jniBuildTool in jni).value - val dir = (baseDirectory in jni).value - val targetDir = (target in jni).value / "lib" - val log = streams.value.log - - tool.api.library(dir, targetDir, log) - }, - - javahClasses in javah := Seq(), - - javahObjects in javah := Seq(), - - target in javah := (baseDirectory in jni).value, - - fullClasspath in javah := (fullClasspath in Compile).value, - - javah := { - val out = (target in javah).value - val jcp: Seq[File] = (fullClasspath in javah).value.map(_.data) - val cp = jcp.mkString(sys.props("path.separator")) - - val classes = (javahClasses in javah).value ++ - (javahObjects in javah).value.map(_ + "$") - - for (clazz <- classes) { - val parts = Seq( - "javah", - "-d", out.getAbsolutePath, - "-classpath", cp, - clazz) - val cmd = parts.mkString(" ") - val ev = Process(cmd) ! streams.value.log - if (ev != 0) sys.error(s"Error occured running javah. Exit code: ${ev}") - } - - out - } - - ) - - lazy val bundleSettings: Seq[Setting[_]] = Seq( - - jniLibraryPath in jni := { - "/" + (organization.value + "." + name.value).replaceAll("\\.|-", "/") - }, - - unmanagedResourceDirectories in jni := Seq( - (baseDirectory).value / "lib_native" - ), - - unmanagedClasspath ++= (unmanagedResourceDirectories in jni).value.map{ file => - Attributed.blank(file) - }, - - resourceManaged in jni := (target in jni).value / "lib_managed", - - managedResourceDirectories in jni := Seq( - (resourceManaged in jni).value - ), - - managedClasspath ++= { - - //build native library - val library = jni.value - - //native library as a managed resource file - val libraryResource = (resourceManaged in jni).value / - NativeLoader.fullLibraryPath( - (jniLibraryPath in jni).value, - (jniPlatform in jni).value - ) - - //copy native library to a managed resource (so it can also be loaded when not packages as a jar) - IO.copyFile(library, libraryResource) - - Seq(Attributed.blank((resourceManaged in jni).value)) - }, - - packageJni in Global := { - - val unmanagedMappings: Seq[(File, String)] = (unmanagedResourceDirectories in jni).value flatMap { dir => - val files = (dir ** "*").filter(_.isFile).get - files map { file => - println(file.getAbsolutePath) - file -> (file relativeTo dir).get.getPath - } - } - - managedClasspath.value //call this to generate files in resourceManaged - - val managedMappings: Seq[(File, String)] = (managedResourceDirectories in jni).value flatMap { dir => - val files = (dir ** "*").filter(_.isFile).get - files map { file => - println(file.getAbsolutePath) - file -> (file relativeTo dir).get.getPath - } - } - - val out = target.value / (name.value + "-native.jar") - - val manifest = new java.util.jar.Manifest - Package.makeJar( - unmanagedMappings ++ managedMappings, - out, - manifest, - streams.value.log - ) - out - } - - ) - - lazy val clientSettings: Seq[Setting[_]] = Seq( - libraryDependencies += "ch.jodersky" %% "jni-library" % "0.1-SNAPSHOT", - fork in run := true, - artifact in jni := { - Artifact( - name = name.value, - `type` = "jar", - extension = "jar", - classifier = Some("native"), - configurations = Seq(Runtime), - url = None - ) extra ( - "platform" -> "all" - ) - } - ) ++ addArtifact(artifact in jni, packageJni) - -} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniJvm.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniJvm.scala new file mode 100644 index 0000000..70c5787 --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniJvm.scala @@ -0,0 +1,71 @@ +package ch.jodersky.sbt.jni + +import sbt._ +import sbt.Keys._ +import util.ByteCode + +object JniJvm extends AutoPlugin { + + override def requires = plugins.JvmPlugin + + object autoImport { + + val javahClasses = taskKey[Set[String]]( + "Fully qualified names of classes containing native declarations." + ) + + val javah = taskKey[File]( + "Generate JNI headers. Returns the directory containing generated headers." + ) + + } + import autoImport._ + + lazy val mainSettings: Seq[Setting[_]] = Seq( + + javahClasses in javah := { + val classFiles: Set[File] = compile.value.relations.allProducts.toSet + val nativeClasses = classFiles.flatMap { file => + ByteCode.natives(file) + } + nativeClasses + }, + + target in javah := target.value / "include", + + fullClasspath in javah := (fullClasspath in Compile).value, + + javah := { + val out = (target in javah).value + val jcp: Seq[File] = (fullClasspath in javah).value.map(_.data) + val cp = jcp.mkString(sys.props("path.separator")) + val log = streams.value.log + + val classes = (javahClasses in javah).value + + for (clazz <- classes) { + log.info("Generating header for " + clazz + "...") + val parts = Seq( + "javah", + "-d", out.getAbsolutePath, + "-classpath", cp, + clazz + ) + val cmd = parts.mkString(" ") + val ev = Process(cmd) ! streams.value.log + if (ev != 0) sys.error(s"Error occured running javah. Exit code: ${ev}") + } + out + } + ) + + lazy val clientSettings = Seq( + //enable enhanced native library extraction + libraryDependencies += "ch.jodersky" %% "jni-library" % Version.PluginVersion, + crossPaths := false, //don't need to appends scala version to native jars + fork in run := true //fork new JVM as native libraries can only be loaded once + ) + + override lazy val projectSettings = inConfig(Compile)(mainSettings) ++ clientSettings + +} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniNative.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniNative.scala new file mode 100644 index 0000000..7e36f50 --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniNative.scala @@ -0,0 +1,114 @@ +package ch.jodersky.sbt.jni + +import build._ +import ch.jodersky.jni.{NativeLoader, Platform} +import sbt._ +import sbt.Keys._ + +object JniNative extends AutoPlugin { + + override def requires = plugins.JvmPlugin + + object autoImport { + + val jni = taskKey[File]("Builds a native library (by calling the native build tool).") + + val jniPlatform = settingKey[Platform]("Platform of the system this build is running on.") + val jniBuildTool = taskKey[BuildTool]("The build tool to be used when building a native library.") + + val jniLibraryPath = settingKey[String]("String that is prepended to the path of a native library when packaged.") + + } + import autoImport._ + + lazy val rawSettings: Seq[Setting[_]] = Seq( + + sourceDirectory in jni := baseDirectory.value / "src", + + target in Global in jni := target.value / "native" / (jniPlatform in jni).value.id, + + jniPlatform in Global in jni := Platform.current.getOrElse { + sLog.value.warn("Warning: cannot determine platform! It will be set to 'unknown'.") + Platform.Unknown + }, + + jniBuildTool in jni := { + val tools = Seq(CMake) + + val base = (sourceDirectory in jni).value + + val tool = if (base.exists && base.isDirectory) { + tools.find(t => t detect base) + } else { + None + } + tool.getOrElse( + sys.error("No supported native build tool detected. " + + s"Check that the setting 'sourceDirectory in jni' (currently $base) " + + "points to a valid directory. Supported build tools are: " + + tools.map(_.name).mkString(",")) + ) + }, + + clean in jni := { + val log = streams.value.log + val tool = try { + Some((jniBuildTool in jni).value) + } catch { + case _: Exception => None + } + tool foreach { t => + log.debug("Cleaning native build") + t.api.clean( + (sourceDirectory in jni).value, + log + ) + } + }, + + jni := { + val tool = (jniBuildTool in jni).value + val dir = (sourceDirectory in jni).value + val buildDir = (target in jni).value / "build" + val targetDir = (target in jni).value / "lib" + val log = streams.value.log + + IO.createDirectory(buildDir) + IO.createDirectory(targetDir) + + tool.api.library(dir, buildDir, targetDir, log) + }, + + clean := { + (clean in jni).value + clean.value + }, + + jniLibraryPath in jni := { + "/" + organization.value.replaceAll("\\.", "/") + "/" + name.value + }, + + unmanagedResourceDirectories += baseDirectory.value / "lib_native", + + resourceGenerators += Def.task { + //build native library + val library = jni.value + + //native library as a managed resource file + val libraryResource = (resourceManaged in Compile).value / + NativeLoader.fullLibraryPath( + (jniLibraryPath in jni).value, + (jniPlatform in jni).value + ) + + //copy native library to a managed resource (so it can also be loaded when not packaged as a jar) + IO.copyFile(library, libraryResource) + + Seq(libraryResource) + }.taskValue + + ) + + override lazy val projectSettings = inConfig(Compile)(rawSettings) + +} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPlugin.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPlugin.scala deleted file mode 100644 index 98f8214..0000000 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPlugin.scala +++ /dev/null @@ -1,19 +0,0 @@ -package ch.jodersky.sbt.jni - -import sbt._ - -object JniPlugin extends AutoPlugin { - - override def requires = plugins.JvmPlugin - - lazy val autoImport = Keys - import autoImport._ - - override lazy val projectSettings = - Defaults.buildSettings ++ - inConfig(Runtime)(Defaults.bundleSettings) ++ - Defaults.clientSettings - //++inConfig(Runtime)(Defaults.bundleSettings) - - -} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/Keys.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/Keys.scala deleted file mode 100644 index 4286ff6..0000000 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/Keys.scala +++ /dev/null @@ -1,53 +0,0 @@ -package ch.jodersky.sbt.jni - -import build.BuildTool - -import ch.jodersky.jni.Platform -import sbt._ - -object Keys { - - val jni = taskKey[File]("Builds a native library (by calling the native build tool).") - val jniClean = taskKey[Unit]("Cleans the native build directory.") - - val jniPlatform = settingKey[Platform]("Platform of the system this build is running on.") - val jniBuildTool = taskKey[BuildTool]("The build tool to be used when building a native library.") - - //bundle - val jniLibraryPath = settingKey[String]("Foo") - val packageJni = taskKey[File]("Package native librraies into a fat jar.") - - val javahClasses = settingKey[Seq[String]]("Fully qualified names of classes containing native declarations.") - val javahObjects = settingKey[Seq[String]]("Fully qualified names of singleton objects containing native declarations.") - val javah = taskKey[File]("Generate JNI headers. Returns the directory containing generated headers.") - - - //ivy - //libraryDependencies += "com.github.jodersky" % "flow" % "2.4" extra("platform", "all") artifact("libflow", "so") - - //maven - //libraryDependencies += "com.github.jodersky" % "flow" % "2.4" classifier "native" - - - - //Wraps tasks associated to an existing native build tool - //val Native = config("native") - - //Extra tasks in native - // val buildTool = settingKey[BuildTool]("The native build tool used.") - //val platform = settingKey[Platform]("Platform of the system this build is being run on.") - - // organization = org.example - // name = foo-bar - // libraryPrefix = organization + "." + normalize(name) - // libraryPrefix = org/example/foo/bar - // libraryPrefix = org/example/foo/bar/native//libraries - - - //val libraryPath = settingKey[String]( - // "A slash (/) seprated path that specifies where native libraries should be stored (in a jar).") - //val libraryResourceDirectory = settingKey[File]("") - - //Javah - -} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/BuildToolApi.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/BuildToolApi.scala index 47d9a90..43dd17a 100644 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/BuildToolApi.scala +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/BuildToolApi.scala @@ -4,19 +4,23 @@ package build import java.io.File import sbt.Logger - trait BuildToolApi { /** Invokes the native build tool's clean task */ - def clean(baseDirectory: File, log: Logger): Unit + def clean(baseDirectory: File, log: Logger): Unit /** - * Invokes the native build tool's main task, resulting in a single shared - * library file. - */ + * Invokes the native build tool's main task, resulting in a single shared + * library file. + * @param baseDirectory the directory where the native project is located + * @param buildDirectory a directory from where the build is called, it may be used to store temporary files + * @param targetDirectory the directory into which the native library is copied + * @return the native library file + */ def library( baseDirectory: File, targetDirectory: File, + buildDirectory: File, log: Logger ): File diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/CMake.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/CMake.scala index cd0b363..3f75585 100644 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/CMake.scala +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/CMake.scala @@ -1,12 +1,8 @@ package ch.jodersky.sbt.jni package build -import Keys._ -import sbt._ -import sbt.Keys._ -import sbt.Logger import java.io.File - +import sbt._ object CMake extends BuildTool { @@ -14,46 +10,21 @@ object CMake extends BuildTool { def detect(baseDirectory: File) = baseDirectory.list().contains("CMakeLists.txt") - object api extends BuildToolApi { + object api extends ConfigureMakeInstall { - def clean(baseDirectory: File, log: Logger) = Process("make clean", baseDirectory) ! log - - def library( - baseDirectory: File, - targetDirectory: File, - log: Logger - ): File = { - val out = targetDirectory - val outPath = out.getAbsolutePath - - val configure = Process( + override def configure(base: File, build: File, target: File) = { + val targetPath = target.getAbsolutePath + Process( //Disable producing versioned library files, not needed for fat jars. - s"cmake -DCMAKE_INSTALL_PREFIX:PATH=$outPath -DLIB_INSTALL_DIR:PATH=$outPath -DENABLE_VERSIONED_LIB:BOOLEAN=OFF", - baseDirectory + "cmake " + + s"-DCMAKE_INSTALL_PREFIX:PATH=$targetPath " + + s"-DLIB_INSTALL_DIR:PATH=$targetPath " + + "-DENABLE_VERSIONED_LIB:BOOLEAN=OFF " + + base.getAbsolutePath, + build ) - - val make = Process("make", baseDirectory) - - val makeInstall = Process("make install", baseDirectory) - - val ev = configure #&& make #&& makeInstall ! log - if (ev != 0) sys.error(s"Building native library failed. Exit code: ${ev}") - - val products: List[File] = (out ** ("*" -- "*.la")).get.filter(_.isFile).toList - - //only one produced library is expected - products match { - case Nil => - sys.error("No files were created during compilation, " + - "something went wrong with the autotools configuration.") - case head :: Nil => - head - case head :: tail => - log.warn("More than one file was created during compilation, " + - s"only the first one (${head.getAbsolutePath}) will be used.") - head - } } + } } diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/ConfigureMakeInstall.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/ConfigureMakeInstall.scala new file mode 100644 index 0000000..97011ed --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/ConfigureMakeInstall.scala @@ -0,0 +1,49 @@ +package ch.jodersky.sbt.jni +package build + +import java.io.File +import sbt._ + +/** Native build tools relying on a standard 'configure && make && make install' process */ +trait ConfigureMakeInstall extends BuildToolApi { + + override def clean(baseDirectory: File, log: Logger) = Process("make clean", baseDirectory) ! log + + def configure(baseDirectory: File, buildDirectory: File, targetDirectory: File): ProcessBuilder + + def make(baseDirectory: File, buildDirectory: File, targetDirectory: File): ProcessBuilder = Process("make", buildDirectory) + + def install(baseDirectory: File, buildDirectory: File, targetDirectory: File): ProcessBuilder = Process("make install", buildDirectory) + + override def library( + baseDirectory: File, + buildDirectory: File, + targetDirectory: File, + log: Logger + ): File = { + + val ev = ( + configure(baseDirectory, buildDirectory, targetDirectory) #&& + make(baseDirectory, buildDirectory, targetDirectory) #&& + install(baseDirectory, buildDirectory, targetDirectory) + ) ! log + + if (ev != 0) sys.error(s"Building native library failed. Exit code: ${ev}") + + val products: List[File] = (targetDirectory ** ("*" -- "*.la")).get.filter(_.isFile).toList + + //only one produced library is expected + products match { + case Nil => + sys.error("No files were created during compilation, " + + "something went wrong with the autotools configuration.") + case head :: Nil => + head + case head :: tail => + log.warn("More than one file was created during compilation, " + + s"only the first one (${head.getAbsolutePath}) will be used.") + head + } + } +} + diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/NoTool.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/NoTool.scala deleted file mode 100644 index 96dd85f..0000000 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/NoTool.scala +++ /dev/null @@ -1,9 +0,0 @@ -package ch.jodersky.sbt.jni -package build - - -object NoTool { - - - -} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/util/ByteCode.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/util/ByteCode.scala new file mode 100644 index 0000000..308a077 --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/util/ByteCode.scala @@ -0,0 +1,57 @@ +package ch.jodersky.sbt.jni +package util + +import java.io.{File, FileInputStream, Closeable} +import scala.collection.mutable.{HashSet} + +import org.objectweb.asm.{ClassReader, ClassVisitor, MethodVisitor, Opcodes} + +object ByteCode { + + private class NativeFinder extends ClassVisitor(Opcodes.ASM5) { + + //contains any classes found to contain at least one @native def + val _nativeClasses = new HashSet[String] + def nativeClasses = _nativeClasses.toSet + + private var fullyQualifiedName: String = "" + + override def visit(version: Int, access: Int, name: String, signature: String, + superName: String, interfaces: Array[String]): Unit = { + fullyQualifiedName = name.replaceAll("/", ".") + } + + override def visitMethod(access: Int, name: String, desc: String, + signature: String, exceptions: Array[String]): MethodVisitor = { + + val isNative = (access & Opcodes.ACC_NATIVE) != 0 + + if (isNative) { + _nativeClasses += fullyQualifiedName + } + + null //do not visit method further + } + + } + + private def using[A >: Null <: Closeable, R](mkStream: => A)(action: A => R): R = { + var stream: A = null + try { + stream = mkStream + action(stream) + } finally { + if (stream != null) { + stream.close() + } + } + } + + def natives(classFile: File): Set[String] = using(new FileInputStream(classFile)) { in => + val reader = new ClassReader(in) + val finder = new NativeFinder + reader.accept(finder, 0) + finder.nativeClasses + } + +} -- cgit v1.2.3