From 2b343f73fc5b7605b22af41c675c2afc86d2a447 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Sun, 17 Jan 2016 17:18:13 -0800 Subject: Refactor to multi-plugin build --- README.md | 113 ++++++++----- .../main/scala/ch/jodersky/sbt/jni/JniJvm.scala | 77 --------- .../main/scala/ch/jodersky/sbt/jni/JniNative.scala | 123 -------------- .../scala/ch/jodersky/sbt/jni/JniPackager.scala | 110 ------------- .../main/scala/ch/jodersky/sbt/jni/JniPlugin.scala | 99 ----------- .../ch/jodersky/sbt/jni/build/Autotools.scala | 5 +- .../ch/jodersky/sbt/jni/plugins/JniJavah.scala | 69 ++++++++ .../ch/jodersky/sbt/jni/plugins/JniLoading.scala | 28 ++++ .../ch/jodersky/sbt/jni/plugins/JniNative.scala | 100 +++++++++++ .../jodersky/sbt/jni/plugins/JniNative.scala.old | 123 ++++++++++++++ .../jodersky/sbt/jni/plugins/JniPackage.scala.old | 183 +++++++++++++++++++++ .../ch/jodersky/sbt/jni/plugins/JniPackaging.scala | 113 +++++++++++++ samples/basic/basic-core/build.sbt | 1 + .../main/scala/ch/jodersky/jni/basic/Library.scala | 8 + .../main/scala/ch/jodersky/jni/basic/Main.scala | 13 ++ .../main/scala/ch/jodersky/jni/basic/Library.scala | 8 - .../main/scala/ch/jodersky/jni/basic/Main.scala | 13 -- samples/basic/build.sbt | 23 ++- samples/basic/project/plugins.sbt | 2 +- 19 files changed, 731 insertions(+), 480 deletions(-) delete mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniJvm.scala delete 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/JniPackager.scala delete mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPlugin.scala create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoading.scala create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniNative.scala create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniNative.scala.old create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniPackage.scala.old create mode 100644 jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniPackaging.scala create mode 100644 samples/basic/basic-core/build.sbt create mode 100644 samples/basic/basic-core/src/main/scala/ch/jodersky/jni/basic/Library.scala create mode 100644 samples/basic/basic-core/src/main/scala/ch/jodersky/jni/basic/Main.scala delete mode 100644 samples/basic/basic-main/src/main/scala/ch/jodersky/jni/basic/Library.scala delete mode 100644 samples/basic/basic-main/src/main/scala/ch/jodersky/jni/basic/Main.scala diff --git a/README.md b/README.md index 7f3c6c3..502dfa7 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,97 @@ -# SBT-JNI -Making JNI tolerable. - -Work in progress... +[Work in progress] -## Overview of JNI -Java Native Interface (JNI), is a standard interface to allow programs written in a JVM language to interact with native code. Creating a program that uses JNI is usually a multi-step process: - -1. The JVM side of the program is written (in Java or Scala for example) and interfacing methods declared as native. -2. JVM sources are compiled. -3. The program "javah" is run to generate C header files for classes containing native declarations. -4. These header files may be included from actual native source files that implement the native methods. -5. Native sources are compiled into a native library. +# SBT-JNI -Running the program also requires special steps: +A suite of plugins for simplifying creation and distribution of JNI programs. -1. Supply the native library to the program's library path. -2. Run the program, making sure to load the native library (through `System.load()`) before calling any native methods. +## Motivation +Java Native Interface (JNI), is a standard interface to allow programs written in a JVM language to interact with native code. Such programs can be divided into two logical parts: the JVM part, consisting of source files that will generate java bytecode (e.g. Scala or Java), and the native part, consisting of source files that will be compiled down to machine-native code (e.g. C, C++ or assembly). Interaction between the two parts is managed by header files generated through the `javah` utility. -Finally, since platform independence is lost with native libraries, publishing an application or library becomes harder: +Using native code can be beneficial in some situations, it can for example provide performance gains or otherwise infeasable features such as system-level interaction with peripherals. However, it also adds a few complexities, notably: -1. Native libraries must be compiled and made available for all supported platforms. -2. Loading the correct library on a given platform has to be managed somehow. +- Compilation: the project is divided into two parts, each of which require separate compilation. Furthermore, header generation with `javah` has to be dealt with. +- Portability: native binaries only run on the platform on which they were compiled. +- Distribution: native binaries must be made available and packaged for every supported platform. -All in all, using JNI impairs platform-independence and thus a considerable amount of the JVM's advantages regarding ease of development and distribution. +The second point, portability, is inherent to JNI and thus unavoidable. However the first and last points can be greatly simplified with the help of build tools. ## Plugin Overview -As described in the previous paragraph, using JNI adds a number of burdens to developing applications. The problems can be divided into two groups, both of which this plugin attempts to address: +This project consists of 4 autoplugins that aim to solve the difficulties in compiling and distributing JNI programs. These plugins provide two functionalities (compiling and distributing) for two parts of a project (JVM and native): -- Build problems, related to the manual work involved with synchronizing JVM and native sources and running applications locally. -- Distribution problems, related to the way libraries and applications can be deployed. +Plugins +--------------|-----------------|------------- +Functionality | JVM part | Native part +--------------|-----------------|------------- +Compiling | JniJavah | JniNative +Packaging | JniLoading | JniPackaging -[how the plugin addresses issues] -[sbt wrapper for native build tools] +A project using this suite of plugins must be divided into two sub-projects, corresponding to the two parts: one that will contain JVM sources and one that will contain native sources. -## Usage +The reason for dividing a project into two subprojects is two-fold: it enables flexible plugging of native sources and also integrates easily into the existing maven ecosystem. Adding the native binaries as additional artifacts has issues with scala versioning. -Depend on plugin `project/plugins.sbt`: +## Usage +Add plugin dependency. In `project/plugins.sbt`: ```scala -addSbtPlugin("ch.jodersky" % "sbt-jni" % "0.2") +addSbtPlugin("ch.jodersky" % "sbt-jni" % "0.3") ``` -Create sbt project containing JVM sources: -```scala -lazy val main = Project("main", file("main)).enablePlugins(JniJvm).dependsOn(native % Runtime) -``` +Define sub-projects for JVM and native sources. In `myproject/build.sbt`: -Create sbt project containing native sources: ```scala -lazy val native = Project("native", file("native")).dependsOn(JniNative) +lazy val core = project in file("myproject-core") // contains regular jvm sources and @native methods +lazy val native = project in file("myproject-native") // contains native sources ``` -### Keys +Select plugins to enable on sub-projects: -``` -javah -jni -``` +- In `myproject-core/build.sbt`: + + ```scala + //enablePlugin(JniJavah) // this plugin is added to all JVM projects by default + enablePlugin(JniLoading) + ``` + +- In `myproject-native/build.sbt`: + + ```scala + enablePlugin(JniNative) + //enablePlugin(JniPackaging) // this plugin is added to all projects using JniNative by default + ``` + +Note that some plugins are added by default. To disable their functionality (for example if you don't wish to package native libraries), add `disablePlugin()` to the corresponding build definition. + + +## Plugin Details +The following gives a detailed description about the various JNI plugins and the ways to customize their behaviour. + +### Javah +*JVM sub-projects* + +#### Functionality +Adds a `javah` task to generate header files for classes containing `@native` methods. + +#### Customization +Change target to point to `include` directory of native sources: `target in javah := file("myproject-native") / "src" / "include"` + + +### JniNative +*Native sub-projects* + +#### Functionality +Provides sbt tasks to call into a native build system such as Automake or CMake. Run `sbt nativeCompile` to compile native sources. + +#### Customization +Change source directory to point to a directory containing the native build definition. Supported build tools are: + +- Automake +- CMake + +*Make sure the native build configurations respect the arguments and output directories as specified in the [native build templates](templates)* + +### JniLoading +*JVM sub-projects* -### Common settings +### JniPackaging +*Native sub-projects* ## Examples 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 deleted file mode 100644 index 789eabf..0000000 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniJvm.scala +++ /dev/null @@ -1,77 +0,0 @@ -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 compiled: inc.Analysis = (compile in Compile).value - val classFiles: Set[File] = compiled.relations.allProducts.toSet - val nativeClasses = classFiles flatMap { file => - ByteCode.natives(file) - } - nativeClasses - }, - - target in javah := target.value / "include", - - javah := { - val out = (target in javah).value - val jcp: Seq[File] = { - (compile in Compile).value - //FIXME: a cleaner approach that would also call compile would - //be to use `fullClasspath`, this however results in generating resources - //which in turn might require header files generated by javah. Hence - //to avoid a cyclic dependency `compile` is called first and the - //class directory returned. - Seq((classDirectory in Compile).value) - } - 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, - fork in run := true //fork new JVM, since native libraries can only be loaded once - ) - - override lazy val projectSettings = 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 deleted file mode 100644 index 9cfa2af..0000000 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniNative.scala +++ /dev/null @@ -1,123 +0,0 @@ -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 mainSettings: Seq[Setting[_]] = Seq( - - sourceDirectory in jni := baseDirectory.value / "src", - - 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, Autotools) - - 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 - } - - ) - - lazy val resourceSettings: Seq[Setting[_]] = Seq( - - unmanagedResourceDirectories in Compile += baseDirectory.value / "lib_native", - - resourceGenerators in Compile += 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 = mainSettings ++ resourceSettings ++ Seq( - //don't add scala version to native jars - crossPaths := false - ) - -} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPackager.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPackager.scala deleted file mode 100644 index 56322ad..0000000 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPackager.scala +++ /dev/null @@ -1,110 +0,0 @@ -package ch.jodersky.sbt.jni - -import build._ -import ch.jodersky.jni.{NativeLoader, Platform} -import sbt._ -import sbt.Keys._ - -object JniPackager extends AutoPlugin { - - override def requires = Jni - override def trigger = allRequirements - - object autoImport { - - //Main task, inspect this first - val packageNative = taskKey[File]("Packages native libraries in a jar.") - - val nativeLibraryPath = settingKey[String]( - "String that is prepended to the path of a native library when packaged.") - - val enableNativeCompilation = settingKey[Boolean]( - "Determines if native compilation is enabled in a scoped key (typically for packaging).") - - val unmanagedNativeDirectories = settingKey[Seq[File]]( - """|Unmanaged directories containing native libraries. The libraries must be regular files - |contained in a subdirectory corresponding to a platform.""".stripMargin) - - val unmanagedNativeLibraries = taskKey[Map[Platform, File]]("") - - } - import autoImport._ - import Jni.autoImport._ - - lazy val settings: Seq[Setting[_]] = Seq( - - nativeLibraryPath := { - val orgPath = organization.value.replaceAll("\\.", "/") - s"/${orgPath}/${name.value}/native" - }, - - //Make NativeLoader available to project - libraryDependencies += "ch.jodersky" %% "jni-library" % Version.PluginVersion, - - name in packageNative := { - name.value + "-native-" + version.value + ".jar" - }, - - enableNativeCompilation in packageNative := true, - - unmanagedNativeDirectories := Seq(baseDirectory.value / "lib_native"), - - unmanagedNativeLibraries := { - val dirs: Seq[File] = unmanagedNativeDirectories.value - val seq: Seq[(Platform, File)] = for ( - dir <- dirs; - platformDir <- dir.listFiles(); - if platformDir.isDirectory; - platform = Platform.fromId(platformDir.name); - library <- platformDir.listFiles(); - if library.isFile - ) yield { - platform -> library.getCanonicalFile() - } - - seq.toMap - }, - - packageNative := { - val log = streams.value.log - - val unmanagedMappings: Seq[(File, String)] = { - unmanagedNativeLibraries.value.toSeq.map{ case (platform, file) => - file -> NativeLoader.fullLibraryPath( - (nativeLibraryPath in Compile).value, - platform) - } - } - - val enableManaged = (enableNativeCompilation in packageNative).value - - val managedMappings: Seq[(File, String)] = if (enableManaged) { - val library: File = (nativeCompile in Compile).value - val path: String = NativeLoader.fullLibraryPath( - (nativeLibraryPath in Compile).value, - (nativePlatform in Compile).value - ) - Seq(library -> path) - } else { - Seq() - } - - val mappings = unmanagedMappings ++ managedMappings - val manifest = new java.util.jar.Manifest - val jar: File = (target in nativeCompile).value / (name in packageNative).value - - Package.makeJar(mappings, jar, manifest, log) - jar - }, - - //Add native jar to runtime classpath. Note that it is not added as a resource (this would cause - //the native library to included in the main jar). - unmanagedClasspath in Runtime += Attributed.blank(packageNative.value), - - fork in run := true //fork new JVM, since native libraries can only be loaded once - - ) - override lazy val projectSettings = (settings) - - -} 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 2b76b7a..0000000 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/JniPlugin.scala +++ /dev/null @@ -1,99 +0,0 @@ -package ch.jodersky.sbt.jni - -import build._ -import ch.jodersky.jni.{NativeLoader, Platform} -import sbt._ -import sbt.Keys._ - -object Jni extends AutoPlugin { - - override def requires = plugins.JvmPlugin - - object autoImport { - - //Main task, inspect this first - val nativeCompile = taskKey[File]("Builds a native library (by calling the native build tool).") - - val nativePlatform = settingKey[Platform]("Platform of the system this build is running on.") - - val nativeBuildTools = taskKey[Seq[BuildTool]]( - "A collection of build tools that are tested to determine the current build environment") - val nativeBuildTool = taskKey[BuildTool]( - "The build tool to be used when building a native library.") - - - } - import autoImport._ - - lazy val mainSettings: Seq[Setting[_]] = Seq( - - nativePlatform := Platform.current.getOrElse{ - sLog.value.warn("Warning: cannot determine platform! It will be set to 'unknown'.") - Platform.Unknown - }, - - sourceDirectory in nativeCompile := sourceDirectory.value / "native", - - target in nativeCompile := target.value / "native" / (nativePlatform).value.id, - - nativeBuildTools := Seq(CMake, Autotools), - - nativeBuildTool := { - val tools = nativeBuildTools.value - - val src = (sourceDirectory in nativeCompile).value - - val tool = if (src.exists && src.isDirectory) { - tools.find(t => t detect src) - } else { - None - } - tool.getOrElse( - sys.error("No supported native build tool detected. " + - s"Check that the setting 'sourceDirectory in nativeCompile' (currently $src) " + - "points to a valid directory. Supported build tools are: " + - tools.map(_.name).mkString(",")) - ) - - }, - - clean in nativeCompile := { - val log = streams.value.log - - log.debug("Cleaning native build") - try { - val tool = nativeBuildTool.value - tool.api.clean( - (sourceDirectory in nativeCompile).value, - log - ) - } catch { - case ex: Exception => - log.debug(s"Native cleaning failed: $ex") - } - - }, - - nativeCompile := { - val tool = nativeBuildTool.value - val src = (sourceDirectory in nativeCompile).value - val buildDir = (target in nativeCompile).value / "build" - val targetDir = (target in nativeCompile).value / "lib" - val log = streams.value.log - - IO.createDirectory(buildDir) - IO.createDirectory(targetDir) - - tool.api.library(src, buildDir, targetDir, log) - }, - - // change clean task to also clean native sources - clean := { - (clean in nativeCompile).value - clean.value - } - - ) - - override lazy val projectSettings = inConfig(Compile)(mainSettings) -} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/Autotools.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/Autotools.scala index c1de1ea..960e66e 100644 --- a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/Autotools.scala +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/build/Autotools.scala @@ -16,11 +16,12 @@ object Autotools extends BuildTool { val targetPath = target.getAbsolutePath Process( - s"${base.getAbsolutePath}/configure " + + s"${base.getAbsolutePath}/configure " + s"--prefix=$targetPath " + s"--libdir=$targetPath " + "--disable-versioned-lib", - build) + build + ) } } diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala new file mode 100644 index 0000000..63e5b23 --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala @@ -0,0 +1,69 @@ +package ch.jodersky.sbt.jni +package plugins + +import sbt._ +import sbt.Keys._ +import util.ByteCode + +/** Adds `javah` header-generation functionality to projects. */ +object JniJavah extends AutoPlugin { + + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + 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 compiled: inc.Analysis = (compile in Compile).value + val classFiles: Set[File] = compiled.relations.allProducts.toSet + val nativeClasses = classFiles flatMap { file => + ByteCode.natives(file) + } + nativeClasses + }, + + target in javah := target.value / "include", + + fullClasspath in javah := fullClasspath.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 + + log.info("Headers will be generated to " + out.getAbsolutePath) + 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 + } + ) + + override lazy val projectSettings = inConfig(Compile)(mainSettings) + +} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoading.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoading.scala new file mode 100644 index 0000000..599bfa1 --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoading.scala @@ -0,0 +1,28 @@ +package ch.jodersky.sbt.jni +package plugins + +import sbt._ +import sbt.Keys._ +import util.ByteCode + +/** + * Enables loading native libraries from the classpath, typically created + * from a project using JniPackaging. + */ +object JniLoading extends AutoPlugin { + + override def requires = plugins.JvmPlugin + + lazy val settings = Seq( + + //enable enhanced native library extraction + libraryDependencies += "ch.jodersky" %% "jni-library" % Version.PluginVersion, + + //fork new JVM, since native libraries can only be loaded once + fork in run := true + + ) + + override lazy val projectSettings = settings + +} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniNative.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniNative.scala new file mode 100644 index 0000000..2ac179d --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniNative.scala @@ -0,0 +1,100 @@ +package ch.jodersky.sbt.jni +package plugins + +import build._ +import ch.jodersky.jni.{NativeLoader, Platform} +import sbt._ +import sbt.Keys._ + +/** Wraps a native build system in sbt tasks. */ +object JniNative extends AutoPlugin { + + object autoImport { + + //Main task, inspect this first + val nativeCompile = taskKey[File]("Builds a native library (by calling the native build tool).") + + val nativePlatform = settingKey[Platform]("Platform of the system this build is running on.") + + val nativeBuildTools = taskKey[Seq[BuildTool]]( + "A collection of build tools that are tested to determine the current build environment" + ) + val nativeBuildTool = taskKey[BuildTool]( + "The build tool to be used when building a native library." + ) + + } + import autoImport._ + + lazy val settings: Seq[Setting[_]] = Seq( + + nativePlatform := Platform.current.getOrElse { + sLog.value.warn("Warning: cannot determine platform! It will be set to 'unknown'.") + Platform.Unknown + }, + + sourceDirectory in nativeCompile := sourceDirectory.value / "native", + + target in nativeCompile := target.value / "native" / (nativePlatform).value.id, + + nativeBuildTools := Seq(CMake, Autotools), + + nativeBuildTool := { + val tools = nativeBuildTools.value + + val src = (sourceDirectory in nativeCompile).value + + val tool = if (src.exists && src.isDirectory) { + tools.find(t => t detect src) + } else { + None + } + tool.getOrElse( + sys.error("No supported native build tool detected. " + + s"Check that the setting 'sourceDirectory in nativeCompile' (currently $src) " + + "points to a valid directory. Supported build tools are: " + + tools.map(_.name).mkString(",")) + ) + + }, + + clean in nativeCompile := { + val log = streams.value.log + + log.debug("Cleaning native build") + try { + val tool = nativeBuildTool.value + tool.api.clean( + (sourceDirectory in nativeCompile).value, + log + ) + } catch { + case ex: Exception => + log.debug(s"Native cleaning failed: $ex") + } + + }, + + nativeCompile := { + val tool = nativeBuildTool.value + val src = (sourceDirectory in nativeCompile).value + val buildDir = (target in nativeCompile).value / "build" + val targetDir = (target in nativeCompile).value / "lib" + val log = streams.value.log + + IO.createDirectory(buildDir) + IO.createDirectory(targetDir) + + tool.api.library(src, buildDir, targetDir, log) + }, + + // change clean task to also clean native sources + clean := { + (clean in nativeCompile).value + clean.value + } + + ) + + override lazy val projectSettings = inConfig(Compile)(settings) +} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniNative.scala.old b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniNative.scala.old new file mode 100644 index 0000000..9cfa2af --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniNative.scala.old @@ -0,0 +1,123 @@ +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 mainSettings: Seq[Setting[_]] = Seq( + + sourceDirectory in jni := baseDirectory.value / "src", + + 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, Autotools) + + 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 + } + + ) + + lazy val resourceSettings: Seq[Setting[_]] = Seq( + + unmanagedResourceDirectories in Compile += baseDirectory.value / "lib_native", + + resourceGenerators in Compile += 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 = mainSettings ++ resourceSettings ++ Seq( + //don't add scala version to native jars + crossPaths := false + ) + +} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniPackage.scala.old b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniPackage.scala.old new file mode 100644 index 0000000..60980f5 --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniPackage.scala.old @@ -0,0 +1,183 @@ +package ch.jodersky.sbt.jni + +import build._ +import ch.jodersky.jni.{NativeLoader, Platform} +import sbt._ +import sbt.Keys._ + +object JniPackager extends AutoPlugin { + + override def requires = Jni + override def trigger = allRequirements + + object autoImport { + + //Main task, inspect this first + //val packageNative = taskKey[File]("Packages native libraries in a jar.") + + //val packageNativeMappings = taskKey[Seq[(File, String)]]("") + + val nativeLibraryPath = settingKey[String]( + "String that is prepended to the path of a native library when packaged.") + + val enableNativeCompilation = settingKey[Boolean]( + "Determines if native compilation is enabled in a scoped key (typically for packaging).") + + val unmanagedNativeDirectories = settingKey[Seq[File]]( + """|Unmanaged directories containing native libraries. The libraries must be regular files + |contained in a subdirectory corresponding to a platform.""".stripMargin) + + val unmanagedNativeLibraries = taskKey[Map[Platform, File]]("") + + val managedNativeLibraries = taskKey[Map[Platform, File]]("") + + } + import autoImport._ + import Jni.autoImport._ + + lazy val settings: Seq[Setting[_]] = Seq( + + nativeLibraryPath := { + val orgPath = organization.value.replaceAll("\\.", "/") + s"/${orgPath}/${name.value}/native" + }, + + //Make NativeLoader available to project + libraryDependencies += "ch.jodersky" %% "jni-library" % Version.PluginVersion, + + name in packageNative := { + name.value + "-native-" + version.value + ".jar" + }, + + enableNativeCompilation := true, + + unmanagedNativeDirectories := Seq(baseDirectory.value / "lib_native"), + + unmanagedNativeLibraries := { + val dirs: Seq[File] = unmanagedNativeDirectories.value + val seq: Seq[(Platform, File)] = for ( + dir <- dirs; + platformDir <- dir.listFiles(); + if platformDir.isDirectory; + platform = Platform.fromId(platformDir.name); + library <- platformDir.listFiles(); + if library.isFile + ) yield { + platform -> library.getCanonicalFile() + } + + seq.toMap + }, + + managedNativeLibraries := Def.taskDyn[Map[Platform, File]]{ + val enableManaged = (enableNativeCompilation).value + if (enableManaged) Def.task { + val library: File = (nativeCompile in Compile).value + val platform = (nativePlatform in Compile).value + Map(platform -> library) + } else Def.task{ + Map() + } + }.value, + + resourceGenerators in Compile += Def.task { + + val libraries: Seq[(Patform, File)] = (managedNativeLibraries.value ++ unmanagedNativeLibraries.value).toSeq + + val resources: Seq[File] = for ((plat, file) <- libraries) yield { + + //native library as a managed resource file + val resource = (resourceManaged in Compile).value / + NativeLoader.fullLibraryPath( + (nativeLibraryPath).value, + plat) + + //copy native library to a managed resource (so it can also be loaded when not packaged as a jar) + IO.copyFile(file, resource) + resource + } + resources + }.taskValue, + + //don't add scala version to native jars + crossPaths := false + + ) + + override lazy val projectSettings = settings + /* + name in packageNative := name, + + + //this is file name of the artifact + artifactName in packageNative := { (sv: ScalaVersion, module: ModuleID, artifact: Artifact) => + artifact.name + "-native." + module.revision + "." + artifact.extension + } + + packageNative := { + val log = streams.value.log + + val mappings: Seq[(File, String)] = { + val libs = unmanagedNativeLibraries.value.toSeq ++ managedNativeLibraries.value.toSeq + libs.map{ case (platform, file) => + file -> NativeLoader.fullLibraryPath( + (nativeLibraryPath in Compile).value, + platform) + } + } + + val manifest = new java.util.jar.Manifest + val namer = (artifactName in packageNative).value + val jar: File = (target in nativeCompile).value / namer( + scalaVersion, + + (artifactName in packageNative).value + ) + + Package.makeJar(mappings, jar, manifest, log) + jar + }, + */ + + //Add native jar to runtime classpath. Note that it is not added as a resource (this would cause + //the native library to included in the main jar). + //unmanagedClasspath in Runtime += Attributed.blank(packageNative.value), + + //Fork new JVM when running locally, since native libraries can only be loaded once + //fork in run := true + + /* + override lazy val projectSettings = + Defaults.packageTaskSettings(packageNative, packageNativeMappings) ++ + addArtifact(artifact in packageNative, packageNative) ++ + inConfig(Compile)(settings) ++ Seq( + + packageNativeMappings := { + val libs = (unmanagedNativeLibraries in Compile).value.toSeq ++ + (managedNativeLibraries in Compile).value.toSeq + libs.map{ case (platform, file) => + file -> NativeLoader.fullLibraryPath( + (nativeLibraryPath in Compile).value, + platform) + } + }, + + artifactClassifier in packageNative := Some("native"), + + //this is file name of the artifact + artifactName := { (sv: ScalaVersion, module: ModuleID, artifact: Artifact) => + artifact.name + "-foso" + module.revision + "." + artifact.extension + }, + + artifact in packageNative := { + Artifact("foo", "jar", "jar", Some("native"), Nil, None, Map("platform" -> "all")) + }, + + crossVersion in packageNative := CrossVersion.Disabled +// projectID in packageNative := { projectID.value.copy(crossVersion = CrossVersion.Disabled) } + + + ) + */ + +} diff --git a/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniPackaging.scala b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniPackaging.scala new file mode 100644 index 0000000..6014086 --- /dev/null +++ b/jni-plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniPackaging.scala @@ -0,0 +1,113 @@ +package ch.jodersky.sbt.jni +package plugins + +import ch.jodersky.jni._ +import sbt._ +import sbt.Keys._ + +/** Packages libraries built with JniNative. */ +object JniPackaging extends AutoPlugin { + + //JvmPlugin is required or else resource generators will be overriden + override def requires = JniNative && plugins.JvmPlugin + override def trigger = allRequirements + + object autoImport { + + val nativeLibraryPath = settingKey[String]( + "String that is prepended to the path of a native library when packaged. This value should be " + + "passed to `NativeLoader.load()`" + ) + + val enableNativeCompilation = settingKey[Boolean]( + "Determines if native compilation is enabled. If disabled, only pre-compiled libraries in " + + "`unmanagedNativeDirectories` will be packaged." + ) + + val unmanagedNativeDirectories = settingKey[Seq[File]]( + "Unmanaged directories containing native libraries. The libraries must be regular files " + + "contained in a subdirectory corresponding to a platform. For example " + + "`/x86_64-linux/libfoo.so` is an unmanaged library for machines having " + + "the x86_64 architecture and running the Linux kernel." + ) + + val unmanagedNativeLibraries = taskKey[Map[Platform, File]]( + "Reads `unmanagedNativeDirectories` and maps platforms to library files specified theirin." + ) + + val managedNativeLibraries = taskKey[Map[Platform, File]]( + "Maps locally built, platform-dependant libraries." + ) + + } + import autoImport._ + import JniNative.autoImport._ + + lazy val settings: Seq[Setting[_]] = Seq( + + nativeLibraryPath := { + val orgPath = organization.value.replaceAll("\\.", "/") + s"/${orgPath}/${name.value}/native" + }, + + enableNativeCompilation := true, + + unmanagedNativeDirectories := Seq(baseDirectory.value / "lib_native"), + + unmanagedNativeLibraries := { + val dirs: Seq[File] = unmanagedNativeDirectories.value + val seq: Seq[(Platform, File)] = for ( + dir <- dirs; + if (dir.exists && dir.isDirectory); + platformDir <- dir.listFiles(); + if platformDir.isDirectory; + platform = Platform.fromId(platformDir.name); + library <- platformDir.listFiles(); + if library.isFile + ) yield { + platform -> library.getCanonicalFile() + } + + seq.toMap + }, + + managedNativeLibraries := Def.taskDyn[Map[Platform, File]] { + val enableManaged = (enableNativeCompilation).value + if (enableManaged) Def.task { + val library: File = nativeCompile.value + val platform = nativePlatform.value + Map(platform -> library) + } + else Def.task { + Map() + } + }.value, + + resourceGenerators += Def.task { + + val libraries: Seq[(Platform, File)] = (managedNativeLibraries.value ++ unmanagedNativeLibraries.value).toSeq + + val resources: Seq[File] = for ((plat, file) <- libraries) yield { + + //native library as a managed resource file + val resource = resourceManaged.value / + NativeLoader.fullLibraryPath( + (nativeLibraryPath).value, + plat + ) + + //copy native library to a managed resource (so it can also be loaded when not packaged as a jar) + IO.copyFile(file, resource) + resource + } + resources + }.taskValue, + + //don't add scala version to native jars + crossPaths in Global := false + + ) + + override lazy val projectSettings = inConfig(Compile)(settings) + +} diff --git a/samples/basic/basic-core/build.sbt b/samples/basic/basic-core/build.sbt new file mode 100644 index 0000000..cd51bb3 --- /dev/null +++ b/samples/basic/basic-core/build.sbt @@ -0,0 +1 @@ +//enablePlugins(JniLoader) diff --git a/samples/basic/basic-core/src/main/scala/ch/jodersky/jni/basic/Library.scala b/samples/basic/basic-core/src/main/scala/ch/jodersky/jni/basic/Library.scala new file mode 100644 index 0000000..efe3018 --- /dev/null +++ b/samples/basic/basic-core/src/main/scala/ch/jodersky/jni/basic/Library.scala @@ -0,0 +1,8 @@ +package ch.jodersky.jni.basic + +/** A demo object, mapping to a native library. */ +object Library { + + @native def print(message: String): Int + +} diff --git a/samples/basic/basic-core/src/main/scala/ch/jodersky/jni/basic/Main.scala b/samples/basic/basic-core/src/main/scala/ch/jodersky/jni/basic/Main.scala new file mode 100644 index 0000000..2aab1ed --- /dev/null +++ b/samples/basic/basic-core/src/main/scala/ch/jodersky/jni/basic/Main.scala @@ -0,0 +1,13 @@ +package ch.jodersky.jni.basic + +import ch.jodersky.jni.NativeLoader + +object Main { + + def main(args: Array[String]): Unit = { + NativeLoader.load("/ch/jodersky/jni/basic/native", "demo1") + val result: Int = Library.print("Hello world!\n") + println("Returned: " + result) + } + +} diff --git a/samples/basic/basic-main/src/main/scala/ch/jodersky/jni/basic/Library.scala b/samples/basic/basic-main/src/main/scala/ch/jodersky/jni/basic/Library.scala deleted file mode 100644 index efe3018..0000000 --- a/samples/basic/basic-main/src/main/scala/ch/jodersky/jni/basic/Library.scala +++ /dev/null @@ -1,8 +0,0 @@ -package ch.jodersky.jni.basic - -/** A demo object, mapping to a native library. */ -object Library { - - @native def print(message: String): Int - -} diff --git a/samples/basic/basic-main/src/main/scala/ch/jodersky/jni/basic/Main.scala b/samples/basic/basic-main/src/main/scala/ch/jodersky/jni/basic/Main.scala deleted file mode 100644 index b46f0d1..0000000 --- a/samples/basic/basic-main/src/main/scala/ch/jodersky/jni/basic/Main.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ch.jodersky.jni.basic - -import ch.jodersky.jni.NativeLoader - -object Main { - - def main(args: Array[String]): Unit = { - NativeLoader.load("/ch/jodersky/jni/basic", "demo1") - val r = Library.print("Hello world!\n") - println("Returned: " + r) - } - -} diff --git a/samples/basic/build.sbt b/samples/basic/build.sbt index cceeb16..7f5bedd 100644 --- a/samples/basic/build.sbt +++ b/samples/basic/build.sbt @@ -3,22 +3,29 @@ val commonSettings = Seq( organization := "ch.jodersky" ) -lazy val main = Project( - id = "sample-basic-main", - base = file("basic-main"), +lazy val root = Project( + id = "root", + base = file("."), + aggregate = Seq(core, native) +) + +lazy val core = Project( + id = "basic-core", + base = file("basic-core"), settings = commonSettings ++ Seq( - target in (Compile, javah) := - (sourceDirectory in native).value / "include" + target in javah in Compile := (sourceDirectory in native).value / "include" ), dependencies = Seq( native % Runtime ) -).enablePlugins(JniJvm) +).enablePlugins(JniLoading) lazy val native = Project( - id = "sample-basic-native", + id = "basic-native", base = file("basic-native"), settings = commonSettings ++ Seq( - jniLibraryPath in (Compile, jni) := "/ch/jodersky/jni/basic" + //enableNativeCompilation in Compile := false, + sourceDirectory in nativeCompile in Compile := sourceDirectory.value, + nativeLibraryPath in Compile := "/ch/jodersky/jni/basic/native" ) ).enablePlugins(JniNative) diff --git a/samples/basic/project/plugins.sbt b/samples/basic/project/plugins.sbt index 08f838a..c683b30 100644 --- a/samples/basic/project/plugins.sbt +++ b/samples/basic/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("ch.jodersky" % "sbt-jni" % "0.2-SNAPSHOT") +addSbtPlugin("ch.jodersky" % "sbt-jni" % "0.3-SNAPSHOT") -- cgit v1.2.3