diff options
-rw-r--r-- | flow-main/src/main/java/com/github/jodersky/flow/internal/NativeSerial.java | 2 | ||||
-rw-r--r-- | flow-main/src/main/scala/com/github/jodersky/flow/internal/NativeLoader.scala | 81 | ||||
-rw-r--r-- | project/Build.scala | 26 | ||||
-rw-r--r-- | project/native.scala | 188 |
4 files changed, 205 insertions, 92 deletions
diff --git a/flow-main/src/main/java/com/github/jodersky/flow/internal/NativeSerial.java b/flow-main/src/main/java/com/github/jodersky/flow/internal/NativeSerial.java index c159058..81bc9d5 100644 --- a/flow-main/src/main/java/com/github/jodersky/flow/internal/NativeSerial.java +++ b/flow-main/src/main/java/com/github/jodersky/flow/internal/NativeSerial.java @@ -25,7 +25,7 @@ import com.github.jodersky.flow.PortInterruptedException; final class NativeSerial { static { - NativeLoader.load("flow3"); + NativeLoader.load("flow3", "/com/github/jodersky/flow/native"); } final static int PARITY_NONE = 0; diff --git a/flow-main/src/main/scala/com/github/jodersky/flow/internal/NativeLoader.scala b/flow-main/src/main/scala/com/github/jodersky/flow/internal/NativeLoader.scala index a5fdc40..4254c5c 100644 --- a/flow-main/src/main/scala/com/github/jodersky/flow/internal/NativeLoader.scala +++ b/flow-main/src/main/scala/com/github/jodersky/flow/internal/NativeLoader.scala @@ -1,19 +1,48 @@ package com.github.jodersky.flow package internal -import java.io.{ File, FileOutputStream, InputStream, OutputStream } +import java.io.{File, FileOutputStream, InputStream, OutputStream} +import scala.io.Source +import scala.sys.process.Process +import scala.util.Try /** Handles loading of the current platform's native library for flow. */ object NativeLoader { - private final val BufferSize = 4096 + /** A platform is the representation of an os-architecture combination */ + case class Platform(kernel: String, arch: String) { + val id = kernel + "-" + arch + } + + object Platform { + + /** Create a platform with spaces stripped and case normalized. */ + def normalize(kernel: String, arch: String) = Platform( + kernel.toLowerCase.filter(!_.isWhitespace), + arch + ) + + /** Run 'uname' to determine current platform. Returns None if uname does not exist. */ + lazy val uname: Option[Platform] = { + val lines = Try { Process("uname -sm").lineStream.head }.toOption + lines.map { line => + val parts = line.split(" ") + if (parts.length != 2) { + sys.error("Could not determine platform: 'uname -sm' returned unexpected string: " + line) + } else { + Platform.normalize(parts(0), parts(1)) + } + } + } + + } - private def os = System.getProperty("os.name").toLowerCase.replaceAll("\\s", "") + private final val BufferSize = 4096 - private def arch = System.getProperty("os.arch").toLowerCase + private final val LibraryManifest = "library" /** Extract a resource from this class loader to a temporary file. */ - private def extract(path: String, prefix: String): Option[File] = { + private def extract(path: String): Option[File] = { var in: Option[InputStream] = None var out: Option[OutputStream] = None @@ -21,7 +50,7 @@ object NativeLoader { in = Option(NativeLoader.getClass.getResourceAsStream(path)) if (in.isEmpty) return None - val file = File.createTempFile(prefix, "") + val file = File.createTempFile(path, "") out = Some(new FileOutputStream(file)) val buffer = new Array[Byte](BufferSize) @@ -38,21 +67,41 @@ object NativeLoader { } } - private def loadFromJar(library: String) = { - val fqlib = System.mapLibraryName(library) //fully qualified library name - val path = s"/native/${os}-${arch}/${fqlib}" - extract(path, fqlib) match { - case Some(file) => System.load(file.getAbsolutePath) - case None => throw new UnsatisfiedLinkError("Cannot extract flow's native library, " + - "the native library does not exist for your specific architecture/OS combination." + - "Could not find " + path + ".") + private def loadError(msg: String): Nothing = throw new UnsatisfiedLinkError( + "Error during native library extraction " + + "(this can happen if your platform is not supported by flow): " + + msg + ) + + private def loadFromJar(libraryPrefix: String): Unit = { + val platformDir: String = libraryPrefix + "/" + Platform.uname.map(_.id).getOrElse { + loadError("Cannot determine current platform.") + } + + val manifest: File = { + val path = platformDir + "/" + LibraryManifest + extract(path) getOrElse { + loadError(s"Manifest file $path does not exist.") + } + } + + Source.fromFile(manifest, "utf-8").getLines foreach { libname => + val path = platformDir + "/" + libname + val lib = extract(path) getOrElse loadError( + s"Library $path not found." + ) + System.load(lib.getAbsolutePath()) } } - def load(library: String) = try { + /** + * Load a native library from the available library path or fall back + * to extracting and loading a native library from available resources. + */ + def load(library: String, libraryPrefix: String): Unit = try { System.loadLibrary(library) } catch { - case ex: UnsatisfiedLinkError => loadFromJar(library) + case ex: UnsatisfiedLinkError => loadFromJar(libraryPrefix) } } diff --git a/project/Build.scala b/project/Build.scala index 47ead93..3e5ba6f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1,15 +1,15 @@ -import sbt._ -import Keys._ import JniKeys._ -import NativeKeys._ - +import native.NativeDefaults +import native.NativeKeys._ +import sbt._ +import sbt.Keys._ object FlowBuild extends Build { val scalaVersions = List("2.11.7", "2.12.0-M2") - + lazy val commonSettings: Seq[Setting[_]] = Seq( - version := "2.3.1-SNAPSHOT", + version := "2.4.0-SNAPSHOT", scalaVersion in ThisBuild := scalaVersions.head, crossScalaVersions in ThisBuild := scalaVersions.reverse, scalacOptions ++= Seq("-deprecation", "-unchecked", "-feature", "-target:jvm-1.8"), @@ -30,7 +30,7 @@ object FlowBuild extends Build { </developers> } ) - + lazy val runSettings: Seq[Setting[_]] = Seq( fork := true, connectInput in run := true, @@ -48,7 +48,7 @@ object FlowBuild extends Build { publishTo := Some(Resolver.file("Unused transient repository", target.value / "unusedrepo")) // make sbt-pgp happy ) ) - + lazy val main: Project = ( Project("flow-main", file("flow-main")) settings(commonSettings: _*) @@ -69,7 +69,8 @@ object FlowBuild extends Build { settings( name := "flow-native", crossPaths := false, - nativeBuildDirectory := (baseDirectory in ThisBuild).value / "flow-native" + libraryPrefix in Compile := "com/github/jodersky/flow/native", + sourceDirectory in Native := (baseDirectory in ThisBuild).value / "flow-native" ) ) @@ -78,12 +79,7 @@ object FlowBuild extends Build { settings(commonSettings: _*) settings(runSettings: _*) dependsOn(main) - - //kind of dirty, but it gets the sample to run without installing native libraries - settings( - (run in Compile) <<= (run in Compile).dependsOn(nativeBuild in native), - javaOptions += "-Djava.library.path=" + (nativeOutputDirectory in native).value.getAbsolutePath() - ) + dependsOn(native) ) lazy val samplesWatcher = ( diff --git a/project/native.scala b/project/native.scala index 342a863..b6e4c19 100644 --- a/project/native.scala +++ b/project/native.scala @@ -1,83 +1,151 @@ -import sbt._ -import Keys._ -import java.io.File -import java.util.jar.Manifest - -object NativeKeys { +package native - val nativeBuildDirectory = settingKey[File]("Directory containing native build scripts.") - val nativeTargetDirectory = settingKey[File]("Base directory to store native products.") - val nativeOutputDirectory = settingKey[File]("Actual directory where native products are stored.") - val nativePackageUnmanagedDirectory = settingKey[File]("Directory containing external products that will be copied to the native jar.") +import java.io.File +import sbt._ +import sbt.Keys._ +import scala.util.Try - val nativeClean = taskKey[Unit]("Clean native build.") - val nativeBuild = taskKey[File]("Invoke native build.") +/** A platform is a the representation of an os-architecture combination */ +case class Platform(kernel: String, arch: String) { + val id = kernel + "-" + arch } -object NativeDefaults { - import NativeKeys._ - - val autoClean = Def.task { - val log = streams.value.log - val build = nativeBuildDirectory.value - - Process("make distclean", build) #|| Process("make clean", build) ! log +object Platform { + + /** Create a platform with spaces stripped and case normalized. */ + def normalize(kernel: String, arch: String) = Platform( + kernel.toLowerCase.filter(!_.isWhitespace), + arch + ) + + /** Run 'uname' to determine current platform. Returns None if uname does not exist. */ + lazy val uname: Option[Platform] = { + val lines = Try { Process("uname -sm").lines.head }.toOption + lines.map { line => + val parts = line.split(" ") + if (parts.length != 2) { + sys.error("Could not determine platform: 'uname -sm' returned unexpected string: " + line) + } else { + Platform.normalize(parts(0), parts(1)) + } } + } - val autoLib = Def.task { - val log = streams.value.log - val build = nativeBuildDirectory.value - val out = nativeOutputDirectory.value +} - val configure = Process( - "./configure " + - "--prefix=" + out.getAbsolutePath + " " + - "--libdir=" + out.getAbsolutePath + " " + - "--disable-versioned-lib", //Disable producing versioned library files, not needed for fat jars. - build) +object NativeKeys { - val make = Process("make", build) + val Native = config("native") - val makeInstall = Process("make install", build) + val platform = settingKey[Platform]("Platform of the system this build is being run on.") - val ev = configure #&& make #&& makeInstall ! log - if (ev != 0) - throw new RuntimeException(s"Building native library failed. Exit code: ${ev}") + //fat jar settings + val libraryPrefix = settingKey[String]("A string to be prepended to native products when packaged.") + val libraryManifest = settingKey[String]("Name of a file that will contain a list of all native products.") + val libraryResourceDirectory = settingKey[File]( + "Directory that contains native products when they treated as resources." + ) - (out ** ("*.la")).get.foreach(_.delete()) +} - out +/** Provides implementations of wrapper tasks suitable for projects using Autotools */ +object Autotools { + import NativeKeys._ + import sbt.Def.Initialize + + private val clean: Initialize[Task[Unit]] = Def.task { + val log = streams.value.log + val src = (sourceDirectory in Native).value + + Process("make distclean", src) #|| Process("make clean", src) ! log + } + + private val lib: Initialize[Task[File]] = Def.task { + val log = streams.value.log + val src = (sourceDirectory in Native).value + val out = (target in Native).value + val outPath = out.getAbsolutePath + + val configure = if ((src / "config.status").exists) { + Process("./config.status", src) + } else { + Process( + //Disable producing versioned library files, not needed for fat jars. + s"./configure --prefix=$outPath --libdir=$outPath --disable-versioned-lib", + src + ) } - val nativePackageMappings = Def.task { - val managedDir = nativeTargetDirectory.value - val unmanagedDir = nativePackageUnmanagedDirectory.value + val make = Process("make", src) - val managed = (nativeBuild.value ** "*").get - val unmanaged = (unmanagedDir ** "*").get + val makeInstall = Process("make install", src) - val managedMappings: Seq[(File, String)] = for (file <- managed; if file.isFile) yield { - file -> ("native/" + (file relativeTo managedDir).get.getPath) - } + val ev = configure #&& make #&& makeInstall ! log + if (ev != 0) + throw new RuntimeException(s"Building native library failed. Exit code: ${ev}") - val unmanagedMappings: Seq[(File, String)] = for (file <- unmanaged; if file.isFile) yield { - file -> ("native/" + (file relativeTo unmanagedDir).get.getPath) - } + val products: List[File] = (out ** ("*" -- "*.la")).get.filter(_.isFile).toList - managedMappings ++ unmanagedMappings + //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 } + } + + val settings: Seq[Setting[_]] = Seq( + Keys.clean in Native := Autotools.clean.value, + Keys.compile in Native := { + lib.value + sbt.inc.Analysis.Empty + }, + Keys.packageBin in Native := { + lib.value + } + ) +} - def os = System.getProperty("os.name").toLowerCase.filter(c => !c.isWhitespace) - def arch = System.getProperty("os.arch").toLowerCase - - val settings: Seq[Setting[_]] = Seq( - nativeTargetDirectory := target.value / "native", - nativeOutputDirectory := nativeTargetDirectory.value / (os + "-" + arch), - nativeClean := autoClean.value, - nativeBuild := autoLib.value, - nativePackageUnmanagedDirectory := baseDirectory.value / "lib_native", - mappings in (Compile, packageBin) ++= nativePackageMappings.value - ) +object NativeDefaults { + import NativeKeys._ + + /** Copy native product to resource directory and create manifest */ + private val libraryResources = Def.task { + val out = (libraryResourceDirectory in Compile).value + + val product = (packageBin in Native).value + + val productResource = out / product.name + val manifestResource = out / (libraryManifest in Compile).value + + IO.copyFile(product, productResource) + IO.write(manifestResource, productResource.name) + + Seq(productResource, manifestResource) + } + + private val fatJarSettings = Seq( + libraryPrefix in Compile := "", + libraryManifest in Compile := "library", + libraryResourceDirectory in Compile := (resourceManaged in Compile).value / + (libraryPrefix in Compile).value / (platform in Native).value.id, + unmanagedResourceDirectories in Compile += (baseDirectory).value / "lib_native", + resourceGenerators in Compile += libraryResources.taskValue + ) + + val settings: Seq[Setting[_]] = Seq( + platform in Native := Platform.uname.getOrElse { + System.err.println("Warning: Cannot determine platform! It will be set to 'unknown'.") + Platform("unknown", "unknown") + }, + target in Native := target.value / "native" / (platform in Native).value.id + ) ++ fatJarSettings ++ Autotools.settings } |